对于标准AES,直接hook就可以拿到初始密钥和iv。标准白盒AES呢,则需要通过DFA还原密钥。若是非标准白盒AES,则需要手动还原代码逻辑。
这里主要分享分析方法+工具,最终分析部分只写了第10轮的分析过程,分析流程都是一样的,处理1~9轮时,先分析第9轮的逻辑
for j in range(9.10)
,等分析完成后,再修改循环条件成for j in range(1.10)
即可。
分析工具主要是:jadx、ida、frida、unidbg,jadx和ida主要负责静态分析,frida、unidbg主要负责动态调试和trace。
分析方法:
逆向三大法宝:hook、trace、debug
hook:通过hook 可以快速的拿到参数与计算结果
trace:函数 trace可以帮助我们很快定位到关键位置,而指令trace可以辅助我们分析关键逻辑,而且trace会记录中间结果,方便我们分析时比对。
debug:对于一些参数是地址的,我们可以通过debug观察内存数据。
其他小技巧:
memory scan:通过监控内存读写,很精准定位到目标数据出现位置。unidbg有提供API,traceWrite/Read系列,还有龙哥写的小工具MemoryScan
从后向前分析,追踪数据流动。
这里主要是结合trace、unidbg 来分析,因为代码基本没有什么混淆,ida可以很清晰的看出来,所以会以ida的伪代码为主,然后结合unidbg console debug、trace中的数据来还原非标准白盒AES。
注:本文章仅做学习交流用,不可用于其他用途,侵权请联系,会第一时间删除。
1. 算法定位
分析目标:x-zse-96,版本:10.37.0
GET /mqtt/probe/pong?
1.1. Frida 检测绕过
目标app存在frida检测,检测位置位于init_array种注册的函数,在JNI_OnLoad执行之前。这里简单演示一下怎么绕过
接下来就是定位检测代码,并patch,绕过Frida检测。通常检测代码都是在so中,所以直接hook android_dlopen_ext,查看是哪个so加载后,frida才退出,定位检测位置。
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log(path)
if (path.indexOf(so_name) !== -1) {
this.match = true
}
}
},
onLeave: function (retval) {
if (this.match) {
console.log(so_name, "加载成功")
}
}
});
}
定位到检测so:libmsaoaidsec.so
之后hook JNI_Onload,定位检测代码位置,若检测代码是在init、init_array中执行,那么,hook JNI_OnlLoad就不会有输出;如果检测代码是在JNI_OnLoad中被调用,那么hook JNI_OnLoad OnEnter中代码就会被执行。
function hook_JNI_OnLoad(so_name){
var module = Process.findModuleByName(so_name);
var JNI_OnLoad = module.findExportByName("JNI_OnLOad")
Interceptor.attach(JNI_OnLoad, {
onEnter:function(args){
console.log("JNI_OnLoad onEnter");
},
onLeave:function(retval){
console.log("JNI_OnLoad onLeave");
}
});
}
hook之后没有输出,说明是在init、init_array中执行检测代码的。ida 查看init_proc中代码,可以直接定位到检测代码,具体分析思路可以看这篇文章libmsaoaidsec.so反frida分析
我们可以通过hook linker的call_constructors函数,来patch掉关键sub_1A8A0函数。需要获取linker64中call_constructors函数偏移,最终代码如下:
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log(path)
if (path.indexOf(so_name) !== -1) {
this.match = true
console.log(so_name, "加载成功")
hook_call_constructors(so_name, 0x50CF8, 0x1A8A0)
}
}
},
onLeave: function (retval) {
if (this.match) {
// hook_JNI_OnLoad(so_name)
}
}
});
}
function hook_call_constructors(so_name,call_constructors_func_off,func_off) {
var linker64_base_addr = Module.getBaseAddress("linker64")
var call_constructors_func_addr = linker64_base_addr.add(call_constructors_func_off)
var listener = Interceptor.attach(call_constructors_func_addr, {
onEnter: function (args) {
var module = Process.findModuleByName(so_name)
if (module != null) {
Interceptor.replace(module.base.add(func_off), new NativeCallback(function () {
console.log("replace funcOffset: ", func_off)
}, "void", []))
listener.detach()
}
},
})
}
1.2 算法定位
jadx直接字符串搜索"x-zse-96"即可,最终定位到:
编写hook 脚本验证:
function hook_java_laesEncryptByteArr(){
const CryptoTool = Java.use("com.bangcle.CryptoTool");
CryptoTool.laesEncryptByteArr.overload('[B', 'java.lang.String', '[B').implementation = function (bArr, str, bArr2) {
try {
console.log("\n[+] Java层调用开始");
// 输入参数记录
console.log(`Java Input:
PlainText: ${bytesToHex(bArr)}
Key: ${str}
IV: ${bytesToHex(bArr2)}`);
// 原始调用
const result = this.laesEncryptByteArr(bArr, str, bArr2);
// 输出记录
console.log(`Java Output: ${bytesToHex(result) || 'null'}`);
return result;
} catch (e) {
console.error(`Java层异常: ${e.stack}`);
return null;
} finally {
console.log("[+] Java层调用结束\n");
}
};
}
so中定位到Java_com_bangcle_CryptoTool_laesEncryptByteArr
函数
编写hook 代码验证
const parseJarray = (env, jarray) => {
if (jarray.isNull()) return null;
const len = env.getArrayLength(jarray);
const ptr = env.getByteArrayElements(jarray, null);
const data = ptr.readByteArray(len);
env.releaseByteArrayElements(jarray, ptr, 0);
return data;
};
function hook_native_laesEncryptByteArr(){
const targetSymbol = 'Java_com_bangcle_CryptoTool_laesEncryptByteArr';
const libName = 'libbangcle_crypto_tool.so';
const lib = Process.getModuleByName(libName);
if (!lib) {
console.error(`[!] 模块未加载: ${libName}`);
return;
}
const funcPtr = lib.getExportByName(targetSymbol);
if (!funcPtr) {
console.error(`[!] 符号未找到: ${targetSymbol}`);
return;
}
Interceptor.attach(funcPtr, {
onEnter: function(args) {
try {
const env = Java.vm.getEnv();
console.log("\n>>> Native进入 <<<");
// 参数解析
// 参数打印
console.log(`Native参数:
a3: ${bytesToHex(parseJarray(env,args[2]))}
a4: ${Java.vm.getEnv().getStringUtfChars(args[3], null).readCString()}
a5: ${bytesToHex(parseJarray(env,args[4]))}`);
} catch (e) {
console.error(`Native进入异常: ${e}`);
}
},
onLeave: function(retval) {
try {
const env = Java.vm.getEnv();
console.log(`Native返回值: ${bytesToHex(parseJarray(env,retval))}`);
} catch (e) {
console.error(`Native退出异常: ${e}`);
} finally {
console.log(">>> Native退出 <<<\n");
}
}
});
}
下来我们需要快速定位加密代码的位置,有很多种思路:
通过ida,静态分析代码流程,然后结合动态hook 验证
通过function trace,打印函数调用链,快速定位!
ida静态分析Java_com_bangcle_CryptoTool_laesEncryptByteArr
代码逻辑,发现最终调用sub_8B2C
函数:
分析sub_8B2C
函数,首先调用了init
函数,在里面对package name进行校验,然后再对dword_20024
赋值为1。
接着分析sub_8B2C
函数后面的代码逻辑,定位到实际调用的是Bangcle_AES_cbc_encrypt
函数。
使用stalker trace 记录 Java_com_bangcle_CryptoTool_laesEncryptByteArr
中函数调用,来辅助分析。trace需要结合主动调用使用,可以看到结果是和上面分析的一致,最终调用了Bangcle_internal_crypto
函数
function call_java_laesEncryptByteArr() {
Java.perform(function() {
try {
const CryptoTool = Java.use("com.bangcle.CryptoTool");
// 构造输入参数
var bArr = Java.array('byte', [
0x81, 0x83, 0x80, 0x23, 0x8d, 0x22, 0x8a, 0x23, 0x8d, 0x22, 0x8d, 0x8f,
0x82, 0x29, 0x28, 0x2a, 0x29, 0x88, 0x88, 0xa8, 0x22, 0x88, 0x80, 0x82,
0x38, 0x22, 0x28, 0x18, 0x1b, 0x8f, 0x2a, 0x8a
]);
// `str` 作为密钥传递时,确保是 Java `String` 类型
var str = Java.use('java.lang.String').$new(
"541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde"
);
// `bArr2` 作为 IV 传递时,确保是 Java `byte[]` 类型
var bArr2 = Java.array('byte', [
0x99, 0x30, 0x3a, 0x3a, 0x32, 0x34, 0x3a, 0x39, 0x92, 0x23, 0xa3, 0xb3,
0xa9, 0x99, 0x29, 0x2
]);
// 主动调用加密方法
var result = CryptoTool.laesEncryptByteArr(bArr, str, bArr2);
// 输出加密后的结果
console.log("Encrypted result: " + bytesToHex(result));
} catch (e) {
console.error("Error during encryption: " + e.message);
console.log("Stack trace: " + e.stack);
}
});
}
function traceFunctionCalls(targetAddr) {
const targetModule = Process.findModuleByAddress(targetAddr);
const moduleBase = targetModule.base;
const size = targetModule.size
Interceptor.attach(targetAddr,
{
onEnter: function (args) {
console.warn(JSON.stringify({
// fname: args[1].readCString(),
// text: new ObjC.Object(args[2]).toString(),
backtrace: Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).map(m => m.moduleName+'!'+m.name),
ctx: this.context
}, null, 2));
var tid = Process.getCurrentThreadId();
this.tid = tid;
Stalker.follow(tid, {
events: {
call: true
},
transform: function (iterator) {
var instruction;
while ((instruction = iterator.next()) !== null) {
iterator.keep();
if (instruction.mnemonic.startsWith('bl')) {
try {
const baseFirstAddress = instruction.address;
if(baseFirstAddress.compare(moduleBase) >= 0 &&
baseFirstAddress.compare(moduleBase.add(size)) < 0){
console.log('#' + tid + ':' + DebugSymbol.fromAddress(ptr(instruction.operands[0].value)));
}
} catch (e) {
}
}
}
}
});
},
onLeave: function (retval) {
Stalker.unfollow(this.tid);
Stalker.garbageCollect();
console.log("=====================================================")
}
})
}
function trace_func(){
const targetSymbol = 'Java_com_bangcle_CryptoTool_laesEncryptByteArr';
const libName = 'libbangcle_crypto_tool.so';
const lib = Process.getModuleByName(libName);
const funcPtr = lib.getExportByName(targetSymbol);
console.log(targetSymbol, funcPtr)
traceFunctionCalls(funcPtr)
}
用python处理下层级打印,更直观一些,stalker里面处理层级打印影响性能,有时候会卡死。
2. 算法还原
2.1. 准备工作
搭建unidbg模拟执行环境&主动调用。
package com.target;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class target extends AbstractJni {
public static AndroidEmulator emulator = null;;
public static Memory memory;
public static VM vm;
public static DalvikVM dalvikVM;
public static Module module;
public target(){
// 1. 创建模拟器,这里用xxx脱敏,实际需要填入apk的包名
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xxx.xxx").build();
// 2. 获取内存对象
memory = emulator.getMemory();
// 3. 设置Android SDK版本
memory.setLibraryResolver(new AndroidResolver(23));
// 4. 创建虚拟机
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/xxx/xxx1037.0.apk"));
// 5. 设置JNI
vm.setJni(this);
// 6. 是否显示调用细节
vm.setVerbose(true);
// 7. 加载so文件
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/xxx/libbangcle_crypto_tool.so"), false);
// 8. 执行JNI_OnLoad
dm.callJNI_OnLoad(emulator);
// 9. dm代表so文件,dm.getModule()得到module对象,基于module对象可以访问so中的成员。
module = dm.getModule();
}
// 编写主动调用
public void call_laesEncryptByteArr(){
DvmClass CryptoTool = vm.resolveClass("com.bangcle.CryptoTool");
String hex1 = "212880898d29828a8f8d2222222181838282822a8f8081838983802829838281";
String hex2 = "99303a3a32343a3992923a3b3a999292";
byte[] barr1 = hexStringToBytes(hex1);
String str = "541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde";
byte[] barr2 = hexStringToBytes(hex2);
// 2.unidbg的主动调用
DvmObject<?> result = CryptoTool.callStaticJniMethodObject(
emulator,
"laesEncryptByteArr([BLjava/lang/String;[B)[B",
barr1,
str,
barr2
);
// 转换为字节数组并输出
byte[] info = (byte[]) result.getValue();
String ret = bytesTohexString(info);
System.out.println("result: "+ret);
}
public static String bytesTohexString(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
public static byte[] hexStringToBytes(String hex) {
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; i++) {
int charPos = i * 2;
int byteVal = Integer.parseInt(
hex.substring(charPos, charPos + 2),
16
);
bytes[i] = (byte) (byteVal & 0xFF); // 确保符号位正确处理
}
return bytes;
}
public static void main(String[] args) {
target obj = new target();
obj.call_laesEncryptByteArr();
}
}
执行,会报错,原因是Unidbg 在内存管理方面存在一些问题,在内存的释放上尤为明显,munmap
以及 free
都有不低的出错概率。这里就是free
报错
编写patch free代码,完成patch
public void patchFree(){
Module libc = memory.findModule("libc.so");
long freeAddr = libc.findSymbolByName("free").getAddress();
emulator.attach().addBreakPoint(freeAddr, new BreakPointCallback(){
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 获取第一个参数
RegisterContext context = (RegisterContext) emulator.getContext();
long targetAddr = context.getPointerArg(0).peer;
System.out.println("targetPtr: "+ Long.toHexString(targetAddr));
// patch free 0x40052000
if (targetAddr == 0x40052000){
System.out.printf("[HOOK] Block free(0x%x) from IP=0x%x, LR=0x%x\n",
targetAddr, context.getPCPointer().peer, context.getLRPointer().peer);
// 强制跳过原函数:设置PC为返回地址(LR寄存器)
Backend backend = emulator.getBackend();
backend.reg_write(
Arm64Const.UC_ARM64_REG_PC,
backend.reg_read(Arm64Const.UC_ARM64_REG_LR) // 修改执行流
);
System.out.printf("[HOOK] Block free(0x%x) from IP=0x%x, LR=0x%x\n",
targetAddr, context.getPCPointer().peer, context.getLRPointer().peer);
}
return true;
}
});
}
得到正确结果:
为了后面分析方便,写个trace代码,将trace得到的数据保存到文件中,便于后面分析:
public void trace_code(String traceFile){
PrintStream traceStream = null;
try{
traceStream = new PrintStream(new FileOutputStream(traceFile));
}catch(FileNotFoundException e){
e.printStackTrace();
}
// 开启trace代码
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
System.out.println("trace file in " + traceFile);
}
2.2. 算法分析
1. 通过内存扫描快速定位
接下来开始分析,分析思路有很多种。一种是从前往后分析,从入参开始,完整分析函数逻辑,好处是能更清晰的了解函数逻辑,缺点是非常耗时。还有一种是根据从后往前分析,从返回值着手,跟踪数据的生成逻辑,好处是不会让你迷失在大量无关逻辑中,能更快速的分析数据的生成流程,缺点是对算法的了解不够全面清晰。
这里我们采用从返回值入手,从后往前分析,来分析它的生成逻辑。首先我们可以通过内存扫描的方式,来定位返回值第一次出现的位置。
// 执行时机要早,在设置SDK后执行 监听result,观察
memory.addModuleListener(new SearchData("9152c573dc9e9e48c85108d34a71ef6f", "libbangcle_crypto_tool.so", 1000));
在控制台输入shr
搜索可读的堆内存,得到目标地址0x40353030
,使用m
命令查看对应地址处数据,可以确认是result前32字节数据。
们的result是96字节长度,所以对目标地址0x40353030
,size是96范围进行tracewrite,监控数据写入,方便我们定位到第一次写入位置0x7024
。
// analysis方法,这里执行我们分析的一些逻辑
public void analysis(){
Debugger debugger = emulator.attach();
// 对 0x40353030 ~ 0x40353030+96 范围内的内存写入进行监控,定位第一次数据写入位置
emulator.traceWrite(0x40353030, 0x40353030+96);
}
// main 中调用
public static void main(String[] args) {
Clz obj = new Clz();
// 在主动调用之前调用
obj.analysis();
obj.patchFree();
obj.call_laesEncryptByteArr();
}
ida中跳转到0x7024
,发现该指令位于函数Bangcle_WB_LAES_encrypt
(0x5D7C)内。
2. Bangcle_CRYPTO_cbc128_encrypt
在函数Bangcle_WB_LAES_encrypt
入口添加断点,然后使用bt
打印调用栈,可以看到是在Bangcle_CRYPTO_cbc128_encrypt
中调用。
IDA跳转到Bangcle_CRYPTO_cbc128_encrypt
进行分析,可以看到Bangcle_WB_LAES_encrypt
的入参a2
也就地址0x0353030
处的数据是由a1
与a4
按位异或a2[i]=a1[i]^a4[i]
得到的。
我们hook Bangcle_CRYPTO_cbc128_encrypt
,打印其a1、a4的内容。Java
public void hook_Bangcle_CRYPTO_cbc128_encrypt(Debugger debugger){
final int[] count = {1}; // 调用次数
debugger.addBreakPoint(module.base + 0x5A6C, new BreakPointCallback(){
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 获取context
RegisterContext context = (RegisterContext) emulator.getContext();
// 打印参数
Pointer a1 = context.getPointerArg(0);
long a2Addr = context.getPointerArg(0).peer;
Pointer a4 = context.getPointerArg(3);
long a4Addr = context.getPointerArg(3).peer;
// System.out.println("第 "+ count[0] +" 次调用");
count[0] +=1;
System.out.println("Bangcle_CRYPTO_cbc128_encrypt onEnter a1 0x" + Long.toHexString(a2Addr) + ": " + bytesTohexString(a1.getByteArray(0, 0x20)));
System.out.println("Bangcle_CRYPTO_cbc128_encrypt onEnter a4 0x" + Long.toHexString(a4Addr) + ": " + bytesTohexString(a4.getByteArray(0, 16)));
return true;
}
});
}
ES CBC 模式,会先将明文与iv进行异或,之后再进行加密。这里的a4只有16字节长度,所以a4应该是iv。
所以Bangcle_CRYPTO_cbc128_encrypt
的流程就是将PlainText
以16字节为一组,然后按位与iv进行异或,之后调用Bangcle_WB_LAES_encrypt
进行加密,第一轮的iv是输入值,后续的iv都是上一轮的加密结果。
def CRYPTO_cbc128_encrypt(plant_text, iv):
plant_text_len = len(plant_text)
result = []
count = 0
while(plant_text_len > 15):
a2 = []
for i in range(0,16):
a2.append(plant_text[i + count * 16] ^ iv[i])
tmp = WB_AES_decrypt(a2) # 此处调用Bangcle_WB_LAES_encrypt
for data in tmp:
result.append(data)
plant_text_len -= 16
iv = tmp
count += 1
return result
2. Bangcle_WB_LAES_encrypt
1.参数&返回值分析:
此函数是根据内存扫描快速定位的。函数地址是0x05d7c。
通过trace搜索0x05d7c可以观察到有3处调用,我们之前得到的加密后结果是HexString格式,长度是96个字符,但是实际长度需要除以2,即实际byte数量是48,而根据我们之前traceWrite可以明显看到是16字节为一组,3*16==48
,这点也对上了。
F5反汇编分析Bangcle_WB_LAES_encrypt
,可以看到大量的查找表操作。通过百度搜索表中常量,也没有什么结果,判断应该是非标准白盒AES。
使用unidbg 对Bangcle_WB_LAES_encrypt
下一个断点,调试一下,主要分析一下入参与返回值。代码如下:emulate.attach().addBreakPoint(module.base + 0x5D7C);
为Bangcle_WB_LAES_encrypt
有三个参数,所以我们主要关注x0、x1、x2的值,观观x0=0x40353030 x1=0x40353030 x2=0xbffff530
,应该都是地址,使用m
对内存查看一数据:
由于x0
、x1
指向同一个地址,所以只打印x0即可。mx0
打印x0寄存器指向的地址的内存,观察发现,和我们之前主动调用传入的参数,这是因为之前CBC处理,第一次和iv异或作为输入,后面都是和前一次密文进行异或或,作为AES输入。
打印x2处数据,可以看到保存了一个地址0x403580C0
,打印一下对应数据,可以发现是一个256字节的表。
参数分析完了,
blr
对LR
下一个断点,之后分析返回数据。
使用c
程序执行到LR处断下,然后分析,这里x0
是0x10,应该是数据的长度。
我们之前分析输入的参数地址是0x40353030
,使用m
来打印一下对应地址的内存数据:m0x40353030
,可以看到和我们的加密结果一致!,所以0x40353030
就是保存加密后结果的地址
编写一下hook,打印一下参数与执行之后的结果。
public void hook_Bangcle_WB_LAES_encrypt(Debugger debugger){
final int[] count = {1}; // 调用次数
debugger.addBreakPoint(module.base + 0x5D7C, new BreakPointCallback(){
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 获取context
RegisterContext context = (RegisterContext) emulator.getContext();
// 打印参数
Pointer x0 = context.getPointerArg(0);
long x0Addr = context.getPointerArg(0).peer;
System.out.println("第 "+ count[0] +" 次调用");
count[0] +=1;
System.out.println("onEnter 0x" + Long.toHexString(x0Addr) + ": " + bytesTohexString(x0.getByteArray(0, 16)));
// hook LR,在函数返回时打印
debugger.addBreakPoint(context.getLRPointer().peer, new BreakPointCallback(){
@Override
public boolean onHit(Emulator<?> emulator, long address) {
System.out.println("onLeave 0x" + Long.toHexString(x0Addr) + ": " + bytesTohexString(x0.getByteArray(0, 16)));
return true;
}
});
return true;
}
});
}
2. 白盒AES加密流程分析。
分析完参数与返回值,接着就开始非标准白盒AES逻辑,看着图再次回忆一下AES128逻辑。
一般逆向分析我们是从后往前分析,所以我们先看Round10,逆向分析一般是从后往前分析,紧跟数据,结合trace得到的结果,然后配合上unidbg debug动态调试。下面开始分析。
1. 轮密钥加
先看轮密钥加部分,为什么我们可以将这段代码识别成轮密钥加?首先正常AES种轮密钥加是将状态矩阵与轮密钥进行异或。矩阵在代码中可以用数组表示,异或操作是^
,而且这里是循环16次,而AES128分组长度就是16字节!所以通过上面的特征,我们判断这段代码是在进行轮密钥加。
将其转为python代码就是:
j = 10 # 执行完1~9轮后,j就为10了
for i in range(16):
# 保存高位数据
# (16 * (byte_B448[(16 * (*(&v20 + i) >> 4)) ^ (*(v53 + 16 * j + i) >> 4) & 0xF] >> 4))
idx = (16 * (v20[i] >> 4)) ^ (v53[j*16 + i] >> 4) & 0xF
first_part = (16 * (byte_B448[idx] >> 4))
# print("first_part idx: " + hex(idx))
# 保存低位数据
# (byte_B448[(16 * *(&v20 + i)) ^ *(v53 + 16 * j + i) & 0xF] >> 4)
idx = ((16 * v20[i]) ^ v53[j*16 + i] & 0xF) & 0xFF
# print("second_part idx: " + hex(idx))
second_part = (byte_B448[idx] >> 4)
tmp = first_part ^ second_part
# print(str(i+1) + " " + str(hex(tmp)))
result.append(tmp)
下来要解决这些byte_B448
、v53
、v20
的数据,首先是byte_B448
,ida跳转对应位置,然后复制出来即可。
接着是v53
,ida向上追踪,确认是第三个参数保存的地址,根据我们之前分析的第三个参数0x403580C0
处保存的是一张11 * 16 大小的表,推测应该是保存轮密钥的表。
将其复制到Python中。
接着再分析v20
,需要用到我们trace的日志。首先ida查取v20值处指令地址0x06f5c
接着去log日志中搜索0x06f5c
定位,分析指定,发现w1
的值就是v20的数据。
将前16个字节复制到python中
运行我们还原的python代码,查看结果,可以确认我们的代码逻辑没有问题,运行结果是一致的。
2. 循环左移与字节替换
由于v20
是动态生成的,所以我们需要继续向上追踪,分析v20的生成逻辑。给v20~v35
赋值,因为栈中数据是连续的,所以实际上是给v20
数组赋值,值来源于表byte_C548
,和之前操作一样,将其复制到python中,这里就不演示了。
字节替换很好识别,实际就是查表操作,仔细观察索引对应关系,很像AES中的行位移。
# v20 数组 # v36数组
# 00 01 02 03 00 05 10 15
# 04 05 06 07 04 09 14 03
# 08 09 10 11 08 13 02 07
# 12 13 14 15 12 01 06 03
# 因为状态矩阵state是从上到下的
# 换一个方向观察,是不是就是循环左移操作
# 00 04 08 12 ==> 第0行,循环左移0次 ==> 00 04 08 12
# 01 05 09 13 ==> 第1行,循环左移1次 ==> 05 09 13 01
# 02 06 10 14 ==> 第2行,循环左移2次 ==> 10 14 02 06
# 03 07 11 15 ==> 第3行,循环左移3次 ==> 15 03 07 11
# 所以他们之间的关系就是,每列循环上移,第0列上移0次、第1列上移1次...
# 根据这个索引对应关系,还原代码
v20 = []
for i in range(16):
# v20[i]
idx = (i * 5) % 16
tmp = byte_C548[v36[idx]]
v20.append(tmp)
接下来我们需要定位到最后一次v36
数组赋值位置0x06cc8
。
在trace文件中搜索0x06cc8
,发现有432条指令。
因为函数调用了3次,而且v36
数组大小为16,会循环16次,所以432 / 3 / 16 = 9,每次都会有9轮给v36
数组赋值操作,我们需要的是第1次调用的第9轮赋值,010editor,可以很方便定位到!仔细观察左边的橘黄色代表一轮,然后有一根金黄色的线表示当前选中指令在哪个轮次。
手动构造一下v36
,然后执行,输出的v20
和我们之前手动构造的一致。
到这里第10轮的三个流程都分析完了,之后就是继续向上分析,先分析第9轮的代码逻辑for j in range(9, 10)
,等第9轮代码逻辑分析出来后,再将循环改为for j in range(1, 10)
即可。最后再分析第1次的轮密钥加,就完成了整体算法的还原,这里就不作演示了,因为操作和上面是一致的。
2.3 最终还原代码:
这里受限于篇幅,这里就不贴那些表了。
from table import *
plant_text = "212880898d29828a8f8d2222222181838282822a8f8081838983802829838281"
iv = "99303a3a32343a3992923a3b3a999292"
a2_v53 = []
def hexStringToBytes(hexString):
clean_hex = hexString.replace(' ', '')
return bytes.fromhex(clean_hex)
# CBC模式
def CRYPTO_cbc128_encrypt(plant_text, iv):
plant_text_len = len(plant_text)
result = []
count = 0
while(plant_text_len > 15):
a2 = []
for i in range(0,16):
a2.append(plant_text[i + count * 16] ^ iv[i])
tmp = WB_lAES_encrypt(a2)
for data in tmp:
result.append(data)
plant_text_len -= 16
iv = tmp
count += 1
return result
def hexprint(tmp):
for i in range(0, len(tmp)):
print(hex(tmp[i]),end=" ")
if (i+1) % 16 == 0:
print("")
def WB_lAES_encrypt(a1):
# 计算v36
v36 = []
v20 = []
# 第0次轮密钥加
for i in range(16):
# *(&v36 + i) = (16 * (byte_B248[(16 * (*(a1 + i) >> 4)) ^ (*(v53 + i) >> 4) & 0xF] >> 4)) ^ (byte_B248[(16 * *(a1 + i)) ^ *(v53 + i) & 0xF] >> 4);
idx = (16 * (a1[i] >> 4)) ^ (v53[i] >> 4) & 0xF
first_part = (16 * (byte_B248[idx] >> 4))
idx = ((16 * a1[i]) ^ v53[i] & 0xF) & 0xFF
second_part = (byte_B248[idx] >> 4)
tmp = first_part ^ second_part
v36.append(tmp)
# print("v36")
# hexprint(v36)
for j in range(1,10):
# print("第 "+str(j)+" 轮")
# print("v36")
# hexprint(v36)
# 第8轮执行结束之后的 v36
# v36 = [0xed, 0x76, 0x2f, 0xe3, 0x4a, 0x57, 0xb2, 0xc8, 0xb7, 0x49, 0x42, 0xc1, 0x41, 0xac, 0xb1, 0xa1]
# 计算v20
int_list = [dword_B548[v36[0]], dword_B548[v36[4]], dword_B548[v36[8]], dword_B548[v36[12]]]
v20 = convert_to_byte_list(int_list)
# print("v20")
# hexprint(v20)
# 计算v4
int_list = [dword_B948[v36[5]], dword_B948[v36[9]], dword_B948[v36[13]], dword_B948[v36[1]]]
v4 = convert_to_byte_list(int_list)
# print("v4")
# hexprint(v4)
# 计算v20
for i in range(16):
# *(&v20 + i) = (16 * (byte_B348[(16 * (*(&v20 + i) >> 4)) ^ (*(&v4 + i) >> 4) & 0xF] >> 4)) ^ (byte_B348[(16 * *(&v20 + i)) ^ *(&v4 + i) & 0xF] >> 4);
idx = (16 * (v20[i] >> 4)) ^ (v4[i] >> 4) & 0xF
first_part = (16 * (byte_B348[idx] >> 4))
idx = ((16 * v20[i]) ^ v4[i] & 0xF) & 0xFF
second_part = (byte_B348[idx] >> 4)
tmp = first_part ^ second_part
v20[i] = tmp
# print("v20")
# hexprint(v20)
# v20 = [0x5b, 0xd9, 0x63, 0x79, 0x94, 0xc8, 0x09, 0x1c, 0x49, 0x4b, 0x3d, 0xb9, 0xf9, 0x11, 0x05, 0x6c]
# 计算v4
int_list = [dword_BD48[v36[10]], dword_BD48[v36[14]], dword_BD48[v36[2]], dword_BD48[v36[6]]]
v4 = convert_to_byte_list(int_list)
# print("v4")
# hexprint(v4)
# 计算v20
for i in range(16):
# *(&v20 + i) = (16 * (byte_B348[(16 * (*(&v20 + i) >> 4)) ^ (*(&v4 + i) >> 4) & 0xF] >> 4)) ^ (byte_B348[(16 * *(&v20 + i)) ^ *(&v4 + i) & 0xF] >> 4);
idx = (16 * (v20[i] >> 4)) ^ (v4[i] >> 4) & 0xF
first_part = (16 * (byte_B348[idx] >> 4))
idx = ((16 * v20[i]) ^ v4[i] & 0xF) & 0xFF
second_part = (byte_B348[idx] >> 4)
tmp = first_part ^ second_part
v20[i] = tmp
# print("v20")
# hexprint(v20)
#v20 = [0x4a, 0x55, 0xbc, 0x68, 0xf4, 0x88, 0x99, 0x5c, 0xfc, 0x91, 0xad, 0x0c, 0x25, 0xb4, 0x3a, 0x5f]
# 计算v4
int_list = [dword_C148[v36[15]], dword_C148[v36[3]], dword_C148[v36[7]], dword_C148[v36[11]]]
v4 = convert_to_byte_list(int_list)
# print("v4")
# hexprint(v4)
# v4 = [0xe0, 0xe0, 0x82, 0x64, 0xdc, 0xdc, 0x58, 0x74, 0x7a, 0x7a, 0x99, 0x48, 0xf0, 0xf0, 0x62, 0x17]
# 计算v20
for i in range(16):
# *(&v20 + i) = (16 * (byte_B348[(16 * (*(&v20 + i) >> 4)) ^ (*(&v4 + i) >> 4) & 0xF] >> 4)) ^ (byte_B348[(16 * *(&v20 + i)) ^ *(&v4 + i) & 0xF] >> 4);
idx = (16 * (v20[i] >> 4)) ^ (v4[i] >> 4) & 0xF
first_part = (16 * (byte_B348[idx] >> 4))
idx = ((16 * v20[i]) ^ v4[i] & 0xF) & 0xFF
second_part = (byte_B348[idx] >> 4)
tmp = first_part ^ second_part
v20[i] = tmp
# print("v20:")
# hexprint(v20)
# v20 = [ 0x26, 0xa8, 0x86, 0xb1, 0xa8, 0x24, 0x6a, 0xd8, 0xe7, 0x44, 0x83, 0xf4, 0x9a, 0xff, 0x73, 0x2e]
# 计算v36
for i in range(16):
# *(&v36 + i) = (16 * (byte_B348[(16 * (*(&v20 + i) >> 4)) ^ (*(v53 + 16 * j + i) >> 4) & 0xF] >> 4)) ^ (byte_B348[(16 * *(&v20 + i)) ^ *(v53 + 16 * j + i) & 0xF] >> 4);
idx = (16 * (v20[i] >> 4)) ^ (v53[16 * j + i] >> 4) & 0xF
first_part = (16 * (byte_B348[idx] >> 4))
idx = ((16 * v20[i]) ^ v53[j * 16 + i] & 0xF) & 0xFF
second_part = (byte_B348[idx] >> 4)
tmp = first_part ^ second_part
v36[i] = tmp
# print("v36: ")
# hexprint(v36)
# 第一次调用 第9轮 v36
# v36 = [0xd3, 0xb0, 0xd0, 0x67,
# 0xff, 0x7f, 0x21, 0x17,
# 0x0e, 0x26, 0x64, 0x0f,
# 0xc7, 0xe4, 0x92, 0x82]
# 计算v20
# 行位位移操作与字节替换
v20 = []
for i in range(16):
# v20[i]
idx = (i * 5) % 16
tmp = byte_C548[v36[idx]]
v20.append(tmp)
# print("v20: ")
# hexprint(v20)
result = []
# *(planText_1 + i) = (16 * (byte_B448[(16 * (*(&v20 + i) >> 4)) ^ (*(v53 + 16 * j + i) >> 4) & 0xF] >> 4))
# ^ (byte_B448[(16 * *(&v20 + i)) ^ *(v53 + 16 * j + i) & 0xF] >> 4);
j = 10
for i in range(16):
# 保存高位数据
# (16 * (byte_B448[(16 * (*(&v20 + i) >> 4)) ^ (*(v53 + 16 * j + i) >> 4) & 0xF] >> 4))
idx = (16 * (v20[i] >> 4)) ^ (v53[j*16 + i] >> 4) & 0xF
first_part = (16 * (byte_B448[idx] >> 4))
# print("first_part idx: " + hex(idx))
# 保存低位数据
# (byte_B448[(16 * *(&v20 + i)) ^ *(v53 + 16 * j + i) & 0xF] >> 4)
idx = ((16 * v20[i]) ^ v53[j*16 + i] & 0xF) & 0xFF
# print("second_part idx: " + hex(idx))
second_part = (byte_B448[idx] >> 4)
tmp = first_part ^ second_part
# print(str(i+1) + " " + str(hex(tmp)))
result.append(tmp)
return result
result = CRYPTO_cbc128_encrypt(hexStringToBytes(plant_text), hexStringToBytes(iv))
# a1 = [0xb8, 0x18, 0xba, 0xb3,
# 0xbf, 0x1d, 0xb8, 0xb3,
# 0x1d, 0x1f, 0x18, 0x19,
# 0x18, 0xb8, 0x13, 0x11]
# result = WB_lAES_encrypt(a1)
print("result: ")
hexprint(result)