У меня была возможность принять участие в NDH Quals 2017, и я провел почти весь день, занимаясь реверс-инжинирингом. Я не нашел флаг в течение 24 часов CTF, но я не сдался и нашел решение после :)

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

[~|maasta]$ ./step2.bin giveMeTheFlag Try again :(

Давайте проанализируем это с помощью peda-gdb и радара2. Во-первых, в начале интересной части есть защита от отладки.

0x004007c9 488b06 mov rax,[rsi] 
0x004007cc 80382e cmp byte [rax], 0x2e 
0x004007cf 0f85e7010000 jnz dword 0x4009bc [2] 
0x004007d5 8078012f cmp byte [rax+0x1], 0x2f 
0x004007d9 0f85dd010000 jnz dword 0x4009bc [3] 
0x004007df 80780273 cmp byte [rax+0x2], 0x73 
0x004007e3 0f85d3010000 jnz dword 0x4009bc [4] 
0x004007e9 80780374 cmp byte [rax+0x3], 0x74 
0x004007ed 0f85c9010000 jnz dword 0x4009bc [5] 
0x004007f3 80780465 cmp byte [rax+0x4], 0x65 
0x004007f7 0f85bf010000 jnz dword 0x4009bc [6] 
0x004007fd 80780570 cmp byte [rax+0x5], 0x70 
0x00400801 0f85b5010000 jnz dword 0x4009bc [7] 
0x00400807 80780632 cmp byte [rax+0x6], 0x32 
0x0040080b 0f85ab010000 jnz dword 0x4009bc [8] 
0x00400811 8078072e cmp byte [rax+0x7], 0x2e 
0x00400815 0f85a1010000 jnz dword 0x4009bc [9] 
0x0040081b 80780862 cmp byte [rax+0x8], 0x62 
0x0040081f 0f8597010000 jnz dword 0x4009bc [?] 
0x00400825 80780969 cmp byte [rax+0x9], 0x69 
0x00400829 0f858d010000 jnz dword 0x4009bc [?] 
0x0040082f 80780a6e cmp byte [rax+0xa], 0x6e 
0x00400833 0f8583010000 jnz dword 0x4009bc [?] 
0x00400839 80780b00 cmp byte [rax+0xb], 0x0

Все эти строки сравнивают argv[0] (который находится по адресу внутри rax) с «./step2.bin». Если вы находитесь в отладчике, argv[0] будет /step2.bin (что не равно «./step2.bin»), а затем вы перейдете к 0x4009bc.

Что находится по адресу 0x4009bc? Функция, которая печатает «Попробуйте еще раз :(». Так что давайте пропустим эту часть, прыгнув сразу после

(peda-gdb) b * 0x00400843 (peda-gdb) jump * 0x00400843

Сразу после этого программа начинает проверять, что находится в argv[1]

0x00400843 488b5608 mov rdx, [rsi+0x8] 
0x00400847 31c0 xor eax, eax 
0x00400849 4883c9ff or rcx, 0xffffffffffffffff 
0x0040084d 4889d7 mov rdi, rdx 
0x00400850 f2ae repne scasb 
0x00400852 4883f9de cmp rcx, 0xde 
0x00400856 0f8560010000 jnz dword 0x4009bc [?] 
0x0040085c 803a57 cmp byte [rdx], 0x57 
0x0040085f 0f8557010000 jnz dword 0x4009bc

Первая часть странная, но она просто проверяет длину argv[1], которая должна быть равна 32. Вторая часть сравнивает argv[1][0] с 0x57, что является char ‘W’.

Итак, мы знаем, что флаг имеет длину 32 символа и начинается с буквы W. Продолжим. Сразу после самое интересное:

0x00400879 488d5801 lea rbx, [rax+0x1] 
0x0040087d 4c8d6821 lea r13, [rax+0x21] 
0x00400881 488d7c2410 lea rdi, [rsp+0x10] 
0x00400886 e8e5feffff call dword imp.sigfillset 
0x0040088b f643ff01 test byte [rbx-0x1], 0x1 
0x0040088f bac60a4000 mov edx, 0x400ac6 
0x00400894 b8d00a4000 mov eax, 0x400ad0 
0x00400899 488d742408 lea rsi, [rsp+0x8] 
0x0040089e bf05000000 mov edi, 0x5 
0x004008a3 c78424900000000. mov dword [rsp+0x90], 
0x10000000 0x004008ae 48891d032e2300 mov [rip+0x232e03], rbx 
0x004008b5 480f44c2 cmovz rax, rdx 
0x004008b9 31d2 xor edx, edx 
0x004008bb 4889442408 mov [rsp+0x8], rax 
0x004008c0 e83bfeffff call dword imp.sigaction 
0x004008c5 8a53ff mov dl, [rbx-0x1] 
0x004008c8 31c0 xor eax, eax 
0x004008ca 38d0 cmp al, dl 
0x004008cc 7405 jz 0x4008d3 
0x004008ce cc int3 
0x004008cf ffc0 inc eax 
0x004008d1 ebf7 jmp 0x4008ca 
0x004008d3 48ffc3 inc rbx 
0x004008d6 4c39eb cmp rbx, r13 
0x004008d9 75a6 jnz 0x400881 

Во-первых, программа вызывает sigfillset(), которая инициализирует объект sigset_t. Затем он вызывает sigaction(). Давайте посмотрим на его аргументы: по адресу 0x40088b программа проверяет байт в [rbx-0x1] с помощью 0x1. Это побитовое значение между [rbx-1] и 1. По адресу внутри rbx-1 находится наш argv[1]. Таким образом, тестовый байт [rbx-1], 0x1 означает «Проверить четность первого символа строки, переданной в качестве аргумента». Если он четный, то zf устанавливается равным 1. Если нет, то установить в 0. Затем программа сохраняет адреса двух функций (0x400ac6 и 0x400ad0) в edx и eax. Эти две функции просто inc([rip+0x232beb]) и dec([rip+0x232be1]). Затем для edi устанавливается значение 5. А для rax устанавливается указатель на функцию inc(), если установлен флаг ZF. Наконец, sigaction() — это вызов. Это означает, что при получении сигнала SIGTRAP (сигнал 5 в edi) программа вызывает функцию в rax.

Давайте предположим это и продолжим. В этой части мы сравниваем счетчик (начиная с 0) с нашим первым байтом argv[1], если он не равен, программа вызывает int3, затем увеличивает счетчик и повторяет до counter = char. Этот int3 является наиболее важной частью, потому что он прерывает программу и вызывает функцию, установленную программой с помощью sigaction. И эта функция увеличивает или уменьшает значение второго символа argv[1]. Таким образом, это действие будет повторяться N раз, где N — это значение ASCII байтов [rbx-1], которое является нашим W. Затем программа увеличивает rbx (указатель на символы аргумента) и повторяет всю эту часть.

Итак, давайте быстро продолжим:

Программа принимает аргумент. Он должен состоять из 32 символов и начинаться с W. Затем для каждого символа в позиции K в аргументе программа вычисляет значение символа в позиции K + 1, следуя этим правилам:

ord(K) – это значение ASCII символа в позиции K

Добавить ord(K) к ord(K+1), если ord(K) четно

Вычесть ord(K) из ord(K+1), если ord(K) нечетно

Давайте проанализируем следующую часть.

0x004008db 31d2 xor edx, edx 
0x004008dd b9efbeadde mov ecx, 0xdeadbeef 
0x004008e2 b001 mov al, 0x1 
0x004008e4 31f6 xor esi, esi 
0x004008e6 41334c1401 xor ecx, [r12+rdx+0x1] 
0x004008eb 3b8a40356300 cmp ecx, [rdx+0x633540] 
0x004008f1 0f45c6 cmovnz eax, esi 
0x004008f4 4883c204 add rdx, 0x4 
0x004008f8 4883fa20 cmp rdx, 0x20 
0x004008fc 75e8 jnz 0x4008e6 
0x004008fe fec8 dec al 
0x00400900 0f85b6000000 jnz dword 0x4009bc 
0x00400906 bf16104000 mov edi, str.Welldone 
0x0040090b e8e0fdffff call dword imp.puts

В этой части программа выполняет операцию XOR над 4 первыми байтами с 0xdeadbeef и сравнивает ее со значением в [rdx+0x633540]. Это значение становится новым ключом, и эта операция выполняется 8 раз (по 4 байта за раз для 32 символов пароля).

Если все эти сравнения верны, то программа печатает «Хорошо».

Итак, давайте напишем небольшую программу, которая сначала вычисляет строку, чтобы пройти все сравнения, а затем инвертирует алгоритм увеличения/уменьшения.

Вот программа, которую я написал на perl6

Мы запускаем его:

[~|maasta]$ perl6 findFlag.p6 
(W h e n _ i _ g r o w _ u p , _ I _ w i l l _ b e _ a _ f l a g )

Вот !