FRIDA辅助分析非标准算法
什么是非标准算法
在 Android 逆向分析中,我们遇到的加密算法大致可以分为两类:标准算法和非标准算法。
- 标准算法:AES、DES、RSA、MD5、SHA 系列等被广泛使用的密码学算法,有公开的规范和实现
- 非标准算法:开发者自行实现的加密方案,包括魔改标准算法、白盒加密、自定义迭代加密等
非标准算法是逆向分析中最具挑战性的部分,因为没有现成的算法库可以直接调用,必须完整还原其实现逻辑。
非标准算法的常见类型
| 类型 | 描述 | 难度 |
|---|---|---|
| 魔改 AES/DES | 修改 S-Box、调整轮数、改变 Feistel 结构 | ★★★ |
| 自定义迭代加密 | 多轮 XOR + 位移 + 查表的组合 | ★★★★ |
| 白盒加密 | 将密钥编码到查找表中,隐藏密钥 | ★★★★★ |
| 自定义哈希 | 非 MD5/SHA 的自定义摘要算法 | ★★★ |
| 流密码变体 | RC4 改进版、自定义 LFSR | ★★★★ |
| 多算法组合 | AES + 自定义变换 + Base64 变种 | ★★★★ |
非标准算法的识别特征
在分析 SO 文件时,以下特征可以帮助我们识别非标准算法的存在:
1. 大表查表
// Hook 内存读取,检测异常的查表行为
function detectLargeTableAccess(moduleName) {
var mod = Process.findModuleByName(moduleName);
var readCount = 0;
var readPatterns = {};
// Hook 目标函数
var funcAddr = mod.base.add(0x1A2B);
Interceptor.attach(funcAddr, {
onEnter: function (args) {
readCount = 0;
readPatterns = {};
}
});
// 监控该模块内的内存读取
// 通过在关键点 Hook 来统计
var checkPoints = [0x1A40, 0x1A55, 0x1A68, 0x1A80];
checkPoints.forEach(function (off) {
Interceptor.attach(mod.base.add(off), {
onEnter: function (args) {
readCount++;
// 记录每次读取的地址模式
var addr = this.context.x0;
var modAddr = addr.sub(mod.base);
readPatterns[modAddr.toString()] =
(readPatterns[modAddr.toString()] || 0) + 1;
}
});
});
// 延迟输出分析结果
setTimeout(function () {
var entries = Object.keys(readPatterns);
console.log("\n[查表分析]");
console.log("总读取次数: " + readCount);
console.log("不同地址数: " + entries.length);
if (entries.length > 100 && readCount > 500) {
console.log("⚠️ 大量查表行为,可能是自定义 S-Box 或白盒加密");
}
}, 3000);
}
2. 异常的迭代次数
// 追踪循环次数
function traceLoopIterations(moduleName, loopStartOffset) {
var mod = Process.findModuleByName(moduleName);
var loopAddr = mod.base.add(loopStartOffset);
var iterationCount = 0;
var maxIterations = 0;
Interceptor.attach(loopAddr, {
onEnter: function (args) {
iterationCount = 0;
this._startTime = Date.now();
},
onLeave: function (retval) {
var elapsed = Date.now() - this._startTime;
if (iterationCount > maxIterations) {
maxIterations = iterationCount;
}
console.log("[循环] 迭代次数: " + iterationCount +
" 耗时: " + elapsed + "ms");
if (iterationCount > 16) {
console.log("⚠️ 迭代次数异常多,可能是自定义加密算法");
console.log(" 标准 AES: 10/12/14 轮");
console.log(" 标准 DES: 16 轮");
console.log(" 标准 MD5: 64 步");
}
}
});
// 追踪循环体的每次执行
var loopBodyAddr = mod.base.add(loopStartOffset + 0x10);
try {
Interceptor.attach(loopBodyAddr, {
onEnter: function (args) {
iterationCount++;
}
});
} catch (e) {
// 可能不是函数入口,无法 Hook
}
}
3. 奇怪的常量
// 检测函数中使用的非常量
function detectUnusualConstants(moduleName, funcOffset, funcSize) {
var mod = Process.findModuleByName(moduleName);
var startAddr = mod.base.add(funcOffset);
// 已知的魔数常量
var knownConstants = {
0x637C777B: "AES S-Box 前缀",
0x67452301: "MD5 初始值 A",
0xEFCDAB89: "MD5 初始值 B",
0x98BADCFE: "MD5 初始值 C",
0x10325476: "MD5 初始值 D",
0x5A827999: "SHA-1 常量 K0",
0x6ED9EBA1: "SHA-1 常量 K1",
0x01010101: "AES/DES 填充"
};
console.log("[常量检测] 扫描 " + funcOffset +
" - 0x" + (funcOffset + funcSize).toString(16));
// 通过读取内存来扫描常量
var pattern = [];
for (var offset = 0; offset < funcSize; offset += 4) {
try {
var val = startAddr.add(offset).readU32();
if (knownConstants[val]) {
console.log(" [已知] 0x" + val.toString(16) +
" @ +0x" + offset.toString(16) +
" → " + knownConstants[val]);
}
// 检测大数值常量(可能是自定义的)
if (val > 0x10000000 && val < 0xFFFFFFFF &&
!knownConstants[val]) {
// 可能是自定义常量
}
} catch (e) {}
}
}
使用 Frida 追踪算法执行过程
参数→运算→输出的完整追踪
// 算法执行过程追踪器
function traceAlgorithmExecution(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
Interceptor.attach(funcAddr, {
onEnter: function (args) {
console.log("\n[算法执行开始]");
console.log("时间: " + new Date().toISOString());
// 记录输入参数
this._input = {
arg0: args[0],
arg1: args[1],
arg2: args[2] ? args[2].toInt32() : 0
};
// 尝试读取输入数据
try {
var inputLen = args[2].toInt32();
if (inputLen > 0 && inputLen < 4096) {
console.log("[输入] 长度: " + inputLen);
console.log(hexdump(args[0], { length: inputLen }));
}
} catch (e) {}
// 保存初始寄存器状态
this._initialRegs = {
x0: this.context.x0,
x1: this.context.x1,
x2: this.context.x2
};
},
onLeave: function (retval) {
console.log("\n[算法执行结束]");
console.log("[输出] 返回值: " + retval);
// 尝试读取输出缓冲区
try {
var outputLen = this._input.arg2;
if (outputLen > 0 && outputLen < 4096) {
console.log("[输出缓冲区]");
console.log(hexdump(this._input.arg0, {
length: outputLen
}));
}
} catch (e) {}
}
});
}
traceAlgorithmExecution("libcrypto.so", 0x4A20);
追踪中间运算结果
在算法的关键运算节点设置 Hook,记录中间值:
// 在算法的多个关键点记录运算结果
function traceIntermediateValues(moduleName, tracePoints) {
var mod = Process.findModuleByName(moduleName);
var callIndex = 0;
tracePoints.forEach(function (point) {
var addr = mod.base.add(point.offset);
var label = point.label || ("0x" + point.offset.toString(16));
try {
Interceptor.attach(addr, {
onEnter: function (args) {
callIndex++;
var ctx = this.context;
// 记录关键寄存器的值
var snapshot = {
index: callIndex,
label: label,
x0: ctx.x0.toInt32(),
x1: ctx.x1.toInt32(),
x2: ctx.x2.toInt32(),
x3: ctx.x3.toInt32()
};
console.log("[T" + callIndex + "][" + label + "] " +
"x0=0x" + snapshot.x0.toString(16) + " " +
"x1=0x" + snapshot.x1.toString(16) + " " +
"x2=0x" + snapshot.x2.toString(16) + " " +
"x3=0x" + snapshot.x3.toString(16));
// 如果指定了内存读取点
if (point.readPtr) {
try {
var data = ctx[point.readPtr.reg];
var len = point.readPtr.len || 16;
console.log(" 内存[" + point.readPtr.reg + "]:");
console.log(hexdump(data, { length: len }));
} catch (e) {}
}
}
});
console.log("[+] 追踪点: " + label + " @ 0x" +
point.offset.toString(16));
} catch (e) {
console.log("[-] 无法 Hook: " + label);
}
});
}
// 使用示例:在加密算法的关键运算点设置追踪
traceIntermediateValues("libcrypto.so", [
{ offset: 0x4A30, label: "SubBytes 入口" },
{ offset: 0x4A60, label: "ShiftRows 入口" },
{ offset: 0x4A90, label: "MixColumns 入口",
readPtr: { reg: "x0", len: 16 } },
{ offset: 0x4AC0, label: "AddRoundKey 入口" },
{ offset: 0x4B00, label: "最终输出" }
]);
Taint Analysis 思想在 Frida 中的应用
Taint Analysis(污点分析)是一种跟踪数据流传播的技术。在 Frida 中,我们可以手动实现类似的效果——标记输入数据的来源,然后追踪这些数据在算法中的传播路径。
简易污点追踪
// Frida 中的简易污点分析
function taintTrack(moduleName, funcOffset, inputSize) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var taintedBytes = {};
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// 标记输入数据为"已污染"
var inputPtr = args[0];
var taintId = "INPUT_" + Date.now();
console.log("[污点分析] 标记输入 " + inputSize + " 字节");
// 在多个关键点检查是否有"污染数据"参与运算
var checkPoints = [0x4A30, 0x4A50, 0x4A80, 0x4AB0, 0x4AE0];
checkPoints.forEach(function (cpOff) {
try {
Interceptor.attach(mod.base.add(cpOff), {
onEnter: function (args) {
// 比较当前寄存器值与原始输入
var ctx = this.context;
var regs = ['x0', 'x1', 'x2', 'x3'];
regs.forEach(function (reg) {
var val = ctx[reg];
// 检查该值是否指向输入数据区域
var offset = val.sub(inputPtr);
if (offset.compare(ptr(0)) >= 0 &&
offset.compare(ptr(inputSize)) < 0) {
console.log("[污点传播] " +
reg + " 指向输入偏移 " +
offset.toInt32() + " @ 0x" +
cpOff.toString(16));
}
});
}
});
} catch (e) {}
});
}
});
}
非标准算法的还原策略
还原方法论
还原非标准算法通常遵循以下策略:
1. 黑盒测试
→ 使用不同输入多次调用,观察输入输出关系
→ 判断是加密/哈希/编码
2. 特征识别
→ 分析 S-Box、迭代次数、常量
→ 判断是否基于某个标准算法魔改
3. 动态追踪
→ 使用 Frida 记录每一步运算
→ 构建运算序列
4. 分步还原
→ 将算法拆分为独立的步骤
→ 逐个步骤还原并验证
5. 完整实现
→ 用 Python/C 重写还原的算法
→ 对比验证
黑盒测试脚本
// 算法黑盒测试
function blackBoxTest(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
// 准备测试用例
var testCases = [
{ input: [0x41, 0x41, 0x41, 0x41], desc: "AAAA" },
{ input: [0x00, 0x00, 0x00, 0x00], desc: "NULL" },
{ input: [0xFF, 0xFF, 0xFF, 0xFF], desc: "0xFF" },
{ input: [0x01, 0x02, 0x03, 0x04], desc: "递增" },
{ input: [0x48, 0x65, 0x6C, 0x6C, 0x6F], desc: "Hello" }
];
var results = [];
var testIndex = 0;
// 分配输入缓冲区
var inputBuf = Memory.alloc(1024);
var outputBuf = Memory.alloc(1024);
testCases.forEach(function (tc) {
// 写入测试输入
for (var i = 0; i < tc.input.length; i++) {
inputBuf.add(i).writeU8(tc.input[i]);
}
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// 替换输入为我们的测试数据
args[0] = inputBuf;
args[2] = ptr(tc.input.length);
this._testCase = tc;
},
onLeave: function (retval) {
// 读取输出
var output = [];
for (var i = 0; i < 32; i++) {
try {
output.push(("0" +
outputBuf.add(i).readU8().toString(16))
.slice(-2));
} catch (e) {
break;
}
}
console.log("\n[测试] " + tc.desc +
" (len=" + tc.input.length + ")");
console.log(" 输入: " + tc.input.map(function (b) {
return ("0" + b.toString(16)).slice(-2);
}).join(" "));
console.log(" 输出: " + output.join(" "));
}
});
});
}
实际案例:某 APP 的自定义加密还原
以某 APP 的自定义加密函数为例,展示完整的还原过程。
分析发现:
- 函数接收 16 字节输入,输出 16 字节
- 内部有一个 256 字节的查找表(自定义 S-Box)
- 共执行 4 轮变换,每轮包含:查表替换 → 行移位 → 列混合
- 列混合使用的不是 AES 标准的 GF(2^8) 乘法,而是自定义的运算
还原步骤:
// Step 1: 提取自定义 S-Box
function extractCustomSBox(moduleName, sboxOffset) {
var mod = Process.findModuleByName(moduleName);
var sboxAddr = mod.base.add(sboxOffset);
var sbox = [];
console.log("[S-Box 提取]");
for (var i = 0; i < 256; i++) {
sbox.push(sboxAddr.add(i).readU8());
}
// 输出为 C 数组格式
var lines = [];
for (var row = 0; row < 16; row++) {
var hexValues = [];
for (var col = 0; col < 16; col++) {
hexValues.push("0x" +
("0" + sbox[row * 16 + col].toString(16)).slice(-2));
}
lines.push(" " + hexValues.join(", "));
}
console.log("uint8_t sbox[256] = {\n" +
lines.join(",\n") + "\n};");
return sbox;
}
// Step 2: 验证每轮变换的输入输出
function verifyRoundTransform(moduleName, roundOffsets) {
var mod = Process.findModuleByName(moduleName);
roundOffsets.forEach(function (off, round) {
var addr = mod.base.add(off);
Interceptor.attach(addr, {
onEnter: function (args) {
console.log("\n[第 " + (round + 1) + " 轮变换]");
var state = [];
for (var i = 0; i < 16; i++) {
state.push(("0" +
args[0].add(i).readU8().toString(16)).slice(-2));
}
console.log(" 输入状态: " + state.join(" "));
},
onLeave: function (retval) {
// 某些变换可能就地修改,需要重新读取
}
});
});
}
// Step 3: 对比还原算法与原始函数
// 在 Python 端实现还原的算法,然后通过 Frida 对比验证
总结
非标准算法是 Android 逆向中的核心难题。本文介绍了非标准算法的识别特征、使用 Frida 追踪算法执行过程的方法、Taint Analysis 思想的应用,以及从黑盒测试到分步还原的完整策略。关键要点是:先通过黑盒测试理解算法的输入输出特性,再通过动态追踪理解内部运算过程,最后逐步还原并用对比验证确保正确性。