1. 程式人生 > 實用技巧 >CTF-pwn-tips-zh_CN

CTF-pwn-tips-zh_CN

CTF-pwn-tips-zh_CN

目錄

緩衝區溢位

現在有

一個 bufferchar buf[40]

一個無符號整形變數 num

signed int num

scanf

  • scanf("%s", buf)

    • %s 沒有進行邊界檢查
    • pwnable
  • scanf("%39s", buf)

    • %39s 只從標準輸入獲取 39 個位元組的資料,並將 NULL 放在輸入資料的結尾
    • useless
  • scanf("%40s", buf)

    • 乍一看好像沒有什麼問題
    • 從標準輸入獲取 40 個位元組的資料,並將 NULL 放在輸入資料的結尾
    • 因為 buf 只有 40 Bytes 的空間,輸入資料加上 NULL溢位了一個位元組(one-byte-overflow
    • pwnable
  • scanf("%d", &num)

    • 輸入的 num 用做 alloca 的引數 alloca(num)
      • alloca 是從呼叫者的棧上分配記憶體,相當於 sub esp, eax
      • 如果我們輸入的是一個負數,就會發生棧幀重疊
      • E.g. Seccon CTF quals 2016 cheer_msg
    • 利用 num 訪問一些資料結構
      • 很多時候程式設計師寫檢查的時候只進行了高邊界的檢查,而沒有檢查低邊界,然後 num 又是無符號型別
      • num 設定成負數會發生整數溢位,num 會變得非常大,這樣我們就能覆蓋到一些重要的資料

gets

  • gets(buf)

    • 沒有進行邊界檢查
    • pwnable
  • fgets(buf, 40, stdin)

    • 從標準輸入獲取 39 個位元組的資料,並將 NULL 放在輸入資料的結尾
    • useless

read

  • read(stdin, buf, 40)
    • 從標準輸入獲取 40 個位元組的資料,但是不會把 NULL 放在輸入資料的結尾
    • 看起來安全,但是可能會發生資訊洩露(information leak
    • leakable

E.g.

記憶體佈局

0x7fffffffdd00: 0x4141414141414141      0x4141414141414141
0x7fffffffdd10: 0x4141414141414141      0x4141414141414141
0x7fffffffdd20: 0x4141414141414141      0x00007fffffffe1cd
  • 如果使用 printf 或者 puts 輸出 buf ,這兩個函式會一直讀取記憶體上的東西直到遇到
    NULL

  • 在這裡我們能輸出 'A'*40 + '\xcd\xe1\xff\xff\xff\x7f'

  • fread(buf, 1, 40, stdin)

    • read 幾乎一樣
    • leakable

strcpy

假設有一個 buffer: char buf2[60]

  • strcpy(buf, buf2)

    • 沒有進行邊界檢查
    • 它會將 buf2的內容複製到 buf (直到遇到 NULL byte) 這時 length(buf2) > length(buf)
    • 因為 length(buf2) > length(buf) 所以 buf 發生溢位
    • pwnable
  • strncpy(buf, buf2, 40) && memcpy(buf, buf2, 40)

    • buf2 複製 40 Bytes 的資料到 buf,但是結尾沒有新增 NULL
    • 由於沒有 NULL 標誌字串結束,所以跟上面的一樣會發生資訊洩露
    • leakable

strcat

假設有另一個 bufferchar buf2[60]

  • strcat(buf, buf2)

    • buf 沒有足夠大的空間的時候會有 緩衝區溢位 漏洞
    • 它會將 NULL 新增到末尾,可能會導致 單位元組溢位
    • 在某些情況下,我們可以使用這個 NULL 來更改棧地址或堆地址
    • pwnable
  • strncat(buf, buf2, n)

在gdb中查詢字串

在有SSP (Stack-smashing Protection) 的情況下 , 我們需要找出 argv[0] 和輸入緩衝區的偏移量

gdb

  • argv[0]位於 environ的地址 - 0x10 的地方,在 gdb 裡面可以使用 p/x ((char **)environ) 檢視環境變數 environ 的地址

E.g.

(gdb) p/x (char **)environ
$9 = 0x7fffffffde38
(gdb) x/gx 0x7fffffffde38-0x10
0x7fffffffde28: 0x00007fffffffe1cd
(gdb) x/s 0x00007fffffffe1cd
0x7fffffffe1cd: "/home/naetw/CTF/seccon2016/check/checker"

gdb peda

  • 使用 searchmem "/home/naetw/CTF/seccon2016/check/checker" 搜尋記憶體中 /home/naetw/CTF/seccon2016/check/checker 字串的地址
  • 然後 searchmem $result_address
gdb-peda$ searchmem "/home/naetw/CTF/seccon2016/check/checker"
Searching for '/home/naetw/CTF/seccon2016/check/checker' in: None ranges
Found 3 results, display max 3 items:
[stack] : 0x7fffffffe1cd ("/home/naetw/CTF/seccon2016/check/checker")
[stack] : 0x7fffffffed7c ("/home/naetw/CTF/seccon2016/check/checker")
[stack] : 0x7fffffffefcf ("/home/naetw/CTF/seccon2016/check/checker")
gdb-peda$ searchmem 0x7fffffffe1cd
Searching for '0x7fffffffe1cd' in: None ranges
Found 2 results, display max 2 items:
   libc : 0x7ffff7dd33b8 --> 0x7fffffffe1cd ("/home/naetw/CTF/seccon2016/check/checker")
[stack] : 0x7fffffffde28 --> 0x7fffffffe1cd ("/home/naetw/CTF/seccon2016/check/checker")

讓程式執行在指定埠上

一般情況下:

  • ncat -vc ./binary -kl 127.0.0.1 $port

下面這兩個方式是指定了 binary 執行時使用的庫:

  • ncat -vc 'LD_PRELOAD=/path/to/libc.so ./binary' -kl 127.0.0.1 $port

  • ncat -vc 'LD_LIBRARY_PATH=/path/of/libc.so ./binary' -kl 127.0.0.1 $port

    然後你就可以使用 nc 連線到 binary 所執行的埠和它進行互動: nc localhost $port.

在libc中查詢特定的函式偏移量

如果我們成功洩漏出了某些函式的 libc 地址,我們就可以通過減去該函式在 libc 裡面的偏移量來獲取 libc 基址

手動

  • readelf -s $libc | grep ${function}@

E.g.

$ readelf -s libc-2.19.so | grep system@
    620: 00040310    56 FUNC    GLOBAL DEFAULT   12 __libc_system@@GLIBC_PRIVATE
   1443: 00040310    56 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0

自動

from pwn import *

libc = ELF('libc.so')
system_off = libc.symbols['system']

在共享庫裡面查詢/bin/sh或者sh字串

需要先獲得 libc 的基地址

手動

  • objdump -s libc.so | less 然後搜尋 'sh'
  • strings -tx libc.so | grep /bin/sh

自動

E.g.

from pwn import *

libc = ELF('libc.so')
...
sh = base + next(libc.search('sh\x00'))
binsh = base + next(libc.search('/bin/sh\x00'))

洩露棧地址

制約因素

  • 已經洩露出 libc 的基地址
  • 可以洩漏任意地址的內容

There is a symbol environ in libc, whose value is the same as the third argument of main function, char **envp.
The value of char **envp is on the stack, thus we can leak stack address with this symbol.

libc 中有一個叫 environsymbol ,他的值與 main 函式的第三個引數 char ** envp 相同。

char ** envp 的值在 棧 上,因此我們可以通過洩露這個 symbol 的地址來洩漏堆疊地址

(gdb) list 1
1       #include <stdlib.h>
2       #include <stdio.h>
3
4       extern char **environ;
5
6       int main(int argc, char **argv, char **envp)
7       {
8           return 0;
9       }
(gdb) x/gx 0x7ffff7a0e000 + 0x3c5f38
0x7ffff7dd3f38 <environ>:       0x00007fffffffe230
(gdb) p/x (char **)envp
$12 = 0x7fffffffe230
  • 0x7ffff7a0e000 是當前 libc 的基地址
  • 0x3c5f38environlibc 裡面的偏移量

這個 手冊 詳細的描述了 environ

gdb中fork跟蹤除錯的問題

當你使用 gdb 除錯帶有 fork() 函式的可執行檔案時,您可以使用下面列出的命令來確定要跟蹤哪個程序(gdb 的預設設定是跟蹤父程序,gdb-peda 的預設設定是跟蹤子程序):

  • set follow-fork-mode parent
  • set follow-fork-mode child

另外,我們可以通過 set detach-on-fork off 命令同時除錯父程序和子程序,通過 inferior X 切換跟蹤除錯程序, X 可以是 info inferiors 得到的任意數字(每個數字代表著一個程序)。 如果 fork 得出的兩個程序都需要跟蹤獲取資訊,上面的只跟蹤任意一個程序是達不到目的的,同時跟蹤兩個程序還是很有用的(像是演示子程序的 canary 是和父程序一樣的時候)

.tls段的祕密

約制因素:

  • 需要有 malloc 函式並且要能分配任意大小的記憶體
  • 可以洩露任意地址的內容

我們使用 mallocmmap(預設情況下,當 malloc 或者 new 操作一次性分配大於等於 128KB 的記憶體時,會使用 mmap 來進行,而在小於 128KB 時,使用的是 brk 的方式)方式來分配記憶體( 0x21000 大小就足夠了)。一般來說,這些頁面將放在 .tls 段之前的地址。

通常會有一些有用的東西會放在 .tls 段, 像是主分配區(main_arena) 的地址, canary (棧保護值) ,還有一個奇怪的棧地址(stack address),它指向棧上的某個地方,每次執行可能不一樣,但它具有固定的偏移量。

在 mmap 之前:

7fecbfe4d000-7fecbfe51000 r--p 001bd000 fd:00 131210         /lib/x86_64-linux-gnu/libc-2.24.so
7fecbfe51000-7fecbfe53000 rw-p 001c1000 fd:00 131210         /lib/x86_64-linux-gnu/libc-2.24.so
7fecbfe53000-7fecbfe57000 rw-p 00000000 00:00 0
7fecbfe57000-7fecbfe7c000 r-xp 00000000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so
7fecc0068000-7fecc006a000 rw-p 00000000 00:00 0              <- .tls section
7fecc0078000-7fecc007b000 rw-p 00000000 00:00 0
7fecc007b000-7fecc007c000 r--p 00024000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so
7fecc007c000-7fecc007d000 rw-p 00025000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so

在 mmap 之後:

7fecbfe4d000-7fecbfe51000 r--p 001bd000 fd:00 131210         /lib/x86_64-linux-gnu/libc-2.24.so
7fecbfe51000-7fecbfe53000 rw-p 001c1000 fd:00 131210         /lib/x86_64-linux-gnu/libc-2.24.so
7fecbfe53000-7fecbfe57000 rw-p 00000000 00:00 0
7fecbfe57000-7fecbfe7c000 r-xp 00000000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so
7fecc0045000-7fecc006a000 rw-p 00000000 00:00 0              <- memory of mmap + .tls section
7fecc0078000-7fecc007b000 rw-p 00000000 00:00 0
7fecc007b000-7fecc007c000 r--p 00024000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so
7fecc007c000-7fecc007d000 rw-p 00025000 fd:00 131206         /lib/x86_64-linux-gnu/ld-2.24.so

可預測的隨機數發生器

當二進位制檔案使用隨機數生成器(RNG) 的生成的偽隨機數作為重要資訊的地址時,如果它是可預測的,我們可以猜測出相同的值。

假設它是可預測的,我們可以使用 ctypes 模組(Python 內建模組)

ctypes 可以讓我們用 python 呼叫 DLL(Dynamic-Link Library 動態連結庫) 或者 共享庫(Shared Library)裡的函式

如果有一個 init_proc 函式 :

srand(time(NULL));
while(addr <= 0x10000){
    addr = rand() & 0xfffff000;
}
secret = mmap(addr,0x1000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS ,-1,0);
if(secret == -1){
    puts("mmap error");
    exit(0);
}

我們可以使用 ctypes 來獲得相同的 addr

import ctypes
LIBC = ctypes.cdll.LoadLibrary('/path/to/dll')
LIBC.srand(LIBC.time(0))
addr = LIBC.rand() & 0xfffff000

使棧可執行

使用one-gadget-RCE代替system

約制條件:

  • libc 的基地址
  • 任意地址寫

幾乎所有的 pwnable 挑戰都要執行 system('/bin/sh') ,如果我們想執行 system('/bin/sh'), 需要能控制函式引數並且能劫持程式執行流程呼叫 system 函式。如果我們不能控制引數該怎麼辦

使用 one-gadget-RCE 技術!

有了 one-gadget-RCE,我們就能劫持 .got.plt或者我們可以用來控制 eip 讓程式跳到 one-gadget 上執行,但是在使用它之前需要滿足一些約束條件。

libc 裡面有很多 one-gadgets 。每種方法都有不同的約束條件,但這些約束條件是相似的。每個約束都與暫存器的狀態有關。

E.g.

  • ebx 存的是 libcrw-p 區的地址
  • [esp+0x34] == NULL

我們怎樣才能滿足這些限制?這裡有一個有用的工具: one_gadget !!!!

如果我們能滿足這些限制,我們就可以更容易地得到一個 shell

劫持鉤子函式

約制條件:

  • libc 基地址
  • 任意地址寫
  • 程式有用到 mallocfreerealloc函式

By manual:

GNU C Library (glibc)允許您通過指定適當的鉤子函式來修改 mallocreallocfree 的行為。 例如,可以使用這些鉤子函式來協助除錯 使用動態記憶體分配的程式。

malloc.h 中聲明瞭鉤子變數,它們的預設值為 0x0

  • __malloc_hook
  • __free_hook
  • ...

因為它們是用來幫助我們除錯程式的,所以它們在執行過程中是可寫的。

0xf77228e0 <__free_hook>:       0x00000000
0xf7722000 0xf7727000 rw-p      mapped

我們可以看看 malloc.c 的原始碼。 我會用 __libc_free 來做演示

void (*hook) (void *, const void *) = atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
    (*hook)(mem, RETURN_ADDRESS (0));
    return;
}

這段程式碼會檢查 __free_hook。如果它不為 NULL,它將優先呼叫鉤子函式。在這裡我們可以使用 one-gadget-RCE。由於鉤子函式是在 libc 中呼叫的, 所以通常滿足 one-gadget 的約束條件。

使用printf觸發malloc和free

來看看 printf 的原始碼,有幾個地方可能會觸發 malloc 。 以 vfprintf.c 的第 1470 行 為例:

#define EXTSIZ 32
enum { WORK_BUFFER_SIZE = 1000 };

if (width >= WORK_BUFFER_SIZE - EXTSIZ)
{
    /* We have to use a special buffer.  */
    size_t needed = ((size_t) width + EXTSIZ) * sizeof (CHAR_T);
    if (__libc_use_alloca (needed))
        workend = (CHAR_T *) alloca (needed) + width + EXTSIZ;
    else
    {
        workstart = (CHAR_T *) malloc (needed);
        if (workstart == NULL)
        {
            done = -1;
            goto all_done;
        }
        workend = workstart + width + EXTSIZ;
    }
}

我們可以發現,如果 width 變數夠大的時候將會觸發 malloc(當然,如果觸發了 mallocprintf 末尾也會觸發 free)。然而,因為 WORK_BUFFER_SIZE 不夠大,所以程式會跳到 else 程式碼塊去執行。 讓我們看看 __libc_use_alloca 來決定我們應該給出的最小的 width

/* Minimum size for a thread.  We are free to choose a reasonable value.  */
#define PTHREAD_STACK_MIN        16384

#define __MAX_ALLOCA_CUTOFF        65536

int __libc_use_alloca (size_t size)
{
    return (__builtin_expect (size <= PTHREAD_STACK_MIN / 4, 1)
        || __builtin_expect (__libc_alloca_cutoff (size), 1));
}

int __libc_alloca_cutoff (size_t size)
{
	return size <= (MIN (__MAX_ALLOCA_CUTOFF,
					THREAD_GETMEM (THREAD_SELF, stackblock_size) / 4
					/* The main thread, before the thread library is
						initialized, has zero in the stackblock_size
						element.  Since it is the main thread we can
						assume the maximum available stack space.  */
					?: __MAX_ALLOCA_CUTOFF * 4));
}

我們必須確保:

  1. size > PTHREAD_STACK_MIN / 4
  2. size > MIN(__MAX_ALLOCA_CUTOFF, THREAD_GETMEM(THREAD_SELF, stackblock_size) / 4 ?: __MAX_ALLOCA_CUTOFF * 4)
    • 我不完全理解 THREAD_GETMEM 到底是做什麼的,但它似乎大多時候返回 0。
    • 因此,第二個條件通常是 size > 65536

More details:

總結

  • 大多數時候,觸發 mallocfree 的最小 width65537
  • 如果存在格式字串漏洞,並且程式在呼叫 printf(buf) 後立即結束,我們可以使用 one-gadget 劫持 __malloc_hook__free_hook 並使用上述技巧觸發 mallocfree,那麼即使在 printf(buf) 後面沒有任何函式呼叫或其他東西,我們仍然可以獲得 shell(這裡的意思是,即使呼叫 printf 結束後程序直接退出,我們還是能做到程式執行流程劫持,因為我們劫持了 __malloc_hook__free_hook ,在觸發 mallocfree 的時候我們已經執行了我們想要的操作)

使用execveat開啟一個shell

提到使用系統呼叫去開一個 shell 時我們的腦子中想到的會是 execve ,然而,由於缺少 gadget 或其他限制,執行起來總是很艱難

實際上,有一個系統呼叫 execveat,其原型如下:

int execveat(int dirfd, const char *pathname,
             char *const argv[], char *const envp[],
             int flags);

根據它在 man 手冊 中的描述,可以發現其操作方式與 execve 相同。 至於附加的引數,它提到:

pathname 是絕對路徑,則 dirfd 可以省略

因此,我們可以讓 pathname 指向 "/bin/sh", 並將 argv, envpflags 設定為 0, 那麼無論 dirfd 的值是多少,我們仍然可以得到一個 shell