Реверс шеллкода отборочного этапа Я-Профи 2019 для магистров
@ Rakovsky Stanislav | Friday, Dec 27, 2019 | 10 minutes read | Update at Friday, Dec 27, 2019

Данное задание подготовлено сообществом 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

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…