CTF-pwn-tips-zh_CN
原項目(英語):https://github.com/Naetw/CTF-pwn-tips
為了說明白,我做了不少改動
目錄
- 緩沖區溢出
- 在 gdb 中查找字符串
- 讓程序運行在指定端口上
- 在 libc 中查找特定的函數偏移量
- 在共享庫里面查找/bin/sh或者sh字符串
- 泄露棧地址
- gdb 中 fork 跟蹤調試的問題
- .tls 段的秘密
- 可預測的隨機數發生器 -- RNG(Random Number Generator))
- 使棧可執行
- 使用 one-gadget-RCE 代替 system
- 劫持鈎子函數
- 使用 printf 觸發 malloc 和 free
- 使用 execveat 打開一個 shell
緩沖區溢出
現在有
一個 buffer : char 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
假設有另一個 buffer:char buf2[60]
-
strcat(buf, buf2)- 在
buf沒有足夠大的空間的時候會有 緩沖區溢出 漏洞 - 它會將
NULL添加到末尾,可能會導致 單字節溢出 - 在某些情況下,我們可以使用這個
NULL來更改棧地址或堆地址 - pwnable
- 在
-
strncat(buf, buf2, n)- 功能跟
strcat一樣,但是會有長度限制(參數n) - pwnable
- E.g. Seccon CTF quals 2016 jmper
- 功能跟
在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
自動
- 使用 pwntools
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
自動
- 使用 pwntools
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 中有一個叫 environ 的 symbol ,他的值與 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的基地址0x3c5f38是environ在libc里面的偏移量
這個 手冊 詳細的描述了 environ
gdb中fork跟蹤調試的問題
當你使用 gdb 調試帶有 fork() 函數的可執行文件時,您可以使用下面列出的命令來確定要跟蹤哪個進程(gdb 的默認設置是跟蹤父進程,gdb-peda 的默認設置是跟蹤子進程):
set follow-fork-mode parentset follow-fork-mode child
另外,我們可以通過 set detach-on-fork off 命令同時調試父進程和子進程,通過 inferior X 切換跟蹤調試進程, X 可以是 info inferiors 得到的任意數字(每個數字代表着一個進程)。 如果 fork 得出的兩個進程都需要跟蹤獲取信息,上面的只跟蹤任意一個進程是達不到目的的,同時跟蹤兩個進程還是很有用的(像是演示子進程的 canary 是和父進程一樣的時候)
.tls段的秘密
約制因素:
- 需要有
malloc函數並且要能分配任意大小的內存 - 可以泄露任意地址的內容
我們使用 malloc 的 mmap(默認情況下,當 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 存的是
libc的rw-p區的地址 - [esp+0x34] == NULL
我們怎樣才能滿足這些限制?這里有一個有用的工具: one_gadget !!!!
如果我們能滿足這些限制,我們就可以更容易地得到一個 shell
劫持鈎子函數
約制條件:
- 有
libc基地址 - 任意地址寫
- 程序有用到
malloc,free或realloc函數
By manual:
GNU C Library (glibc)允許您通過指定適當的鈎子函數來修改
malloc、realloc和free的行為。 例如,可以使用這些鈎子函數來協助調試 使用動態內存分配的程序。
在 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(當然,如果觸發了 malloc,printf 末尾也會觸發 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));
}
我們必須確保:
size > PTHREAD_STACK_MIN / 4size > MIN(__MAX_ALLOCA_CUTOFF, THREAD_GETMEM(THREAD_SELF, stackblock_size) / 4 ?: __MAX_ALLOCA_CUTOFF * 4)- 我不完全理解
THREAD_GETMEM到底是做什么的,但它似乎大多時候返回 0。 - 因此,第二個條件通常是
size > 65536
- 我不完全理解
More details:
總結
- 大多數時候,觸發
malloc和free的最小width是65537。 - 如果存在格式字符串漏洞,並且程序在調用
printf(buf)后立即結束,我們可以使用one-gadget劫持__malloc_hook或__free_hook並使用上述技巧觸發malloc和free,那么即使在printf(buf)后面沒有任何函數調用或其他東西,我們仍然可以獲得shell(這里的意思是,即使調用printf結束后程序直接退出,我們還是能做到程序執行流程劫持,因為我們劫持了__malloc_hook或__free_hook,在觸發malloc和free的時候我們已經執行了我們想要的操作)
使用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, envp 和 flags 設置為 0, 那么無論 dirfd 的值是多少,我們仍然可以得到一個 shell。
