Skip to content

Instantly share code, notes, and snippets.

@vient
Last active September 29, 2019 17:19
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vient/a668d55a22d214e75676126d3ceecadc to your computer and use it in GitHub Desktop.
Save vient/a668d55a22d214e75676126d3ceecadc to your computer and use it in GitHub Desktop.
Zeronights 2017 HackQuest Day #2 Writeup

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 за нас подбирает решение системы.

import sys
sys.path.append(r'C:\tools\z3-4.5.0-x64-win\bin\python')
from z3 import *
init(r'C:\tools\z3-4.5.0-x64-win\bin')
mail = 'zn2017@reverse4you.org'
cons = True
def add_con(con):
global cons
cons = And(cons, con)
key = [[Int('key_{}_{}'.format(i, j)) for j in range(4)] for i in range(6)]
# for i in range(len(key)):
# add_con((key[i][0] + key[i][1] + key[i][2]) % 5 == key[i][3] % 3)
for i in range(len(key)):
for j in range(len(key[i])):
add_con(
Or(
And(key[i][j] >= ord('0'), key[i][j] <= ord('9')),
And(key[i][j] >= ord('A'), key[i][j] <= ord('Z'))))
keypart_sums = [sum(key[i]) for i in range(len(key))]
m = [[None for j in range(4)] for i in range(4)]
for i in range(4):
for j in range(4):
m[i][j] = (keypart_sums[i] + keypart_sums[j]) % 169
mail_ext = (mail * 100)[:64]
mail_sums = [sum(map(ord, mail_ext[i::4]), 1) % 13 for i in range(4)]
for i in range(4):
m[i][i] = mail_sums[i]
col_sums = [sum(m[j][i] for j in range(4)) for i in range(4)]
add_con(col_sums[0] == col_sums[1])
add_con(col_sums[1] == col_sums[2])
add_con(col_sums[2] == col_sums[3])
add_con(sum(map(ord, '%02X%02X' % (ord(mail[0]), ord(mail[1])))) + 0x5A1C + keypart_sums[4] == 0x5C44)
add_con(sum(map(ord, '%02X%02X' % (ord(mail[4]), ord(mail[5])))) + 0x2FABE + keypart_sums[5] == 0x2FCE6)
# print(cons)
cons = simplify(cons)
# print(cons)
s = Solver()
s.add(cons)
print(s.check())
model = s.model()
print('-'.join(''.join(chr(model[key[i][j]].as_long()) for j in range(4)) for i in range(6)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment