某幸咖啡逆向及DFA还原白盒AES密钥
zsk Lv4

样例APP:某幸咖啡
版本:5.0.01
下面会用DFA方式对该样本白盒AES分析还原

抓包脱壳

首先上来先抓包,这里直接抓包首页商品推荐,无论登录还是搜索接口都是一样的参数加密sign和q值,sign的长度不是固定的38,39,40位都有
发现是360的壳
image使用r0ysue的脱壳工具 https://github.com/r0ysue/frida_dump

1
frida -U --no-pause -f packagename  -l dump_dex.js

脱壳后的dex文件夹整个拉进jadx
用搜索关键词的方式没定位到,可能是字符串加密了
image请求表单大都是用hashmap put添加,这里也尝试hook hashmap

1
2
3
4
5
6
7
8
9
10
11
12
function hook_hashmap(){
Java.perform(function(){
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function(a, b) {
if(a!=null && a.equals("sign")){
console.log("hashMap.put ==> ", a, b);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
return this.put(a, b);
}
})
}

使用attach会发现失败,因为该样本存在双进程保护,会占用ptrace,attach 需要使用ptrace实现注入,ptrace被占用无法使用,就会导致attach注入失败。

1
2
3
4
└─# adb shell
blueline:/ $ ps -A | grep lucky
u0_a225 28594 1075 1856704 360324 0 0 S com.lucky.luckyclient
u0_a225 28645 28594 1463932 126400 0 0 S com.lucky.luckyclient

查看进程后确实是双进程,那就改用Frida Spawn方式注入

1
frida -U -f 包名 -l js文件 --no-pause -o log.txt

image通过对应的包对比找到了请求参数的位置 com.lucky.lib.http2.AbstractLcRequest.getRequestParams
image果然跟想的一样字符串加密,主动调用一下看看加密的字符串是什么,顺便看到哪个是结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function call_getRequestParams(){
Java.perform(function(){
var StubApp = Java.use('com.stub.StubApp');
var result1 = StubApp['getString2']('14154');
var result2 = StubApp['getString2']('457');
var result3 = StubApp['getString2']('4005');
var result4 = StubApp['getString2']('16944');
var result5 = StubApp['getString2']('7719');
console.log("StubApp.getString2(14154) ==> ", result1)
console.log("StubApp.getString2(457) ==> ", result2)
console.log("StubApp.getString2(4005) ==> ", result3)
console.log("StubApp.getString2(16944) ==> ", result4)
console.log("StubApp.getString2(7719) ==> ", result5)
})
}
1
2
3
4
5
StubApp.getString2(14154) ==>  appversion
StubApp.getString2(457) ==> q
StubApp.getString2(4005) ==> cid
StubApp.getString2(16944) ==> uid
StubApp.getString2(7719) ==> sign

多次抓包只有q和sign的值是会变的,sign是由cid,uid,q的值加密来的
一路跟进到native方法,其中q是由localAESWork4Api加密生成后转base64,sign是通过参数的拼接成字符串最后调用md5_crypt加密生成,
image其中最上面加载了lib库,System.loadLibrary(StubApp.getString2(“30491”));
用上面的主动调用解密后是 cryptoDD
解压apk找到这个so用ida打开
在Exports搜java,啥都没搜到,那就只有动态注册了,hook libart找动态注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function find_RegisterNatives() {
let symbols = Module.enumerateSymbolsSync("libart.so");
let addrRegisterNatives = null;
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];

//_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
hook_RegisterNatives(addrRegisterNatives)
}
}

}

function hook_RegisterNatives(addrRegisterNatives) {

if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
// console.log("[RegisterNatives] method_count:", args[3]);
let java_class = args[1];
let class_name = Java.vm.tryGetEnv().getClassName(java_class);
//console.log(class_name);

let methods_ptr = ptr(args[2]);

let method_count = parseInt(args[3]);
for (let i = 0; i < method_count; i++) {
let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

let name = Memory.readCString(name_ptr);
let sig = Memory.readCString(sig_ptr);
var find_module = Process.findModuleByAddress(name_ptr);
if(find_module != null && find_module.name == "libcryptoDD.so") {
console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "offset:", ptr(fnPtr_ptr).sub(find_module.base));
}
}
}
});
}
}

setImmediate(find_RegisterNatives)
1
2
3
4
[RegisterNatives] java_class: com.luckincoffee.safeboxlib.CryptoHelper name: localAESWork sig: ([BI[B)[B fnPtr: 0xbe7f084d offset: 0x1984d
[RegisterNatives] java_class: com.luckincoffee.safeboxlib.CryptoHelper name: localConnectWork sig: ([B[B)[B fnPtr: 0xbe7f078d offset: 0x1978d
[RegisterNatives] java_class: com.luckincoffee.safeboxlib.CryptoHelper name: md5_crypt sig: ([BI)[B fnPtr: 0xbe7f1981 offset: 0x1a981
[RegisterNatives] java_class: com.luckincoffee.safeboxlib.CryptoHelper name: localAESWork4Api sig: ([BI)[B fnPtr: 0xbe7f21cd offset: 0x1b1cd

只要关注md5_crypt和localAESWork4Api,偏移量分别为0x1a981,0x1b1cd
ida跳转到0x1b1cd位置函数名是android_native_wbaes,0x1a981函数名是android_native_md5
image大概浏览跟进android_native_wbaes查看有PKCS5Padding,wbaes_decrypt_ecb函数
从这些函数名可以猜测 wbaes_encrypt_ecb 是调用白盒aes的ecb模式加密
image

算法分析

尝试对 wbaes_encrypt_ecb 进行hook参看参数,从参数名可以得知 in是明文,in_len是长度,out是输出结果,mode是模式
既然写了是ecb,那可以主动调用测试下是不是真的是ecb模式,我们知道aes是16字节一组,那可以设置明文两组一样的,然后看加密结果
以明文 zskkk.cnzskkk.cn 为例,转为字节是 7a 73 6b 6b 6b 2e 63 6e 7a 73 6b 6b 6b 2e 63 6e

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}

function call_wbaes_encrypt_ecb(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
var real_addr = base_addr.add(0x17bd5);
var wbaes_encrypt_ecb_func = new NativeFunction(real_addr, "int", ["pointer", "int", "pointer", "int"]);
var inputPtr = Memory.alloc(0x20);
var inputArray = hexToBytes("7a736b6b6b2e636e7a736b6b6b2e636e7a736b6b6b2e636e7a736b6b6b2e636e");
Memory.writeByteArray(inputPtr, inputArray)
var outputPtr = Memory.alloc(0x20);
wbaes_encrypt_ecb_func(inputPtr, 0x20, outputPtr, 0);
console.log(hexdump(outputPtr,{length: 0x20}));
}
1
2
3
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
f24005f8 4e 64 b5 10 8d a6 82 b5 13 f3 6e 69 aa a4 63 56 Nd........ni..cV
f2400608 4e 64 b5 10 8d a6 82 b5 13 f3 6e 69 aa a4 63 56 Nd........ni..cV

可以发现,两个分组加密结果一致,看来确实是ECB。
现在对其进行hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_wbaes_encrypt_ecb(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
// wbaes_encrypt_ecb 的偏移地址0x17bd4,因为thumb所以+1
var real_addr = base_addr.add(0x17bd5)
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("Input");
this.buffer = args[2];
this.length = args[1].toInt32();
console.log(hexdump(args[0],{length: this.length}));
console.log("mode:"+args[3]);
},
onLeave: function (retval) {
console.log("Output")
console.log(hexdump(this.buffer,{length: this.length}));
}
});
}

也是使用spawn的方式启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Pixel 3::com.lucky.luckyclient ]-> Input
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
f6b9ff10 7b 22 70 61 67 65 54 79 70 65 22 3a 22 31 22 2c {"pageType":"1",
f6b9ff20 22 74 61 67 49 6e 64 65 78 22 3a 22 42 51 30 32 "tagIndex":"BQ02
f6b9ff30 30 22 2c 22 61 70 70 76 65 72 73 69 6f 6e 22 3a 0","appversion":
f6b9ff40 22 35 30 30 31 22 2c 22 70 61 67 65 22 3a 32 2c "5001","page":2,
f6b9ff50 22 72 6f 77 73 22 3a 31 36 2c 22 62 72 61 6e 64 "rows":16,"brand
f6b9ff60 43 6f 64 65 22 3a 22 4c 4b 30 30 31 22 7d 02 02 Code":"LK001"}..
mode:0x0
Output
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
f6b89cb0 f0 43 ce dd 27 e5 47 0a c5 8b fa 67 16 20 0d 20 .C..'.G....g. .
f6b89cc0 75 fa d6 ad a4 48 b0 fb 68 5f 81 60 86 53 68 03 u....H..h_.`.Sh.
f6b89cd0 c3 0e 21 28 46 ae d2 dc e4 0c 3e 36 4f 10 32 ee ..!(F.....>6O.2.
f6b89ce0 cc b7 57 6c 3c f7 85 c5 86 a4 b4 a6 7f da 47 67 ..Wl<.........Gg
f6b89cf0 ef bc 3a 8e 13 e0 30 1a 56 86 76 de 16 26 63 cc ..:...0.V.v..&c.
f6b89d00 a7 93 16 80 8a 23 6d 63 8b 9d 98 55 26 26 cc fb .....#mc...U&&..
[Pixel 3::com.lucky.luckyclient ]->

观察输入可以发现,待加密的内容经过了PCKS7填充,末尾缺了2字节用了0x20填充。
虽然存在一些混淆,但是符号没去掉给了很大的帮助,从ida的代码中也能看出,wbaes_encrypt_ecb只是外层的简单包装,后面还会调用 aes128_enc_wb_coff 和 aes128_enc_wb_xlc 。

1
2
void __fastcall aes128_enc_wb_xlc(uint8_t *in, uint8_t *out)
void __fastcall aes128_enc_wb_coff(uint8_t *in, uint8_t *out)

接下来只关注于单个分组,最后的call代码如下,完善Hook脚本,添加对这两个函数的Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}

function hook_aes128_enc_wb_xlc(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
// aes128_enc_wb_xlc 的偏移地址0x15c8c,因为thumb所以+1
var real_addr = base_addr.add(0x15c8d);
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("aes128_enc_wb_xlc");
console.log(hexdump(args[0],{length: 0x10}))
}
});
}

function hook_aes128_enc_wb_coff(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
// aes128_enc_wb_coff 的偏移地址0x15320,因为thumb所以+1
var real_addr = base_addr.add(0x15321);
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("aes128_enc_wb_coff");
console.log(hexdump(args[0],{length: 0x10}))
}
});
}

function call_wbaes_encrypt_ecb(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
var real_addr = base_addr.add(0x17bd5);
var wbaes_encrypt_ecb_func = new NativeFunction(real_addr, "int", ["pointer", "int", "pointer", "int"]);
var inputPtr = Memory.alloc(0x10);
var inputArray = hexToBytes("7a736b6b6b2e636e7a736b6b6b2e636e");
Memory.writeByteArray(inputPtr, inputArray)
var outputPtr = Memory.alloc(0x10);
wbaes_encrypt_ecb_func(inputPtr, 0x10, outputPtr, 0);
console.log(hexdump(outputPtr,{length: 0x10}));
}

执行结果

1
2
3
4
5
aes128_enc_wb_coff
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
df194260 7a 73 6b 6b 6b 2e 63 6e 7a 73 6b 6b 6b 2e 63 6e zskkk.cnzskkk.cn
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
f1ad5988 4e 64 b5 10 8d a6 82 b5 13 f3 6e 69 aa a4 63 56 Nd........ni..cV

可以确认,逻辑最终走到了aes128_enc_wb_coff,下面分析aes128_enc_wb_coff。观察可以看到三个表

1
2
3
const uint8_t Tboxes_[16][256];
const uint8_t Txor[16][16];
const uint32_t Tyboxes[9][16][256];

image
image都是不小的表,且程序逻辑中存在大量查表运算,确实是白盒加密。

DFA攻击获取密钥

使用Frida

DFA还原白盒AES密钥这一篇中说过,总结就是
三步骤

  • 找轮
  • 找时机,即具体第几轮做故障注入
  • 找state
    由于代码被混淆了,很难静态做出这些判断,只能大概分析,鉴于函数名没有混淆,很快找到 wbShiftRows 这个函数,从名字得知应该是做循环左移,在aes一次加密中,会进行十轮循环左移,对其进行hook验证下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function hook_wbShiftRows(){
    var count = 0;
    var base_addr = Module.findBaseAddress("libcryptoDD.so");
    var real_addr = base_addr.add(0x14f99);
    Interceptor.attach(real_addr, {
    onEnter: function (args) {
    count += 1;
    console.log("wbShiftRows:"+count);
    }
    });
    }
    运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Pixel 3::com.lucky.luckyclient ]-> aes128_enc_wb_coff
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    df194260 7a 73 6b 6b 6b 2e 63 6e 7a 73 6b 6b 6b 2e 63 6e zskkk.cnzskkk.cn
    wbShiftRows:1
    wbShiftRows:2
    wbShiftRows:3
    wbShiftRows:4
    wbShiftRows:5
    wbShiftRows:6
    wbShiftRows:7
    wbShiftRows:8
    wbShiftRows:9
    wbShiftRows:10
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    f2c7d930 4e 64 b5 10 8d a6 82 b5 13 f3 6e 69 aa a4 63 56 Nd........ni..cV
    确实是十轮,设置故障点最好的时机就是在第九轮,而循环左移后就是列混淆了,
    1
    void __fastcall wbShiftRows(uint8_t *out)
    发现,函数就一个参数,out即作输入又当输出,那么可以对这个函数参数进行修改,就可以故障注入,可以将第一个字节改成0。运行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    aes128_enc_wb_coff
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    df194260 7a 73 6b 6b 6b 2e 63 6e 7a 73 6b 6b 6b 2e 63 6e zskkk.cnzskkk.cn
    wbShiftRows:1
    wbShiftRows:2
    wbShiftRows:3
    wbShiftRows:4
    wbShiftRows:5
    wbShiftRows:6
    wbShiftRows:7
    wbShiftRows:8
    wbShiftRows:9
    wbShiftRows:10
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    f2ad6450 de 64 b5 10 8d a6 82 12 13 f3 9c 69 aa 5f 63 56 .d.........i._cV
    将正确明文和故障密文做对比
    正确密文:4e 64 b5 10 8d a6 82 b5 13 f3 6e 69 aa a4 63 56
    故障密文:de 64 b5 10 8d a6 82 12 13 f3 9c 69 aa 5f 63 56
    注入成功了,接下来只需要收集多点故障密文加上正常密文,喂给phoenixAES就可以得到第十轮密钥了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    function bufferToHex(buffer) {
    return [...new Uint8Array (buffer)]
    .map (b => b.toString (16).padStart (2, "0"))
    .join ("");
    }

    function hook_wbShiftRows(){
    var count = 0;
    var base_addr = Module.findBaseAddress("libcryptoDD.so");
    // wbShiftRows 的偏移地址0x14f90,因为thumb所以+1
    var real_addr = base_addr.add(0x14f99);
    Interceptor.attach(real_addr, {
    onEnter: function (args) {
    count += 1;
    if (count % 9 == 0){
    args[0].add(Math.floor(Math.random() * 16)).writeS8(0x0);
    }
    }
    });
    }

    function call_wbaes_encrypt_ecb(){
    var base_addr = Module.findBaseAddress("libcryptoDD.so");
    var real_addr = base_addr.add(0x17bd5);
    var wbaes_encrypt_ecb_func = new NativeFunction(real_addr, "int", ["pointer", "int", "pointer", "int"]);
    var inputPtr = Memory.alloc(0x10);
    var inputArray = hexToBytes("7a736b6b6b2e636e7a736b6b6b2e636e");
    Memory.writeByteArray(inputPtr, inputArray)
    var outputPtr = Memory.alloc(0x10);
    wbaes_encrypt_ecb_func(inputPtr, 0x10, outputPtr, 0);
    // console.log(hexdump(outputPtr,{length: 0x10}));
    var output = Memory.readByteArray(outputPtr, 0x10);
    console.log(bufferToHex(output))
    }

    function dfa(){
    hook_wbShiftRows();
    for(var i = 1; i<200; i++) {
    call_wbaes_encrypt_ecb();
    }
    }
    这里打印成hexstr而不是hexdump,然后在hook_wbShiftRows中计数器为九的倍数时就注入故障,因为计数器的数字会随着call_wbaes_encrypt_ecb不断增加,注入故障的位置是随机在0-15字节位置修改为0x0,随机字节也可以。
    然后将正确密文以及输出的200个故障密文放入phoenixAES,结果为
    1
    2
    Last round key #N found:
    869D92BBB700D0D25BD9FD3E224B5DF2
    放入 stark
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ./aes_keyschedule 869D92BBB700D0D25BD9FD3E224B5DF2 10
    K00: 644A4C64434A69566E44764D394A5570
    K01: B3B61D76F0FC74209EB8026DA7F2571D
    K02: 38EDB92AC811CD0A56A9CF67F15B987A
    K03: 05AB638BCDBAAE819B1361E66A48F99C
    K04: 5F32BD8992881308099B72EE63D38B72
    K05: 290FFD72BB87EE7AB21C9C94D1CF17E6
    K06: 83FF734C38789D368A6401A25BAB1644
    K07: A1B8687599C0F54313A4F4E1480FE2A5
    K08: 57206E27CEE09B64DD446F85954B8D20
    K09: FF7DD90D319D4269ECD92DEC7992A0CC
    K10: 869D92BBB700D0D25BD9FD3E224B5DF2
    得出密钥:644A4C64434A69566E44764D394A5570
    在cyber中用一开始抓包的密文做测试解密看看
    image成功解密
    再次抓包hook localAESWork4Api的返回值与aes ecb加密后的结果是否一致,看下是否还有做其它操作
    新增hook函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function hook_localAESWork4Api(){
    Java.perform(function (){
    let CryptoHelper = Java.use("com.luckincoffee.safeboxlib.CryptoHelper");
    CryptoHelper["localAESWork4Api"].implementation = function (bArr, i) {
    let ret = this.localAESWork4Api(bArr, i);
    console.log('localAESWork4Api value: ' + bufferToHex(ret));
    return ret;
    };
    })
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [Pixel 3::com.lucky.luckyclient ]-> Input
    0x9fd42f50
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    9fd42f50 7b 22 70 61 67 65 54 79 70 65 22 3a 22 31 22 2c {"pageType":"1",
    9fd42f60 22 74 61 67 49 6e 64 65 78 22 3a 22 42 51 30 32 "tagIndex":"BQ02
    9fd42f70 30 22 2c 22 61 70 70 76 65 72 73 69 6f 6e 22 3a 0","appversion":
    9fd42f80 22 35 30 30 31 22 2c 22 70 61 67 65 22 3a 35 2c "5001","page":5,
    9fd42f90 22 72 6f 77 73 22 3a 31 36 2c 22 62 72 61 6e 64 "rows":16,"brand
    9fd42fa0 43 6f 64 65 22 3a 22 4c 4b 30 30 31 22 7d 02 02 Code":"LK001"}..
    mode:0x0
    Output
    0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
    e7a50000 f0 43 ce dd 27 e5 47 0a c5 8b fa 67 16 20 0d 20 .C..'.G....g. .
    e7a50010 75 fa d6 ad a4 48 b0 fb 68 5f 81 60 86 53 68 03 u....H..h_.`.Sh.
    e7a50020 c3 0e 21 28 46 ae d2 dc e4 0c 3e 36 4f 10 32 ee ..!(F.....>6O.2.
    e7a50030 a0 6f a2 2f c6 f5 73 59 b3 5d bb 5b f3 ec 41 bf .o./..sY.].[..A.
    e7a50040 ef bc 3a 8e 13 e0 30 1a 56 86 76 de 16 26 63 cc ..:...0.V.v..&c.
    e7a50050 a7 93 16 80 8a 23 6d 63 8b 9d 98 55 26 26 cc fb .....#mc...U&&..
    localAESWork4Api value: f043cedd27e5470ac58bfa6716200d2075fad6ada448b0fb685f816086536803c30e212846aed2dce40c3e364f1032eea06fa22fc6f57359b35dbb5bf3ec41bfefbc3a8e13e0301a568676de162663cca79316808a236d638b9d98552626ccfb
    结果是一致的,回到localAESWork4Api看密文出来后接下来的处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public synchronized String m29712c(String str) {
    byte[] localAESWork4Api;
    if (this.f235976e != null) {
    String mo24058a = this.f235976e.mo24058a();
    if (!mo24058a.endsWith(StubApp.getString2("30492"))) {
    localAESWork4Api = localAESWork(str.getBytes(), 2, Base64.decode(mo24058a.replace('-', '+').replace('_', '/').getBytes(), 2));
    } else {
    localAESWork4Api = localAESWork4Api(str.getBytes(), mo24058a.startsWith(StubApp.getString2("11453")) ? 0 : 2);
    }
    } else {
    throw new RuntimeException(StubApp.getString2("30494"));
    }
    return new String(Base64.encode(localAESWork4Api, 2)).replace('+', '-').replace('/', '_');
    }

使用Unidbg

先搭架子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.LuckyCoffee;

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.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;

public class LuckyAES extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;

public LuckyAES(){
emulator = AndroidEmulatorBuilder.for32Bit().build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分

// 模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/LuckyCoffee/ruixing5.0.01.apk"));
vm.setVerbose(true);
// 加载基本库
new AndroidModule(emulator, vm).register(memory);
// 加载so到虚拟内存
DalvikModule dm = vm.loadLibrary("cryptoDD", true);
module = dm.getModule();
// 设置JNI
vm.setJni(this);
dm.callJNI_OnLoad(emulator);
}

public static void main(String[] args) {
LuckyAES luckyAES = new LuckyAES();
}
}

运行完没问题后就开始调用函数wbaes_encrypt_ecb,通过分析知道了该函数偏移0x17bd4+1,
参数明文跟上面frida一样”zskkk.cnzskkk.cn”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static byte[] hexStringToBytes(String hexString) {
return DatatypeConverter.parseHexBinary(hexString);
//if(hexString.isEmpty()){
// return null;
//}
//hexString = hexString.toLowerCase();
//byte[] byteArray = new byte[hexString.length() >> 1];
//int index = 0;
//for(int i = 0; i < hexString.length(); i++) {
// if(index > hexString.length() - 1){
// return byteArray;
// }
// byte highDit = (byte) (Character.digit(hexString.charAt(index), 16) & 0xFF);
// byte lowDit = (byte) (Character.digit(hexString.charAt(index + 1), 16) & 0xFF);
// byteArray[i] = (byte) (highDit << 4 | lowDit);
// index += 2;
//}
//return byteArray;
}

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 void call_wbaes_encrypt_ecb(){
MemoryBlock inBlock = emulator.getMemory().malloc(16, true);
UnidbgPointer inPtr = inBlock.getPointer();
MemoryBlock outBlock = emulator.getMemory().malloc(16, true);
UnidbgPointer outPtr = outBlock.getPointer();

byte[] stub = hexStringToBytes("7a736b6b6b2e636e7a736b6b6b2e636e");
assert stub != null;
inPtr.write(0, stub, 0, stub.length);
module.callFunction(emulator, 0x17bd5, inPtr, 16, outPtr, 0);
String ret = bytesTohexString(outPtr.getByteArray(0, 0x10));
System.out.println("white box result:"+ret);
inBlock.free();
outBlock.free();
}

运行后结果也是一致

1
2
3
4
5
6
7
JNIEnv->FindClass(com/luckincoffee/safeboxlib/CryptoHelper) was called from RX@0x1201b43b[libcryptoDD.so]0x1b43b
JNIEnv->RegisterNatives(com/luckincoffee/safeboxlib/CryptoHelper, RW@0x120dfd0c[libcryptoDD.so]0xdfd0c, 4) was called from RX@0x1201b3e3[libcryptoDD.so]0x1b3e3
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localAESWork([BI[B)[B, RX@0x1201984d[libcryptoDD.so]0x1984d)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localConnectWork([B[B)[B, RX@0x1201978d[libcryptoDD.so]0x1978d)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, md5_crypt([BI)[B, RX@0x1201a981[libcryptoDD.so]0x1a981)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localAESWork4Api([BI)[B, RX@0x1201b1cd[libcryptoDD.so]0x1b1cd)
white box result:4e64b5108da682b513f36e69aaa46356

接下来跟龙哥学了一招,作图
因为白盒加密的主要实现方式是查表法,所以加密主体就是大量的内存访问。那么记录函数对内存的访问以及发起访问的地址(PC指针),绘制成折线图,就可以较好的反映加密流程。
使用Unidbg的ReadHook
规则如下:监控整个SO地址范围内的内存读取操作,记录PC发起的地址,减去SO基地址,只打印偏移,这样呈现效果更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void traceAESRead() {
ReadHook readHook = new ReadHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
int now = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_PC).intValue();
if((now > module.base) & (now < (module.base + module.size))){
System.out.println(now - module.base);
}
}

@Override
public void onAttach(UnHook unHook) {

}

@Override
public void detach() {

}
};
emulator.getBackend().hook_add_new(readHook, module.base, module.base+module.size, null);
}

traceAESRead 函数放于如下位置

1
2
3
4
5
public static void main(String[] args) {
LuckyAES luckyAES = new LuckyAES();
luckyAES.traceAESRead();
luckyAES.call_wbaes_encrypt_ecb();
}

结果如下
image将这几千条记录拷贝出来,保存在trace.txt 中,在Python中做可视化。需要安装matplotlib以及numpy库。

1
2
3
4
5
6
7
import matplotlib.pyplot as plt
import numpy

input = numpy.loadtxt("trace.txt", int)

plt.plot(range(len(input)), input)
plt.show()

运行后生成折线图,将其放大是如下效果
imageX轴的计数单位是次数,表示当前是第几次内存访问,如图,在程序的运行过程中,发生了1400余次对SO内存的读操作。Y轴是发起访问的偏移地址。需要注意,X与Y轴的数值表示为十进制。图上可得,Y主要在80000-100000之间,我们修改Y轴范围,增强呈现效果。

1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt
import numpy

input = numpy.loadtxt("trace.txt", int)

plt.plot(range(len(input)), input)
# 限制Y
plt.ylim(80000,100000)
plt.plot(range(len(input)), input)
plt.show()

image还可以缩小到85000-90000之间,再次缩小Y的范围,
image可已看到重复了10次的相同模式,代表了十轮运算,这一点可用于区分AES-128/192/256,分别对应10/12/14 轮。
除此之外,我们发现每轮运算的起点是一个较低的地址,具体在86000附近左右,转成十六进制就是0x14FF0附近。
在IDA中查看,是处于wbShiftRows函数中,
image接下来就再次来到故障攻击的部分,很接近Frida Hook的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static int randInt(int min, int max) {
Random rand = new Random();
return rand.nextInt((max - min) + 1) + min;
}

public void dfaAttack() {
emulator.attach().addBreakPoint(module.base + 0x14f98 + 1, new BreakPointCallback() {
int count = 0;
UnidbgPointer pointer;

@Override
public boolean onHit(Emulator<?> emulator, long address) {
count += 1;
RegisterContext registerContext = emulator.getContext();
pointer = registerContext.getPointerArg(0);
emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (count % 9 == 0) {
pointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
}
return true;
}
});
return true;
}
});
}

public static void main(String[] args) {
LuckyAES luckyAES = new LuckyAES();
luckyAES.dfaAttack();
for (int i = 0; i < 200; i++) {
luckyAES.call_wbaes_encrypt_ecb();
}
}

运行出来的故障密文同样喂给phoenixAES,也是得到相同第十轮密钥,放入stark得到初始密钥

sign参数分析

上次说到md5_crypt是sign的加密函数,在so中对应的是android_native_md5偏移量0x1a981
也是在该函数中很快的发现md5,doMD5sign可疑函数

1
2
void __fastcall md5(const uint8_t *initial_msg, size_t initial_len, uint8_t *digest)
uint32_t __fastcall doMD5sign(const uint8_t *initial_msg, size_t initial_len, int8_t **digest)

查看doMD5sign的代码后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
uint32_t __fastcall doMD5sign(const uint8_t *initial_msg, size_t initial_len, int8_t **digest)
{
signed int v4; // r0
int v5; // r2
signed int v6; // r0
int v7; // r2
signed int v8; // r2
signed int v9; // r0
int v10; // r2
size_t v11; // r4
int8_t *v12; // r0
uint8_t v14[16]; // [sp+0h] [bp-A8h] BYREF
char s[64]; // [sp+10h] [bp-98h] BYREF
char v16[20]; // [sp+50h] [bp-58h] BYREF
char v17[20]; // [sp+64h] [bp-44h] BYREF
char v18[20]; // [sp+78h] [bp-30h] BYREF

md5(initial_msg, initial_len, v14);
v4 = bytesToInt(v14, 0);
v5 = v4;
if ( v4 < 0 )
v5 = -v4;
sprintf(s, &byte_E0021, v5);
v6 = bytesToInt(v14, 4u);
v7 = v6;
if ( v6 < 0 )
v7 = -v6;
sprintf(v18, &byte_E0021, v7);
v8 = bytesToInt(v14, 8u);
if ( v8 <= -1 )
v8 = -v8;
sprintf(v17, &byte_E0021, v8);
v9 = bytesToInt(v14, 0xCu);
v10 = v9;
if ( v9 < 0 )
v10 = -v9;
sprintf(v16, &byte_E0021, v10);
strcat(s, v18);
strcat(s, v17);
strcat(s, v16);
v11 = strlen(s);
v12 = (int8_t *)malloc(v11);
*digest = v12;
qmemcpy(v12, s, v11);
return v11;
}

调用的了4次bytesToInt,最后经过strcat拼接成字符串,抓包的sign也都是由数字拼接,那会不会跟这个函数有关?hook看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function hook_doMD5sign(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
var real_addr = base_addr.add(0x14d55)
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("doMD5sign Input");
console.log(args[0])
this.length = args[1].toInt32();
this.buffer = args[2];
console.log(hexdump(args[0],{length: this.length}));
},
onLeave: function (retval) {
console.log("doMD5sign Output")
// console.log(retval.toInt32());
console.log(hexdump(this.buffer,{length: retval.toInt32()}));
}
});
}

function hook_md5(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
var real_addr = base_addr.add(0x13e3d)
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("md5 Input");
console.log(args[0])
this.length = args[1].toInt32();
this.buffer = args[2];
console.log(hexdump(args[0],{length: this.length}));
},
onLeave: function (retval) {
console.log("md5 Output")
console.log(hexdump(this.buffer,{length: 32}));
}
});
}

function hook_bytesToInt(){
var base_addr = Module.findBaseAddress("libcryptoDD.so");
var real_addr = base_addr.add(0x13925)
Interceptor.attach(real_addr, {
onEnter: function (args) {
console.log("bytesToInt Input");
console.log(args[0])
this.offset = args[1].toInt32();
console.log(hexdump(args[0]));
},
onLeave: function (retval) {
console.log("bytesToInt Output")
console.log(retval.toInt32());
}
});
}

执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[Pixel 3::com.lucky.luckyclient ]-> doMD5sign Input
0xe239f5a0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e239f5a0 63 69 64 3d 32 31 30 31 30 31 3b 71 3d 38 45 50 cid=210101;q=8EP
e239f5b0 4f 33 53 66 6c 52 77 72 46 69 5f 70 6e 46 69 41 O3SflRwrFi_pnFiA
e239f5c0 4e 49 4f 5f 68 46 5f 7a 74 57 75 5f 33 49 77 77 NIO_hF_ztWu_3Iww
e239f5d0 4c 48 71 4b 71 73 49 66 43 5f 70 65 58 73 6b 70 LHqKqsIfC_peXskp
e239f5e0 53 38 76 6e 44 54 5a 6e 72 4b 37 43 62 4c 73 4f S8vnDTZnrK7CbLsO
e239f5f0 59 54 51 59 37 79 33 45 65 31 6f 6d 49 75 4c 48 YTQY7y3Ee1omIuLH
e239f600 56 45 2d 2d 38 4f 6f 34 54 34 44 41 61 56 6f 5a VE--8Oo4T4DAaVoZ
e239f610 32 33 68 59 6d 59 38 79 6e 6b 78 61 41 69 69 4e 23hYmY8ynkxaAiiN
e239f620 74 59 34 75 64 6d 46 55 6d 4a 73 7a 37 3b 75 69 tY4udmFUmJsz7;ui
e239f630 64 3d 65 32 65 63 33 38 37 31 2d 37 33 63 35 2d d=e2ec3871-73c5-
e239f640 34 38 32 33 2d 62 36 63 35 2d 32 31 33 33 66 38 4823-b6c5-2133f8
e239f650 30 63 32 36 37 66 31 37 32 35 38 30 30 37 34 31 0c267f1725800741
e239f660 31 32 36 64 4a 4c 64 43 4a 69 56 6e 44 76 4d 39 126dJLdCJiVnDvM9
e239f670 4a 55 70 73 6f 6d 39 JUpsom9
md5 Input
0xe239f5a0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e239f5a0 63 69 64 3d 32 31 30 31 30 31 3b 71 3d 38 45 50 cid=210101;q=8EP
e239f5b0 4f 33 53 66 6c 52 77 72 46 69 5f 70 6e 46 69 41 O3SflRwrFi_pnFiA
e239f5c0 4e 49 4f 5f 68 46 5f 7a 74 57 75 5f 33 49 77 77 NIO_hF_ztWu_3Iww
e239f5d0 4c 48 71 4b 71 73 49 66 43 5f 70 65 58 73 6b 70 LHqKqsIfC_peXskp
e239f5e0 53 38 76 6e 44 54 5a 6e 72 4b 37 43 62 4c 73 4f S8vnDTZnrK7CbLsO
e239f5f0 59 54 51 59 37 79 33 45 65 31 6f 6d 49 75 4c 48 YTQY7y3Ee1omIuLH
e239f600 56 45 2d 2d 38 4f 6f 34 54 34 44 41 61 56 6f 5a VE--8Oo4T4DAaVoZ
e239f610 32 33 68 59 6d 59 38 79 6e 6b 78 61 41 69 69 4e 23hYmY8ynkxaAiiN
e239f620 74 59 34 75 64 6d 46 55 6d 4a 73 7a 37 3b 75 69 tY4udmFUmJsz7;ui
e239f630 64 3d 65 32 65 63 33 38 37 31 2d 37 33 63 35 2d d=e2ec3871-73c5-
e239f640 34 38 32 33 2d 62 36 63 35 2d 32 31 33 33 66 38 4823-b6c5-2133f8
e239f650 30 63 32 36 37 66 31 37 32 35 38 30 30 37 34 31 0c267f1725800741
e239f660 31 32 36 64 4a 4c 64 43 4a 69 56 6e 44 76 4d 39 126dJLdCJiVnDvM9
e239f670 4a 55 70 73 6f 6d 39 JUpsom9
md5 Output
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
ffb70848 46 6b 91 12 1c 7c 30 66 92 da 99 8e 63 15 70 6c Fk...|0f....c.pl
bytesToInt Input offset: 0
bytesToInt Output: 1181454610

bytesToInt Input offset: 4
bytesToInt Output: 477900902

bytesToInt Input offset: 8
bytesToInt Output: -1831167602

bytesToInt Input offset: 12
bytesToInt Output: 1662349420

doMD5sign Output
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
ffb70964 90 78 ba af a1 70 40 18 00 00 00 00 10 00 dc de .x...p@.........
ffb70974 a0 09 b7 ff 10 00 dc de 89 6a ad eb 34 0a b7 ff .........j..4...
ffb70984 24 0a b7 ff 03 00 00 $......

多次抓包发现除了由刚才说的几个另外的参数拼接,还加了uid+盐值 dJLdCJiVnDvM9JUpsom9
验证下是不是标准的md5
image是标准的md5,没有经过魔改
再看看抓包的sign和hook对比发现bytesToInt的4个返回值的绝对值拼接起来就是sign
1181454610 477900902 1831167602 1662349420
查看bytesToInt(const uint8_t *src, uint32_t offset)的代码只要的就是

1
v9 = (src[offset + 1] << 16) | (src[offset] << 24) | (src[offset + 2] << 8) | src[offset + 3];

现在就很清晰了,sign由
md5(cid,q,uid,dJLdCJiVnDvM9JUpsom9拼接)的第1,4,10,15字节偏移而来
response也是aes ebc加密的,先replace("_","/").replace("-","+"),base64解码,再用上面的密钥解密就可以了
image

 评论