December 10, 2024

Обходим защиту от дизассемблирования в Golang

tg: AFANX
канал: K0N70R4
GitHub: GitHub

Ку, киберпанки. Сегодня будет разбор на один интересный таск с CTF, который был на этих выходных. В таске встретите защиту от дизассемблирования и необходимость написания кейгена, а также реверс программы, которая написана на Golang. Сам таск отнесу в категорию easy.

Изучаем прогу

Закинем в DiE (Detect It Easy) и посмотрим стороки, энтрипоинт, обфусцирован ли он, на чём написал и чем скомпилен:

Наш энтрипоинт располагается по адресу 0xA9BB40 и этот бинарь написан на Golang, что не есть хорошо. Это плохо, потому что Go компилирует всё статически и оставляет очень много мусора. Теперь попробуем закинуть в IDA Pro и посмотреть, что под капотом. Во время трассировки, я думал, что у меня комп взлетит и понятно почему:

С каждой секундой он находил новую функцию и не мог быстро трассировать. Сразу стало ясно, что это защита от дизассемблирования.

Обходим защиту от дизассемблирования

IDA и GHIDRA не справились с задачей дизассемблирования, поэтому решил сделать это через objdump:

objdump -A memes.exe -M intel

Как видите много одних и тех же инструкций, причем они все ссылаются друг на друга. Короче говоря, это такая защита от дизассемлирования. Последняя интсрукция указывает на ту, которая в начале, а самая первая на следующую. По DiE знаем, что энтрипоинт по адресу 0xa9bb40, т.е.

a9bb40:	e9 f6 ff ff ff       	jmp    0xa9bb3b

Заполнить всё это NOPами (0x90) можно, только получим чисто дизассемблирования, а нам нужна еще отладка. Посмотрим чем всё это заканчивается:

Теперь посмотрим, что находится по адресу 0x46d9e0:

Это уже похоже на нормальный энтрипоит и на загрузку проги. Таким образом, необходимо запатчить прогу так:

  1. Все jmp заменить на nop
  2. Первый jmp сделать на адрес 0x46A140

После таких патчей сможем избавиться от защиты от дизассемблирования и нормально отлаживать программу.

Патчим

В HxD нахожу первую инструкцию по байтам E9 F6 FF FF FF :

После этого заменил всё, до первой такой инструкции (энтипоинт 0xA9BB40):

Так как, теперь это хорошо открывается в IDA, патчинг закончу там.

Вот мой энтрипоинт:

Патчим на интересующий нас адрес - 0x46A140.

Теперь можно поставить точку останова на main и начать реверсить!

Реверс

В начале есть интересная функция под названием main_keygen. Она формирует ключ шифрования.

В начале есть такой блок кода:

Это зашифрованный ключ в hex'ах. Расшифровывается так:

Первый шаг - побайтный XOR с 0x66

Второй шаг - побайтный XOR с 0x99

Третий шаг - побайтный XOR с 0x37

Таким образом, получим такой ключ:

В памяти видим, что две одинаковые строки, но это нужно будет для дальнейшего шифорвания.

Дальше обращаем внимание на следующих блок:

Это наш зашифрованный флаг, который будет побайтно сравниваться с нашей строкой.

Само шифрование происходит в функции main_generator:

В main_generator строка проходит пять этапов шифрования:

Первый шаг - каждый байт ксорится с ключём encr7pt1on_k3yencr7pt1on_k3y

Второй шаг - каждый байт ксорится по кругу с захардкоженными значениями 0xE5, 0x99, 0xB3:

Третий шаг - каждый байт складывается с 0x65:

Четвертый шаг - каждый байт складывается с 0x7D:

Последний шаг - каждый байт ксорится с другими захардкоженными значениями 0xE8,0x90,0xB6:

После выхода из функции будет сравнение длины введенной строки с длиной флага. Длина должна быть равна 28 и происходит сравнение строк:

После реверса становится ясно, что без написания кейгена будет сложно расшифровать флаг, да и к тому же все операции обратимы.

Пишем кейген

Для тестирования взял такую строку: ABCDEFGHIJKLMNOPQRSTUVWXYZ{}.

Первым шагом был ксор строки с ключом encr7pt1on_k3yencr7pt1on_k3y:

for i in range(len(test)):
	print(hex(ord(test[i]) ^ ord(key[i])),end=" ")
	res_1.append(ord(test[i]) ^ ord(key[i]))

Дальше ксор с захардкоженными значениями:

round_key_2 = [0xE5, 0x99, 0xB3]
for i in range(len(res_1)):
	print(hex(res_1[i] ^ round_key_2[i%3]),end=" ")
	res_2.append(res_1[i] ^ round_key_2[i%3])

После сложение с 0x65 и 0x7d:

for i in range(len(res_2)):
	print(hex((((res_2[i] + 0x65)%0x100) + 0x7D)%0x100),end=" ")
	res_3.append((res_2[i] + 0x65)%0x100) 

Последний шаг - ксор со вторыми захардкоженными значениями:

round_key_3 = [0xE8, 0x90, 0xB6]
for i in range(len(res_4)):
	print(hex(res_4[i] ^ round_key_3[i%3]),end=" ")
	res_5.append(res_4[i] ^ round_key_3[i%3])

Таким образом, полный алгоритм шифрования выглядит так:

test = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"
key = "encr7pt1on_k3yencr7pt1on_k3y"
round_key_2 = [0xE5, 0x99, 0xB3]
round_key_3 = [0xE8, 0x90, 0xB6]

res_1 = []
res_2 = []
res_3 = []
res_4 = []
res_5 = []

print("ROUND 1")
# round 1
for i in range(len(test)):
	print(hex(ord(test[i]) ^ ord(key[i])),end=" ")
	res_1.append(ord(test[i]) ^ ord(key[i]))

#round 2
print("")
print("ROUND 2")
for i in range(len(res_1)):
	print(hex(res_1[i] ^ round_key_2[i%3]),end=" ")
	res_2.append(res_1[i] ^ round_key_2[i%3])

#round 3
print("")
print("ROUND 3")
for i in range(len(res_2)):
	print(hex((res_2[i] + 0x65)%0x100),end=" ")
	res_3.append((res_2[i] + 0x65)%0x100)

#round 4
print("")
print("ROUND 4")
for i in range(len(res_3)):
	print(hex((res_3[i] + 0x7D)%0x100),end=" ")
	res_4.append((res_3[i] + 0x7D)%0x100)

#round 5
print("")
print("ROUND 5")
for i in range(len(res_4)):
	print(hex(res_4[i] ^ round_key_3[i%3]),end=" ")
	res_5.append(res_4[i] ^ round_key_3[i%3])


print("\n")
print("RESULT")
for i in res_5:
	print(hex(i), end=" ")

Выводит следующее:

И проверим, что выведет бинарь:

Получили тоже самое. Теперь осталось это всё обратить и получим флаг.

Кейген выглядит так:

flag = [0x20,0xed,0x25,0x3a,0x27,0x30,0x6b,0x49,0x79,0x32,0x1a,0x38,0x83,0xe5,0x2b,0x29,0xe4,0x36,0x70,0x2a,0x35,0x7c,0x39,0x16,0x59,0x10,0x2f,0x2b]

key = "encr7pt1on_k3yencr7pt1on_k3y"
round_key_2 = [0xE5, 0x99, 0xB3]
round_key_3 = [0xE8, 0x90, 0xB6]

res_1 = []
res_2 = []
res_3 = []
res_4 = []
res_5 = []

for i in range(len(flag)):
	print(hex(flag[i] ^ round_key_3[i%3]),end=" ")
	res_1.append(flag[i] ^ round_key_3[i%3])

for i in range(len(res_1)):
	print(hex((res_1[i] - 0x7D)%0x100),end=" ")
	res_2.append((res_1[i] - 0x7D)%0x100)

for i in range(len(res_2)):
	print(hex((res_2[i] - 0x65)%0x100),end=" ")
	res_3.append((res_2[i] - 0x65)%0x100)

for i in range(len(res_1)):
	print(hex(res_3[i] ^ round_key_2[i%3]),end=" ")
	res_4.append(res_3[i] ^ round_key_2[i%3])

for i in range(len(res_4)):
	print(hex(res_4[i] ^ ord(key[i])),end=" ")
	res_5.append(chr(res_4[i] ^ ord(key[i])))

print("\n\n")
for i in res_5:
    print(i,end="")