mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1688 字
4 分钟
某音还原异常解决
2026-06-28

VM还原触发SDK检测的深度分析与修复#

一、问题现象#

在使用AST技术将VM保护的字节码解释器还原为switch-case结构后,替换到抖音网页中触发SDK检测,导致页面不再执行bdm.js文件,功能异常。

表面现象:代码逻辑看似正确,76个操作码全部收集,switch-case结构完整生成,但浏览器检测到代码篡改。

二、根本原因分析#

2.1 SDK检测机制#

字节跳动SLARDAR_WEB SDK通过多种方式检测代码完整性:

  1. 代码结构指纹检测:对比关键函数的结构特征
  2. 执行流特征检测:监控关键代码路径的执行轨迹
  3. 返回值校验:检查特定操作码的返回值是否符合预期

2.2 操作码75的错误还原#

通过深度分析,发现问题的核心在于操作码75的代码被错误还原

原始代码结构#

else {
if (75 !== t)
return f = 2, void (l = v[p--]);
v[++p] = null
}

这段代码的逻辑是:

  • t !== 75 时(条件为真),执行return语句,终止函数
  • t === 75 时(条件为假),执行 v[++p] = null

错误的还原结果#

case 75:
return f = 2, void (l = v[p--]); // ❌ 错误!
break;

正确的还原结果#

case 75:
v[++p] = null; // ✅ 正确!
break;

为什么错误?

  • 当VM执行操作码75时,应该将null压入栈顶
  • 但错误的还原导致函数直接返回,改变了VM的执行行为
  • SDK检测到执行流异常,触发防御机制

三、技术原理深度剖析#

3.1 区间夹逼技术基础#

VM解释器使用嵌套if-else结构来分发不同操作码:

if (t < 38) {
if (t < 19) {
if (t < 9) {
if (t < 4) {
if (t < 2) {
if (0 === t) { ... }
else { ... } // 操作码1
} else {
if (2 === t) { ... }
else { ... } // 操作码3
}
} else {
// 操作码4-8
}
} else {
// 操作码9-18
}
} else {
// 操作码19-37
}
} else {
// 操作码38-75
}

通过层层条件判断,最终确定操作码t的具体值。我们的任务是反向推导每个代码块对应的操作码。

3.2 区间表示法#

使用左闭右开区间 [min, max) 表示操作码的可能范围:

条件表达式真区间假区间
t < 19[-∞, 19)[19, +∞)
t <= 19[-∞, 20)[20, +∞)
t > 19[20, +∞)[-∞, 20)
t >= 19[19, +∞)[-∞, 19)
t === 19[19, 20)去掉19的区间
t !== 19去掉19的区间[19, 20)

关键发现

  • 对于 === 操作符,真区间是精确的单值区间 [num, num+1)
  • 对于 !== 操作符,假区间才是精确的单值区间 [num, num+1),真区间是去掉该值的剩余区间

3.3 parseRange函数的设计#

function parseRange(testPath) {
if (!testPath || testPath.type !== 'BinaryExpression') return null;
const left = testPath.get('left');
const right = testPath.get('right');
const op = testPath.get('operator').node;
if (left.type === 'Identifier' && left.node.name === 't') {
if (right.type === 'NumericLiteral') {
const num = right.node.value;
switch (op) {
case '<':
return { min: -Infinity, max: num };
case '<=':
return { min: -Infinity, max: num + 1 };
case '>':
return { min: num + 1, max: Infinity };
case '>=':
return { min: num, max: Infinity };
case '===':
case '==':
return { min: num, max: num + 1 }; // 真区间
case '!==':
case '!=':
return { min: num, max: num + 1 }; // 假区间!
}
}
}
// 处理反向情况 (num === t)
else if (right.type === 'Identifier' && right.node.name === 't') {
// ... 类似处理
}
return null;
}

关键设计决策

  • 对于 !== 操作符,parseRange返回的是假区间(当条件不成立时的区间)
  • 这是因为我们关心的是”当条件为假时执行什么代码”

3.4 squeezeRange函数的核心逻辑#

function squeezeRange(path, currentRange) {
const testPath = path.get('test');
const range = parseRange(testPath);
if (!range) return;
// 对于 !== 操作符,parseRange返回的是假区间
const testOp = testPath.get('operator').node;
const isNotEqual = testOp === '!==' || testOp === '!=';
// 计算当前分支的实际区间
let branchRange;
if (isNotEqual) {
// !== 的真分支区间是当前区间减去假区间
// 即:条件为真时,t 不等于 num
branchRange = { min: currentRange.min, max: range.min };
} else {
// 其他操作符的真分支区间是当前区间与parseRange结果的交集
branchRange = intersect(currentRange, range);
}
// 处理 consequent 分支(条件为真时执行)
const consequent = path.get('consequent');
// ... 处理真分支代码
// 处理 alternate 分支(条件为假时执行)
const alternate = path.get('alternate');
if (alternate.node) {
// 对于 !==,假分支区间就是 parseRange 返回的区间
// 对于其他操作符,需要根据操作符类型计算假分支区间
let elseRangeRaw;
const testOp = testPath.get('operator').node;
if (testOp === '<') {
elseRangeRaw = { min: range.max, max: Infinity };
} else if (testOp === '<=') {
elseRangeRaw = { min: range.max, max: Infinity };
} else if (testOp === '>' || testOp === '>=') {
elseRangeRaw = { min: -Infinity, max: range.min };
} else if (testOp === '===' || testOp === '==' || testOp === '!==' || testOp === '!=') {
elseRangeRaw = { min: currentRange.min, max: currentRange.max };
}
const elseRange = intersect(currentRange, elseRangeRaw);
// ... 处理假分支代码
}
}

四、隐式else分支的特殊处理#

4.1 问题场景#

VM代码中存在一种特殊结构:

if (52 === t)
return U = o[a++], f = 1, void (l = a + U);
U = o[a++], a += U; // 这是操作码53的代码,但不是显式的else分支

以及:

if (75 !== t)
return f = 2, void (l = v[p--]);
v[++p] = null // 这是操作码75的代码,隐式else分支

这种结构的特点:

  • if语句没有显式的else分支
  • if语句后面紧跟其他语句
  • 这些后续语句实际上是”隐式else分支”,只有当if条件不成立时才执行

4.2 BlockStatement中的隐式else检测#

// 特殊处理:如果BlockStatement中有if语句和其他语句,其他语句可能是if的else分支
if (body.length === 2 && body[0].isIfStatement() && !body[0].node.alternate) {
const ifStmt = body[0];
const elseStmt = body[1]; // 第二个语句是隐式else
const ifTestPath = ifStmt.get('test');
const ifRange = parseRange(ifTestPath);
if (ifRange && (ifTestPath.get('operator').node === '===' || ifTestPath.get('operator').node === '!==')) {
const op = ifTestPath.get('operator').node;
if (op === '===') {
// 对于 === 条件,隐式else区间是去掉这个精确值
const elseRange1 = { min: branchRange.min, max: ifRange.min };
const elseRange2 = { min: ifRange.max, max: branchRange.max };
storeOpcode(intersect(branchRange, elseRange1), elseStmt.node);
storeOpcode(intersect(branchRange, elseRange2), elseStmt.node);
squeezeRange(ifStmt, branchRange); // 递归处理if语句
} else if (op === '!==') {
// 对于 !== 条件,隐式else区间就是if的精确值(当条件为假时)
const elseRange = { min: ifRange.min, max: ifRange.max };
storeOpcode(intersect(branchRange, elseRange), elseStmt.node);
// 不要递归处理if语句!它的consequent属于不同的区间
}
}
}

关键逻辑差异

操作符隐式else区间是否递归处理if
===去掉精确值的剩余区间是,因为if的consequent也属于当前区间
!==精确值本身 [num, num+1)否,因为if的consequent属于真区间,不在当前区间内

4.3 else代码块中的隐式else处理#

else if (t < 75) {
// ... 操作码70-74
} else {
// 这个else块的区间是 [75, +∞)
if (75 !== t)
return f = 2, void (l = v[p--]);
v[++p] = null
}

处理else代码块时,需要检测其中的隐式else模式:

elseBody.forEach((stmtPath, index) => {
if (stmtPath.isIfStatement()) {
// 检查是否是 "num !== t" 的模式
const test = stmtPath.get('test');
if (test.type === 'BinaryExpression' && test.get('operator').node === '!==') {
const num = extractNumericValue(test);
if (num !== null) {
// 设置隐式else区间:t == num
implicitElseRange = { min: num, max: num + 1 };
}
}
squeezeRange(stmtPath, elseRange);
} else if (stmtPath.isExpressionStatement()) {
// 如果前面有 "if (num !== t)",使用隐式else区间
const rangeToUse = implicitElseRange || elseRange;
storeOpcode(rangeToUse, stmtPath.node);
}
});

流程图

else代码块处理流程
├─ 遍历body中的语句
│ │
│ ├─ 发现 if (75 !== t)
│ │ ├─ 提取 num = 75
│ │ ├─ 设置 implicitElseRange = [75, 76)
│ │ └─ 调用 squeezeRange 处理if语句(不存储)
│ │
│ ├─ 发现 v[++p] = null
│ │ ├─ 检查 implicitElseRange 是否已设置
│ │ ├─ 使用 implicitElseRange = [75, 76) 作为区间
│ │ └─ storeOpcode([75, 76), v[++p] = null)
│ │ ├─ isSingleInt([75, 76)) = true
│ │ └─ 存储到 stream[75]
│ │
│ └─ 继续处理后续语句...

五、修复过程总结#

5.1 问题诊断步骤#

  1. 对比操作码列表:发现操作码数量从76降至75,缺少操作码75

  2. 添加调试信息:在关键函数中添加console.log,跟踪区间计算过程

  3. 分析输出结果

    [DEBUG] 存储操作码 75 内容= ReturnStatement // ❌ 错误!

    发现操作码75被错误存储为return语句

  4. 定位代码结构:检查vmp.js中操作码75的原始代码

    if (75 !== t)
    return ...;
    v[++p] = null
  5. 分析逻辑错误:发现squeezeRange对!==操作符的处理有误

5.2 关键修复点#

修复点1:parseRange函数#

添加对!==操作符的支持:

case '!==':
case '!=':
return { min: num, max: num + 1 }; // 返回假区间

修复点2:squeezeRange函数#

!==操作符,交换真分支和假分支的处理:

let branchRange;
if (isNotEqual) {
// !== 的真分支区间是当前区间减去假区间
branchRange = { min: currentRange.min, max: range.min };
} else {
branchRange = intersect(currentRange, range);
}

修复点3:隐式else分支处理#

恢复对===操作符的特殊处理,确保操作码53也能被捕获:

if (ifRange && (ifTestPath.get('operator').node === '===' || ifTestPath.get('operator').node === '!==')) {
// 分别处理 === 和 !==
}

修复点4:else代码块中的隐式else#

检测if (num !== t)模式,为后续语句设置正确的区间:

let implicitElseRange = null;
if (test.get('operator').node === '!==') {
implicitElseRange = { min: num, max: num + 1 };
}

5.3 验证结果#

修复后的输出:

收集到的操作码数量: 76
操作码列表: [
'0', '1', '2', ..., '75' // 全部76个操作码
]

生成的switch-case结构:

case 75:
v[++p] = null; // ✅ 正确!
break;

六、核心技术要点总结#

6.1 区间夹逼的核心思想#

  1. 正向推导:VM通过层层条件判断,逐步缩小操作码范围,最终定位到单个操作码
  2. 逆向还原:我们反向推导,从最内层代码块开始,根据条件表达式计算每个代码块对应的操作码区间
  3. 区间交集:通过intersect函数,将父级区间与当前条件区间求交集,逐步夹逼到单值区间

6.2 关键技术难点#

  1. !==操作符的特殊性
    • 真区间:去掉精确值的剩余区间
    • 假区间:精确值本身
    • parseRange返回假区间,需要特殊处理
  2. 隐式else分支识别
    • BlockStatement中”if语句 + 其他语句”的模式
    • 其他语句实际上是隐式else分支
    • 需要根据if条件操作符类型,正确计算隐式else区间
  3. 递归处理边界
    • 对于===,需要递归处理if语句(因为consequent也属于当前区间)
    • 对于!==,不需要递归处理if语句(因为consequent属于不同的区间)

6.3 防止SDK检测的建议#

  1. 确保逻辑完整性:每个操作码的代码必须与原始逻辑完全一致
  2. 验证执行流:测试关键操作码的执行路径是否符合预期
  3. 对比返回值:检查涉及return语句的操作码,确保返回时机和返回值正确
  4. 完整性测试:在浏览器中测试还原后的代码,观察是否触发异常行为

七、代码参考#

核心函数包括:

  • parseRange:解析条件表达式获取区间
  • squeezeRange:区间夹逼函数,递归分析if-else链
  • storeOpcode:存储单值区间的操作码
  • intersect:区间交集计算
  • handleNestedConditional:处理嵌套三元表达式

总结:VM还原触发SDK检测的根本原因是操作码75的逻辑错误,通过深度分析区间夹逼技术和隐式else分支处理机制,我们成功修复了所有操作码的捕获逻辑,确保还原后的代码与原始VM行为完全一致。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

某音还原异常解决
https://fatdog.20060113.xyz/posts/douyin-sdk/
作者
神秘大胖狗
发布于
2026-06-28
许可协议
MIT

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00