对于标准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");
            }
        }
    });
}

下来我们需要快速定位加密代码的位置,有很多种思路:

  1. 通过ida,静态分析代码流程,然后结合动态hook 验证

  2. 通过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处的数据是由a1a4按位异或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对内存查看一数据:

由于x0x1指向同一个地址,所以只打印x0即可。mx0打印x0寄存器指向的地址的内存,观察发现,和我们之前主动调用传入的参数,这是因为之前CBC处理,第一次和iv异或作为输入,后面都是和前一次密文进行异或或,作为AES输入。

打印x2处数据,可以看到保存了一个地址0x403580C0,打印一下对应数据,可以发现是一个256字节的表。

参数分析完了,blrLR下一个断点,之后分析返回数据。

使用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逻辑。

image-twMS.png

一般逆向分析我们是从后往前分析,所以我们先看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_B448v53v20的数据,首先是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)