R0 CREW

Zeronights 2017 SSB Task 3 Writeup

Нам дан исполняемый 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;
    }

Сами проверки:

  1. z3(input[0) сверяется с 0x49 -> input[0] == 0x49-1 == ‘H’
  2. z3(input[11]) сверяется с 0x62 -> input[11] == 0x62-1 == ‘a’
  3. z7(input[0],input[1],input[2],input[3]) == z0(…). Так как результат z0 не зависит от входных значений, можно посмотреть результат функции в отладчике - 0x00493369. -> input[1] == ‘I’, input[2] == ‘3’, input[3] == ‘i’.
  4. z4(input[4],0x66) возвращает true если оба аргументы равны -> input[4] == 0x66 == ‘f’
  5. Дважды вызываем z6, которая проверяет что оба аргументы не равны 0xA. z6(input[5],input[7]), потом z6(input[5],input[6]).
  6. По аналогии с пунктом 3, вызывается z7(input[5], input[6], input[7], input[8]) и проверяется равенство с z0(…). Т.к. результат z0 == 0x00705667 -> input[6] == ‘p’, input[7] == ‘V’, input[8] == ‘g’
  7. Проверяем что input[5] != input[9] в z4
  8. В z5 -> input[2] == ‘A’, input[5] == ‘7’
  9. Последний шаг - вызов z1(0x78, input[10], z2). Из-за подмены адреса возврата, прежде чем вернуться в вызывающую функцию z8 мы попадем в z2. Результат затем сравнивается с 0x4E -> input[10] == 0x4E + 0x17 - 0x33 == 0x32 == ‘2’

Таким образом можем собрать корректный input и получить наш флаг - ‘HI3if7pVgA2a’.

Проверим:

Оригинальное решение, с использованием angr

import angr

p = angr.Project('./task_2.elf', auto_load_libs=False)

# Create a blank state
st = p.factory.blank_state()

# Constrain to be non-null and non-newline:
for _ in xrange(12):
    k = st.posix.files[0].read_from(1)
    st.se.add(k != 0)
    st.se.add(k != 10)

 # Reset the symbolic stdin's properties and set its length.
st.posix.files[0].seek(0)
st.posix.files[0].length = 12

# Construct a SimulationManager to perform symbolic execution.
# Step until there is nothing left to be stepped.
sm = p.factory.simgr(st)
sm.run()

# Get the stdout of every path that reached an exit syscall. The flag
# should be in one of these!
out = ''
for pp in sm.deadended:
    out = pp.posix.dumps(1)
    if 'Done' in out:
        print 'Result: ', out
        print 'Flag is (hex): ', pp.posix.dumps(0).encode('hex')
        print 'Flag is:', pp.posix.dumps(0)
        with open('solution.txt', 'wb') as fo:
            fo.write(pp.posix.dumps(0))

Нам дан исполняемый 64-битный ELF файл. Ссылка на задание.

Ссылка не работает.