Данное задание подготовлено сообществом SPbCTF https://vk.com/spbctf
Мы дизассемблировали огромный корпоративный продукт,
и нашли обфусцированную функцию проверки ключа.
Все 30 гигабайт этого продукта передать вам не сможем — NDA,
— поэтому вырезали для вас только байты самой функции
(ссылка)
Её общий вид:
int check_key(char * key<rdi>) {
if (… key_is_valid ...)
return 1
else
return 0;
}
Какой верный ключ?
Формат ответа: yaprofi{...}
385 байт шеллкода.
Поставим образовательную цель - разобрать по кирпичикам, что делает эта программа. Шеллкод простой, поэтому имеет смысл сперва самому изучить, а в случае затупа - обратиться к этому разбору. РАЗБИРАТЬ ВЕСЬ ШЕЛЛКОД ВО ВРЕМЯ СОРЕВНОВАНИЯ НЕ РЕКОМЕНДУЕТСЯ, но для развития интуиции и понимания, как выглядят мусорные инструкции, имеет смысл.
Начало:
00000000 E8 00 00 00 00 call $+5
00000005 58 pop eax
00000006 48 dec eax
00000007 83 C0 07 add eax, 7
0000000A 50 push eax
0000000B C3 retn
Первая инструкция - пустой call
, который переместит нас на следующую инструкцию, а также
поместит ее адрес (0x5
) в стек.
Вторая инструкция заберет из стека адрес и положит его в eax
.
Следуюшими двумя инструкциями произойдет eax = eax -1 + 7 = 0xb
. Затем он запушит
0xb
на стек и…
Wait a minute. Он исполнит два раза return
и пошлет нас. А всё потому, что
разрядность у нас 64 бита, что намекалось в задании (используется регистр rdi
).
Как раз на тему распознания x32/x64 недавно был твит.
Теперь будем работать с x64:
0000000000000000 E8 00 00 00 00 call $+5
0000000000000005 58 pop rax
0000000000000006 48 83 C0 07 add rax, 7
000000000000000A 50 push rax
000000000000000B C3 retn
Этот блок не совершает никакой полезной работы, при этом мы забудем предыдущее значение rax
.
Но согласно тексту задания мы знаем,
что параметр передавался через rdi
, так что эффект нулевой.
000000000000000C 55 push rbp
000000000000000D 53 push rbx
000000000000000E 51 push rcx
000000000000000F 52 push rdx
0000000000000010 50 push rax ; запомните этот момент
0000000000000011 E8 00 00 00 00 call $+5
0000000000000016 58 pop rax
0000000000000017 48 83 C0 07 add rax, 7
000000000000001B 50 push rax
000000000000001C C3 retn
Аналогично, бесполезный блок, но теперь на стеке лежат rax
=0x0C, rdx
=?, rcx
=?, rbx
=?, rbp
=?.
000000000000001D 58 pop rax ; запомните этот момент
; начало блока 1E...24
000000000000001E 53 push rbx
000000000000001F 52 push rdx
0000000000000020 5B pop rbx
0000000000000021 5A pop rdx
0000000000000022 48 87 D3 xchg rdx, rbx
; конец блока. В итоге всё осталось на своих местах
; ничего не значит, ведь для нас в этих регистрах нет значимой информации
0000000000000025 0F 57 FD xorps xmm7, xmm5
; обнулим rdx
0000000000000028 48 31 D2 xor rdx, rdx
; и поместим туда пользовательские данные, которые по условиям задачи находятся в rdi
000000000000002B 48 31 FA xor rdx, rdi
; еще один блок - 2e..38. Смысл - зануление регистра
000000000000002E B9 00 00 00 00 mov ecx, 0
0000000000000033 48 F7 D1 not rcx
0000000000000036 48 FF C1 inc rcx
; ранее знакомый блок 1E-24
0000000000000039 52 push rdx
000000000000003A 56 push rsi
000000000000003B 5A pop rdx
000000000000003C 5E pop rsi
000000000000003D 48 87 F2 xchg rsi, rdx
; запушим 0x0C
0000000000000040 50 push rax ; запомните этот момент
; знакомое ничего
0000000000000041 E8 00 00 00 00 call $+5
0000000000000046 58 pop rax
0000000000000047 48 83 C0 07 add rax, 7
000000000000004B 50 push rax
000000000000004C C3 retn
В результате в rdx
попало значение из rdi
, всё остальное осталось по-прежнему.
000000000000004D 58 pop rax ; запомните этот момент
; Обмен значениями, используя три ксора. Но для нас это снова не несет смысла -
; значения регистров неизвестны для нас
000000000000004E 0F 57 F7 xorps xmm6, xmm7
0000000000000051 0F 57 FE xorps xmm7, xmm6
0000000000000054 0F 57 F7 xorps xmm6, xmm7
; просто зануление rax, мы видели схожий блок
0000000000000057 B8 00 00 00 00 mov eax, 0
000000000000005C 48 F7 D0 not rax
000000000000005F 48 FF C0 inc rax
; rcx = FF FF FF FF FF FF FF FF
0000000000000062 48 F7 D1 not rcx
; происходит циклическое сравнение байта, на который указывает адрес rdi, и значения
; регистра al т.к. al = 0 (это младший байт ранее обнуленного RAX), а rdi указывает
; на начало пользовательского ключа, то смысл операции - подсчитать длину ключа
; У операции два условия остановки - когда rcx == 0 и когда значение байта по адресу
; rdi будет равно al
; Поэтому счетчик ecx занегативили - чтобы он не мешался
; в результате rcx-=длина, rdi+=длина. Эта длина включает в себя нулевой символ в конце
0000000000000065 F2 AE repne scasb
; обратно инвертировали, теперь rcx должен быть равен rdi
0000000000000067 48 F7 D1 not rcx
; пустышка
000000000000006A 48 8D 36 lea rsi, [rsi]
; пустышка
000000000000006D 48 87 C9 xchg rcx, rcx
; вычитаем из длины нулевой символ
0000000000000070 48 FF C9 dec rcx
; сравниваем с 0d32
0000000000000073 48 83 F9 20 cmp rcx, 20h
; если длина не совпала - прыжок в deadend
0000000000000077 0F 85 FA 00 00 00 jnz loc_177
000000000000007D 50 push rax ; запомните этот момент
; знакомое ничего
000000000000007E E8 00 00 00 00 call $+5
0000000000000083 58 pop rax
0000000000000084 48 83 C0 07 add rax, 7
0000000000000088 50 push rax
0000000000000089 C3 retn
Посмотрите на места с комментарием “запомните этот момент”. Не только retn, но и наличие этих пар push-pop позволяет нам разделять блоки.
Отлично, теперь мы знаем длину, которую от нас ожидают - 32 символа.
000000000000008A 58 pop rax
; бесполезно
000000000000008A 58 pop rax
000000000000008B 48 87 D2 xchg rdx, rdx
000000000000008E 48 87 C9 xchg rcx, rcx
0000000000000091 48 31 FF xor rdi, rdi
0000000000000094 48 31 D7 xor rdi, rdx
; а вот это уже интересно. Прыжок в середину инструкции. Это произошло из-за того,
; что Иде удалось линейно дизассемблрировать инструкции ниже, хотя на самом деле
; это не код, а данные. В таких случаях нужно сказать Иде разопределить/удалить
; информацию о том, что инструкции, начиная с 0x9C, это код (хоткей U), и
; определить инструкции, начиная с 0xA9+3 = 0xAC как код (хоткей C)
0000000000000097 E8 10 00 00 00 call near ptr loc_A9+3
000000000000009C 22 48 D7 and cl, [rax-29h]
000000000000009F EE out dx, al
00000000000000A0 38 37 cmp [rdi], dh
00000000000000A2 52 push rdx
00000000000000A3 FD std
00000000000000A4 12 44 13 CD adc al, [rbx+rdx-33h]
00000000000000A8 C9 leave
00000000000000A9 loc_A9: ; CODE XREF: seg000:0000000000000097↑p
00000000000000A9 2D 95 15 5B 0F sub eax, 0F5B1595h
00000000000000AE 57 push rdi
После:
000000000000008A 58 pop rax
000000000000008B 48 87 D2 xchg rdx, rdx
000000000000008E 48 87 C9 xchg rcx, rcx
0000000000000091 48 31 FF xor rdi, rdi
0000000000000094 48 31 D7 xor rdi, rdx
0000000000000097 E8 10 00 00 00 call loc_AC
0000000000000097 ; ---------------------------------------------------------------------------
000000000000009C 22 db 22h ; "
000000000000009D 48 db 48h ; H
000000000000009E D7 db 0D7h
000000000000009F EE db 0EEh
00000000000000A0 38 db 38h ; 8
00000000000000A1 37 db 37h ; 7
00000000000000A2 52 db 52h ; R
00000000000000A3 FD db 0FDh
00000000000000A4 12 unk_A4 db 12h ; CODE XREF: seg000:00000000000000DB↓j
00000000000000A5 44 db 44h ; D
00000000000000A6 13 db 13h
00000000000000A7 CD db 0CDh
00000000000000A8 C9 db 0C9h
00000000000000A9 2D db 2Dh ; -
00000000000000AA 95 db 95h
00000000000000AB 15 db 15h
00000000000000AC ; ---------------------------------------------------------------------------
00000000000000AC
; теперь выглядит лучше
00000000000000AC loc_AC: ; CODE XREF: seg000:0000000000000097↑p
; rbx = 9c, это адрес следующей инструкции после call
00000000000000AC 5B pop rbx
; бесполезные 2 инструкция
00000000000000AD 0F 57 FE xorps xmm7, xmm6
00000000000000B0 0F 57 FE xorps xmm7, xmm6
; обмен значениями между rcx и rdi
00000000000000B3 51 push rcx
00000000000000B4 57 push rdi
00000000000000B5 59 pop rcx
00000000000000B6 5F pop rdi
; и обратно обмен между rcx и rdi
00000000000000B7 48 87 F9 xchg rdi, rcx
; две бесполезные инструкции
00000000000000BA 48 87 FF xchg rdi, rdi
00000000000000BD 48 87 FF xchg rdi, rdi
; четыре бесполезные инструкции
00000000000000C0 52 push rdx
00000000000000C1 52 push rdx
00000000000000C2 5A pop rdx
00000000000000C3 5A pop rdx
; еще две бесполезные инструкции
00000000000000C4 48 87 D2 xchg rdx, rdx
00000000000000C7 48 87 C9 xchg rcx, rcx
; в xmm0 помещаются 16 байт, располагающиеся в 9c..ab
00000000000000CA 0F 10 03 movups xmm0, xmmword ptr [rbx]
; Прыгаем на E2. Скорее, как и в предыдущем случае, инструкции D2..E1 (32 байта) также
; будут помещены в один из xmm-регистров
00000000000000CD E8 10 00 00 00 call sub_E2
00000000000000D2 E5 B5 in eax, 0B5h ; Interrupt Controller #2, 8259A
00000000000000D4 7C 58 jl short near ptr loc_12C+2
Смотрим в место, куда происходит прыжок
00000000000000E2 sub_E2 proc near ; CODE XREF: seg000:00000000000000CD↑p
; rbx = D2
00000000000000E2 5B pop rbx
; бесполезная инструкция
00000000000000E3 48 8D 09 lea rcx, [rcx]
; в xmm1 помещаются байты по адресам d2..e1
00000000000000E6 0F 10 0B movups xmm1, xmmword ptr [rbx]
; снова прыжок, в стек падает EE
00000000000000E9 E8 10 00 00 00 call sub_FE
Аналогичное происходит и с xmm2:
00000000000000FE 5B pop rbx
00000000000000FF 0F 10 13 movups xmm2, xmmword ptr [rbx]
0000000000000102 E8 10 00 00 00 call sub_117
И с xmm3:
0000000000000117 5B pop rbx
0000000000000118 0F 10 1B movups xmm3, xmmword ptr [rbx]
То есть регистры xmm0…xmm3 заполнены предопределенными данными.
Идем дальше:
; 5 бесполезных инструкций
000000000000011B 53 push rbx
000000000000011C 56 push rsi
000000000000011D 5B pop rbx
000000000000011E 5E pop rsi
000000000000011F 48 87 F3 xchg rsi, rbx
; в xmm4 попадает пользовательские 32 байта
0000000000000122 0F 10 27 movups xmm4, xmmword ptr [rdi]
0000000000000125 50 push rax
; известный прием
0000000000000126 E8 00 00 00 00 call $+5
000000000000012B 58 pop rax
000000000000012C 48 83 C0 07 add rax, 7
0000000000000130 50 push rax
0000000000000131 C3 retn
И в xmm4 хранится пользовательский ввод
; забрали старое значение
0000000000000132 58 pop rax
; 5 бесполезных инструкций
0000000000000133 50 push rax
0000000000000134 56 push rsi
0000000000000135 58 pop rax
0000000000000136 5E pop rsi
0000000000000137 48 96 xchg rax, rsi
; бесполезная инструкция
0000000000000139 48 8D 12 lea rdx, [rdx]
; xmm4 ^= xmm3
000000000000013C 0F 57 E3 xorps xmm4, xmm3
; вероятно, мусорная инструкция
000000000000013F 48 0F BA E1 00 bt rcx, 0
; xmm4 ^= xmm0
0000000000000144 0F 57 E0 xorps xmm4, xmm0
; сравнивается, что xmm4 стал равен 0
0000000000000147 66 0F 38 17 E4 ptest xmm4, xmm4
; не 0 - выходим
000000000000014C 75 29 jnz short loc_177
; иначе продолжаем дальше
000000000000014E 50 push rax
000000000000014F E8 00 00 00 00 call $+5
0000000000000154 58 pop rax
0000000000000155 48 83 C0 07 add rax, 7
0000000000000159 50 push rax
000000000000015A C3 retn
Давайте посмотрим, что за данные хранились в xmm3 и xmm0:
xmm3 = GetManyBytes(0x107, 32)
xmm0 = GetManyBytes(0x9c, 32)
print "".join(chr(ord(i)^ord(j)) for i, j in zip(xmm3, xmm0))
Вывод: yaprofi{HcwidWLD
Прекрасно, первая часть флага у нас в кармане.
000000000000015B 58 pop rax
; помещаем вторую половину (17-32 байты) пользовательского ввода в xmm4
000000000000015C 0F 10 67 10 movups xmm4, xmmword ptr [rdi+10h]
; xmm4 ^= xmm1
0000000000000160 0F 57 E1 xorps xmm4, xmm1
; xmm4 ^= xmm2
0000000000000163 0F 57 E2 xorps xmm4, xmm2
; сравнение, стал ли нулем
0000000000000166 66 0F 38 17 E4 ptest xmm4, xmm4
; нет? выходим
000000000000016B 75 0A jnz short loc_177
; да? возвращаем в вызывающую функцию 1
000000000000016D 5A pop rdx
000000000000016E 59 pop rcx
000000000000016F 5B pop rbx
0000000000000170 5D pop rbp
0000000000000171 B8 01 00 00 00 mov eax, 1
0000000000000176 C3 retn
Посмотрим вторую половину флага:
xmm1 = GetManyBytes(0xD2, 32)
xmm2 = GetManyBytes(0xEE, 32)
print "".join(chr(ord(i)^ord(j)) for i, j in zip(xmm1, xmm2))
Вывод: WxHlqqVhihNZlMm}
Остается последний кусок, связанный с выходом, он просто возвращает 0:
0000000000000177 5A pop rdx
0000000000000178 59 pop rcx
0000000000000179 5B pop rbx
000000000000017A 5D pop rbp
000000000000017B B8 00 00 00 00 mov eax, 0
0000000000000180 C3 retn
Итог: флаг yaprofi{HcwidWLDWxHlqqVhihNZlMm}
author, editor: Rakovsky Stanislav, Unicorn CTF