Нам дан исполняемый 64-битный ELF файл.
Открываем в IDA и видим, что main фактически пустой. Запускаем readelf и определяем настояющую точку входа:
$ readelf -h task_2.elf | grep Entry
Entry point address: 0x40063a
Видим что точка входа находится в функции z8.
В самом начале функции проверяется значение Trap Flag регистра EFLAGS - прием антиотладки, пропатчим jz на jmp чтобы это нам дальше не мешало.
.text:000000000040067F 9C pushfq
.text:0000000000400680 58 pop rax
.text:0000000000400681 48 89 C2 mov rdx, rax
.text:0000000000400684 48 89 55 C8 mov [rbp+var_38], rdx
.text:0000000000400688 48 8B 45 C8 mov rax, [rbp+var_38]
.text:000000000040068C 25 00 01 00 00 and eax, 100h
.text:0000000000400691 48 85 C0 test rax, rax
.text:0000000000400694 74 0A jz short loc_4006A0 ; patched to jmp
Затем читается 12 символов из stdin, для удобства обозначим его как char input[11]. Мы видим что массиву input применяется поочередно несколько функций, разберемся что делает каждая из них:
QWORD z1(QWORD a1, QWORD a2, QWORD newRetAddr)
{
//адрес возврата подменяется на newRetAddr
.text:00000000004004D8 mov [rbp+var_28], rdx; newRetAddr -> var_28
.text:00000000004004DC mov rax, [rbp+8]; оригинальный адрес возврата oldRetAddr
.text:00000000004004E0 mov [rbp+var_8], rax; сохраняем в лок. переменную
.text:00000000004004E4 lea rax, [rbp+var_18]; кладем в RAX адрес первого аргумента a1 (который дальше не будет использован)
.text:00000000004004E8 add rax, 20h; прибавляем sizeof(QWORD)*4 - чтобы пропустить 3 аргумента, в RAX теперь адрес где лежит старый адрес возврата (&oldRetAddr)
.text:00000000004004EC mov rdx, [rbp+var_28]; newRetAddr -> rdx
.text:00000000004004F0 mov [rax], rdx; пишем новый адрес возврата newRetAddr по адресу оригинального адреса возврата
return a2 - 0x17;
// тут произойдет вызов функции по newRetAddr((a2 - 0x17), oldRetAddr);
}
QWORD z2(QWORD a1, QWORD newRetAddr)
{
//адрес возврата подменяется на newRetAddr аналогично z1
return a1 + 0x33
// тут произойдет вызов функции по newRetAddr((a1 + 0x33), newRetAddr);
}
QWORD z3(QWORD a1) {
return a1++;
}
BOOL z4(QWORD a1, QWORD a2) {
return a1 == a2
}
BOOL z5(QWORD a1, QWORD a2)
{
return ((a1 == '7') && (a2 == 'A'))
}
BOOL z6(QWORD a1, QWORD a2) {
return (a1 || a2 != 0xA)
}
QWORD z7(QWORD a1, QWORD a2, QWORD a3, QWORD a4)
{
return ((a3 << 8) + (a2 << 16) + (a1 << 24) + a4) & 0xFFFFFF;
}
Сами проверки:
- z3(input[0) сверяется с 0x49 -> input[0] == 0x49-1 == 'H'
- z3(input[11]) сверяется с 0x62 -> input[11] == 0x62-1 == 'a'
- z7(input[0],input[1],input[2],input[3]) == z0(...). Так как результат z0 не зависит от входных значений, можно посмотреть результат функции в отладчике - 0x00493369. -> input[1] == 'I', input[2] == '3', input[3] == 'i'.
- z4(input[4],0x66) возвращает true если оба аргументы равны -> input[4] == 0x66 == 'f'
- Дважды вызываем z6, которая проверяет что оба аргументы не равны 0xA. z6(input[5],input[7]), потом z6(input[5],input[6]).
- По аналогии с пунктом 3, вызывается z7(input[5], input[6], input[7], input[8]) и проверяется равенство с z0(...). Т.к. результат z0 == 0x00705667 -> input[6] == 'p', input[7] == 'V', input[8] == 'g'
- Проверяем что input[5] != input[9] в z4
- В z5 -> input[2] == 'A', input[5] == '7'
- Последний шаг - вызов z1(0x78, input[10], z2). Из-за подмены адреса возврата, прежде чем вернуться в вызывающую функцию z8 мы попадем в z2. Результат затем сравнивается с 0x4E -> input[10] == 0x4E + 0x17 - 0x33 == 0x32 == '2'
Таким образом можем собрать корректный input и получить наш флаг - 'HI3if7pVgA2a'. Проверим:
$ Hi! Enter Your Serial:
HI3if7pVgA2a
Done!