Your friend works in an antivirus company. He developed a new algorithm for generating a license key and asks you to test it.
Нам дан архив с исполняемым файлом ELF x86_64 "petrovavlic". Недолго думая, открываем его в IDA, и видим, что он запакован UPX 3.94. Сам UPX распаковать его не может, автор вырезал имена секций. Каким-нибудь образом его распаковываем, например, восстановлением названий, и продолжаем.
По строкам из распакованного файла сразу понятно, что он написан на Go. Из них же и узнаем об авторе задания.
00000fb0: 2800 0000 0400 0000 476f 0000 3766 6661 (.......Go..7ffa
00000fc0: 3865 6437 3736 6134 3236 3237 3165 3864 8ed776a426271e8d
00000fd0: 6664 3937 3062 3530 6330 3163 6637 3666 fd970b50c01cf76f
0024e7e0: 44eb 0900 2f68 6f6d 652f 6b72 656f 6e2f D.../home/kreon/
0024e7f0: 476f 676c 616e 6450 726f 6a65 6374 732f GoglandProjects/
0024e800: 7461 736b 3230 302f 766d 2e67 6f00 002f task200/vm.go../
0024e810: 686f 6d65 2f6b 7265 6f6e 2f47 6f67 6c61 home/kreon/Gogla
0024e820: 6e64 5072 6f6a 6563 7473 2f74 6173 6b32 ndProjects/task2
0024e830: 3030 2f6d 6169 6e2e 676f 0000 2f68 6f6d 00/main.go../hom
...
Бинарь постриплен — стандартная отладочная информация отсутствует. К счастью, в Go для рефлексии в секции .gopclntab
сохраняются названию всех функций, и легко найти готовые скрипты для их восстановления, например, этот.
Все названия восстановлены — направляемся прямиком в main.main
.
- Side note: в golang используется нестандартное соглашение о вызовах. В x86_64 стандартным является только одно, fastcall. В golang не используются регистры для передачи параметров, а возвращаемые значения (их может быть больше одного QWORD) кладутся на стек. Это доставляет определённые неудобства при использовании Hex-Rays
Там происходит примерно это:
main.__pre__start()
fmt.Println("PetrovAntivirus Activator")
fmt.Print( "Please enter a valid email: ")
bufio._p_Reader_.ReadString(email)
main.__check__email(email)
fmt_Print( "Please enter an activation key: ")
bufio._p_Reader_.ReadString(key)
main.__check__key(key)
table = main.__gen__table(email, key)
main.__check_key_e(email, key, table)
Разберём вызовы по порядку.
main.__pre__start()
: устанавливаются обработчики сигналов и происходит несколько системных вызовов SYS_ptrace с параметром PTRACE_TRACEME. Таким образом, в том числе, становится невозможно дебажить бинарь. Для нормального дебага можно вырезать установку сигналов и системные вызовы. Почему нельзя просто вырезать вызов main.__pre__start()
? Для получения номеров системных вызовов используется функция main._p_syscall__table.__get__syscall__id
, в которой находится большой свитч. Он смотрит текущее значение системного вызова, определает по нему следующий и сохраняет. Таким образом, если не вызвать эту функцию один раз, все её дальнейшие результаты окажутся невалидны.
main.__check__email(email)
: почта просто проверяется на нормальный вид.
main.__check__key(key)
: проверяется, что ключ имеет вид XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
, где X
это [0-9A-Z]
.
main.__gen__table(email, key)
: тут начинаются первые сложности. Подсчитывается сумма 5 и 6 блоков ключа (ord(key[0]) + ...
), также подсчитывается MD5 почты, и этот хеш никак не используется. Делается sprintf("%02X%02X", ...)
для email[0:1]
и email[4:5]
, далее для этих строк из 4 символов по тому же принципу подсчитываются суммы. Затем считается результат, пара чисел:
syscall_id_1 = main__p_syscall__table____get__syscall_id(&a1);
syscall_id_2 = main__p_syscall__table____get__syscall_id(&a1);
table[0] = Part5_sum + 4 * syscall_id_1 * syscall_id_2 + EMAIL__0_1_sum;
syscall_id_3 = main__p_syscall__table____get__syscall_id(&a1);
syscall_id_4 = main__p_syscall__table____get__syscall_id(&a1);
table[1] = Part6_sum& + 2 * syscall_id_3 * syscall_id_4 + EMAIL__4_5_sum;
Таким образом, в расчёте неких двух чисел участвует email и последние 2 блока ключа.
И вот мы подошли к главной функции: main.__check_key_e(email, key, table)
. Почти первой же строчкой идёт такой вызов github_com_Shopify_golua_NewState();
. Название говорит само за себя, это модуль для исполнения Lua в Go. Таким образом, где-то в бинаре спрятан проверочный скрипт на Lua.
Далее по ходу функции нужно выделить вызовы github_com_Shopify_golua__p_State__Register
, которые регистрируют в виртуальной машине Lua внешние функции, написанные на Go. Таких внешних функций 4: getkey — получение key в виде 6 блоков, getmail — получение email, goodkey — сообщение об успехе, badkey — о неудаче. После этого происходит github_com_Shopify_golua__p_State__Load(...)
и сразу за ним github_com_Shopify_golua__p_State__ProtectedCall(...)
, то есть запускается проверочный скрипт.
Откуда берётся проверочный скрипт? Исходный код go-lua говорит, что первым аргументом в Load идёт io.Reader, из которого читается скрипт. io.Reader это интерфейс, в котором есть ровно один метод: Read. Поискав функции с _Read
в названии, находим интересную _home_kreon_GoglandProjects_task200_eblob__p_BlobReader__Read
. Её полный код с небольшими изменениями:
__int64 __usercall _home_kreon_GoglandProjects_task200_eblob__p_BlobReader__Read@<rax>(_QWORD *a1, _BYTE *a2, unsigned __int64 a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v3 = qword_6B69F8;
v4 = a1[2] - a1[4];
if ( (signed __int64)a3 <= v4 )
v4 = a3;
for ( i = 0LL; (signed __int64)i < v4; ++i )
{
v6 = a1[4];
j = v6 + *a1;
if ( j >= qword_6AF6A8 || (v8 = EncBlob[j], k = a1[1] + v6, k >= qword_6AF6A8) || (v10 = EncBlob[k] ^ v8, i >= a3) )
runtime_panicindex(a2, a3, a1);
a2[i] = v10;
++a1[4];
}
if ( a1[4] >= a1[2] )
result = v3;
else
result = 0LL;
return result;
}
Видно, что a[0]
и a[1]
это некоторые оффсеты в EncBlob, по которым находится 2 массива. В цикле берутся последовательные элементы из этих массивов и ксорятся. Логично предположить, что в этом массиве и спрятан скрипт.
Для поиска скрипта можно перебрать все возможные оффсеты, поксорить пару чисел из этих адресов и посмотреть на результат. Мы уже знаем, что в скрипте вызываются функции goodkey и badkey, можно поискать DWORD 'good' и найти нужные оффсеты: 23620 и 195814 (о чём и была третья подсказка). Также можно заметить, что перед вызовом Load создаётся объект Reader, в который в [0]
и [1]
записываются наши результаты __gen__table
. Значит, для подсчитанных в __gen__table
значений известно, какими они должны быть, следовательно, это тоже проверка ключа.
Исходный код скрипта (с небольшим рефакторингом):
local KEY = getkey()
local MAIL = getmail()
local keypart_sums = {}
local M = {}
local keypart_it = 1
local MAIL_extended = ""
local MAIL_ext_sums = {1,1,1,1}
keypart_it = 1
for i=1,4 do
local keypart_sum = 0
local keypart_len = 0
for c=1,KEY[i]:len() do
keypart_sum = keypart_sum + KEY[i]:byte(c)
keypart_len = keypart_len +1
end
if keypart_len ~= 4 then
return badkey()
end
keypart_sums[keypart_it] = keypart_sum
keypart_it = keypart_it +1
end
for i=1,4 do
for j=1,4 do
M[(i - 1) * 4 + j] = (keypart_sums[i] + keypart_sums[j]) % 169
end
end
while string.len(MAIL_extended) < 64 do
MAIL_extended = MAIL_extended .. MAIL
end
keypart_it = 1
local MAIL_ = 1
for c=1,64 do
MAIL_ext_sums[MAIL_] = MAIL_ext_sums[MAIL_] + MAIL_extended:byte(c)
MAIL_ = MAIL_ + 1
keypart_it = keypart_it + 1
if MAIL_ == 5 then
MAIL_ = 1
end
end
for i=1,4 do
MAIL_ext_sums[i] = MAIL_ext_sums[i] % 13
end
keypart_it = 1
for i=1,16,5 do
M[i] = MAIL_ext_sums[keypart_it]
keypart_it = keypart_it + 1
end
local v________ = {}
for i=1,4 do
s = 0
for j=1,4 do
s = s + M[(j - 1)*4 + i]
end
v________[s] = 1
end
local pairs_num = 0
for k,v in pairs(v________) do
pairs_num = pairs_num + 1
end
if pairs_num == 1 then
goodkey()
else
badkey()
end
Вкратце, в скрипте так же считаются суммы кодов символов, складываются друг с другом в матрицу 4х4, туда же записываются суммы кодов символов в email, и проверяется, что суммы столбцов матрицы равны между собой.
У нас есть все проверки. Чтобы с их помощью сгенерировать ключ, можно использовать z3. Полный скрипт лежит в keygen.py, в нём мы создаём символический ключ и добавляем в решатель все найденные ограничения, затем z3 за нас подбирает решение системы.