Featured image of post 某咖啡app逆向分析

某咖啡app逆向分析

样本:找的一个低版本的 v5.0.01 首先,总所周知,这个 app 是加壳的,360 加固,整体型加固,直接用 frida 就可以将壳给脱下来
脚本的话可以用 yang 神写的:https://github.com/lasting-yang/frida_dump

1
frida -U --no-pause -f packageName -l .\dump_dex.js

抓包

首先抓一下首页的这个包

发现在请求参数中有 sign,q 这个加密参数,响应这个包也是加密的

sign

首先来分析以下 sign 这个参数,首先来搜索,发现并没啥用,搜不到一点

那就换一种方式,hook 一下 hashMap,经常会有一些 app 会使用这个数据结构来存储数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

function hook_map() {
    console.log("hook hashMap")
    var hashMap = Java.use("java.util.HashMap");
    hashMap.put.implementation = function (a, b) {
        if (a!=null&&a.equals("sign")) {
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            console.log('输出-->', a, b)

        }
        return this.put(a, b)
    }
}
Java.perform(function(){
    hook_map()
})

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
java.lang.Throwable
        at java.util.HashMap.put(Native Method)
        at com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:14)
        at com.lucky.lib.http2.k.a(SourceFile:3)
        at com.lucky.lib.http2.k.b(SourceFile:2)
        at com.lucky.lib.http2.k.getRequest(SourceFile:1)
        at com.lucky.lib.http2.AbstractLcRequest.async(SourceFile:5)
        at com.lucky.lib.http2.AbstractLcRequest.enqueue(SourceFile:3)
        at com.lucky.lib.http2.k.enqueue(SourceFile:1)
        at f.i.b.d.b.b.a(SourceFile:4)
        at io.reactivex.internal.operators.observable.C.e(SourceFile:3)
        at io.reactivex.A.a(SourceFile:450)
        at io.reactivex.internal.operators.observable.X.e(SourceFile:2)
        at io.reactivex.A.a(SourceFile:450)
        at io.reactivex.internal.operators.observable.wa.e(SourceFile:1)
        at io.reactivex.A.a(SourceFile:450)
        at io.reactivex.internal.operators.observable.Da.e(SourceFile:4)
        at io.reactivex.A.a(SourceFile:450)
        at io.reactivex.internal.operators.observable.Fa.e(SourceFile:1)
        at io.reactivex.A.a(SourceFile:450)
        at io.reactivex.internal.operators.observable.X.e(SourceFile:2)
        at io.reactivex.A.a(SourceFile:450)
        at com.luckin.client.home.e.U(SourceFile:9)
        at com.luckin.client.home.e.onEventMainThread(SourceFile:2)
        at java.lang.reflect.Method.invoke(Native Method)
        at org.greenrobot.eventbus.e.a(SourceFile:91)
        at org.greenrobot.eventbus.e.a(SourceFile:79)
        at org.greenrobot.eventbus.e.a(SourceFile:62)
        at org.greenrobot.eventbus.e.a(SourceFile:49)
        at org.greenrobot.eventbus.e.c(SourceFile:15)
        at com.luckin.location.b.onLocationChanged(SourceFile:10)
        at com.loc.Sa.a(Unknown Source:175)
        at com.loc.Sa.a(Unknown Source:0)
        at com.loc.Sa$c.handleMessage(Unknown Source:74)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

输出--> sign 147698339513130812842001300726905629272

所以,自然就应该去 getRequestParams 看看

它是通过 StubApp 这个类调用 getString2 这个方法来完成字符串解密的,所以,我也可以通过主动调用的方式,来查看 put 进去的是什么字符串

1
2
3
4
5
6
function hook_getString(num) {
  Java.perform(function () {
    var StubApp = Java.use("com.stub.StubApp");
    console.log("num:", num,'-->',StubApp.getString2(num));
  })
}

得到如下的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(14154)
num: 14154 --> appversion
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(457)
num: 457 --> q
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(4005)
num: 4005 --> cid
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(16944)
num: 16944 --> uid
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(7719)
num: 7719 --> sign
[Pixel 2::com.lucky.luckyclient ]->

添加上注释,这样好看一点,把 jadx 的反混淆开一下,这个又是 a,又是 b 的看着好混
sign 这个参数的生成还会用到 cid,uid,q 的值,这个后面再分析,现在主要看 sign 这个参数,一个一个来,不然容易混乱
进入 C10604r.m24314a 这个方法

发现好像还是在构造参数,应该还是上面的 cid,uid,q 这三个值,放到一个 map 的结构中,最后通过 C10584c.f247939b.m23838a 这个方法进行加密,继续跟进去

发现最后会调用 md5_crypt 这个方法,而这个函数是一个 native 层的函数

继续 hook 这个函数,看一下需要哪些参数,但是这儿的参数类型是字节数组的类型,需要进行转换一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function hook_md5_crypt(){
    Java.perform(function(){
        var ByteString = Java.use("com.android.okhttp.okio.ByteString");
        var Class = Java.use('com.luckincoffee.safeboxlib.CryptoHelper'); 
        Class['md5_crypt'].implementation = function(){
            console.log('sign参数加密位置::');
            var result = this['md5_crypt']['apply'](this,arguments);
            console.log('arg1:'+ByteString.of(arguments[0]).utf8());
            console.log('arg2:'+arguments[1]);
            console.log('result_sign:'+ByteString.of(result).utf8());   
            return result;
        }
    })
}

得到如下的结果:

1
2
3
arg1:cid=210101;q=3uda9DTWh1HzaMzrcZhTxcwe8AgwkxzHjqEegyMDUsEB79NUuaF-yz3Tr95ivqP96KFYlrb4g_m0H9p6GoIAdO7kWUXiBFQsl7SZEgHXBL0bzG5qAdTpfBhia0FXpCUZRxtjFsJ4IkBXUSYqY67IbCR1a-G8iola34uRO1D-R46WTsnmOy4LmWTW2CDNyo9v;uid=bd17e261-c511-4ca6-b8a1-5f3900b733b81771931072631
arg2:1
result_sign:79903440421321686324907558431145496727

经过多次测试,cid 和 uid 是不变的,变的是 q 这个参数,目前先不管这个参数,后面会分析 上面加载的 so 名字也被加密函数给加密了,hook 一下

1
2
[Pixel 2::com.lucky.luckyclient ]-> hook_getString(30491)
num: 30491 --> cryptoDD

但是不知道 md5_crypt 这个函数的地址是哪儿,hook 一下 registerNative 这个构造函数

1
2
3
4
5
来自:<libcryptoDD.so>
函数名字:md5_crypt
函数签名:([BI)[B
当前函数绝对地址:0xb80a9981
偏移地址:0x1a981

使用 unidbg 模拟调用一下,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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.ruixing;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.HookZzArm32RegisterContext;
import com.github.unidbg.hook.hookzz.WrapCallback;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;


public class CryptoDD extends AbstractJni implements IOResolver {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    //构造函数,配置相关信息
    CryptoDD() {
//        emulator = AndroidEmulatorBuilder
//                .for32Bit()
//                .setRootDir(new File("target/rootfs"))
//                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
//        emulator.getSyscallHandler().addIOResolver(this); // 绑定IO重定向接口
//        System.out.println("当前进程PID:" + emulator.getPid());
        emulator = AndroidEmulatorBuilder.for32Bit().build();
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("apks/ruixing/ruixing.apk"));
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        module = dm.getModule();
        vm.setJni(this);
        vm.setVerbose(true);
        dm.callJNI_OnLoad(emulator);

    }
    public void md5_crypt(){
        ArrayList<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
        String s = "cid=210101;q=3uda9DTWh1HzaMzrcZhTxcwe8AgwkxzHjqEegyMDUsEB79NUuaF-yz3Tr95ivqP96KFYlrb4g_m0H9p6GoIAdO7kWUXiBFQsl7SZEgHXBL0bzG5qAdTpfBhia0FXpCUZRxtjFsJ4IkBXUSYqY67IbCR1a-G8iola34uRO1D-R46WTsnmOy4LmWTW2CDNyo9v;uid=bd17e261-c511-4ca6-b8a1-5f3900b733b81771931072631";
        ByteArray byteArray = new ByteArray(vm,s.getBytes(StandardCharsets.UTF_8));
        list.add(vm.addLocalObject(byteArray));
        list.add(1);
        Number number = module.callFunction(emulator, 0x1a981, list.toArray());
        ByteArray result_arr = vm.getObject(number.intValue());
        String result = new String(result_arr.getValue());
        Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(),"md5_crypt");
        System.out.println("sign==>"+ result);
    }
    public static void main(String[] args) {
        CryptoDD cryptoDD = new CryptoDD();
        cryptoDD.md5_crypt();
    }
    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("pathname open:" + pathname);
        return null;
    }
}

还不用补环境,得到的值跟 hook 的值一样

直接跳转到 SetByteArrayRegion,地址:0x1abb7 这个地方

doMD5sign 这个函数就非常可疑了,而且可以分析到 v41 就是入参,他拼接了一个长度为 20 的字符串,可以来 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
mr0 0x117

>-----------------------------------------------------------------------------<
[11:35:21 093]r0=RW@0x12248000, md5=d05fb3dc7f164fb81d405703bbb91b69, hex=6369643d3231303130313b713d33756461394454576831487a614d7a72635a685478637765384167776b787a486a71456567794d445573454237394e557561462d797a33547239356976715039364b46596c726234675f6d3048397036476f4941644f376b57555869424651736c37535a45674858424c30627a4735714164547066426869613046587043555a5278746a46734a34496b4258555359715936374962435231612d4738696f6c61333475524f31442d5234365754736e6d4f79344c6d5754573243444e796f39763b7569643d62643137653236312d633531312d346361362d623861312d35663339303062373333623831373731393331303732363331644a4c64434a69566e44764d394a5570736f6d39
size: 279
0000: 63 69 64 3D 32 31 30 31 30 31 3B 71 3D 33 75 64    cid=210101;q=3ud
0010: 61 39 44 54 57 68 31 48 7A 61 4D 7A 72 63 5A 68    a9DTWh1HzaMzrcZh
0020: 54 78 63 77 65 38 41 67 77 6B 78 7A 48 6A 71 45    Txcwe8AgwkxzHjqE
0030: 65 67 79 4D 44 55 73 45 42 37 39 4E 55 75 61 46    egyMDUsEB79NUuaF
0040: 2D 79 7A 33 54 72 39 35 69 76 71 50 39 36 4B 46    -yz3Tr95ivqP96KF
0050: 59 6C 72 62 34 67 5F 6D 30 48 39 70 36 47 6F 49    Ylrb4g_m0H9p6GoI
0060: 41 64 4F 37 6B 57 55 58 69 42 46 51 73 6C 37 53    AdO7kWUXiBFQsl7S
0070: 5A 45 67 48 58 42 4C 30 62 7A 47 35 71 41 64 54    ZEgHXBL0bzG5qAdT
0080: 70 66 42 68 69 61 30 46 58 70 43 55 5A 52 78 74    pfBhia0FXpCUZRxt
0090: 6A 46 73 4A 34 49 6B 42 58 55 53 59 71 59 36 37    jFsJ4IkBXUSYqY67
00A0: 49 62 43 52 31 61 2D 47 38 69 6F 6C 61 33 34 75    IbCR1a-G8iola34u
00B0: 52 4F 31 44 2D 52 34 36 57 54 73 6E 6D 4F 79 34    RO1D-R46WTsnmOy4
00C0: 4C 6D 57 54 57 32 43 44 4E 79 6F 39 76 3B 75 69    LmWTW2CDNyo9v;ui
00D0: 64 3D 62 64 31 37 65 32 36 31 2D 63 35 31 31 2D    d=bd17e261-c511-
00E0: 34 63 61 36 2D 62 38 61 31 2D 35 66 33 39 30 30    4ca6-b8a1-5f3900
00F0: 62 37 33 33 62 38 31 37 37 31 39 33 31 30 37 32    b733b81771931072
0100: 36 33 31 64 4A 4C 64 43 4A 69 56 6E 44 76 4D 39    631dJLdCJiVnDvM9
0110: 4A 55 70 73 6F 6D 39                               JUpsom9
^-----------------------------------------------------------------------------^

他后面添加了一个固定的盐值: dJLdCJiVnDvM9JUpsom9,长度为 20

继续跟进去,发现第一行就是 md5 这个函数,第一个参数为明文,第二个参数为明文的长度,第三个参数为加密过后的结果
继续 hook 地址 0x13E3C,得到加密后的结果为:

去在线网站加密一下,看看跟标准的 md5 加密有什么不同

得到的结果跟标准的 md5 结果是一样的,但是为什么跟 unidbg 之前模拟的结果不太一样呢
发现它会使用到 bytesToInt 这个方法

很可能是在这里面做了处理,进去看一下

前面一大堆都没做啥,主要是最后的 v8,将传进来的四个字节组合成大端序的 32 位整数

 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
import hashlib
import struct

def do_md5_sign_simulated(initial_msg: bytes):
    # 1. 计算标准 MD5
    m = hashlib.md5()
    m.update(initial_msg)
    v14 = m.digest() # 得到 16 字节数据
    
    # 2. 将 16 字节拆分为 4 个 32 位整数 (大端序,对应你之前的 bytesToInt)
    # '>4i' 表示 4 个有符号的 int32
    ints = struct.unpack('>4i', v14)
    print(ints)
    # 3. 对每个整数取绝对值,并转为字符串
    parts = []
    for val in ints:
        # 模拟 C 语言中的 if (v < 0) v = -v;
        abs_val = abs(val)
        parts.append(str(abs_val))
    
    # 4. 拼接所有字符串
    result_str = "".join(parts)
    
    # 返回结果及其长度 (对应 C 里的 v11)
    return result_str.encode('utf-8'), len(result_str)

msg = b"cid=210101;q=3uda9DTWh1HzaMzrcZhTxcwe8AgwkxzHjqEegyMDUsEB79NUuaF-yz3Tr95ivqP96KFYlrb4g_m0H9p6GoIAdO7kWUXiBFQsl7SZEgHXBL0bzG5qAdTpfBhia0FXpCUZRxtjFsJ4IkBXUSYqY67IbCR1a-G8iola34uRO1D-R46WTsnmOy4LmWTW2CDNyo9v;uid=bd17e261-c511-4ca6-b8a1-5f3900b733b81771931072631dJLdCJiVnDvM9JUpsom9"
digest_bytes, length = do_md5_sign_simulated(msg)
print(f"Digest: {digest_bytes.decode()}")  // 79903440421321686324907558431145496727
print(f"Length: {length}")

刚好就能够得到正确的值了

q

接下来就应该看一下 q 这个参数是从哪儿生成的了

跟到 C10584c.m24187b 这个函数中,然后一直跟一直跟,就能够发现是 `

接下来进行 hook localAESWork4Api 这个方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function hook_aes() {
  var String = Java.use('java.lang.String');
  var Base64 = Java.use('java.util.Base64');
  var CryptoHelper = Java.use("com.luckincoffee.safeboxlib.CryptoHelper");
  CryptoHelper["localAESWork4Api"].implementation = function (bArr, i2) {
    let result = this["localAESWork4Api"](bArr, i2);
    if (i2 == 0) {// 代表加密
     console.log("参数1::",String.$new(bArr)) 
     console.log("参数2::", i2)
     console.log("加密结果::",Base64.getEncoder().encodeToString(result))
    }
    if(i2 == 1){// 代表解密
      console.log("参数1::",Base64.getEncoder().encodeToString(bArr))
      console.log("参数2::",i2)
      console.log("解密结果::",String.$new(result))
    }
    return result;
  };
}

可以知道 i2的值如果为 0,代表加密;如果为 1,代表解密
也就是说请求参数中是加密,返回值就是解密了
hook 的结果如下:

1
2
3
4
5
6
7
8
9
参数1:: {"regId":"","appversion":"5001","deviceId":"android_lucky_f92362bbcc270362","systemVersion":"29","deviceBrand":"google"}
参数2:: 0
加密结果:: SEP7zksRC5arjIcj6P+BBMtLdwQEgQt6GUP+FPWo1O7G+VB+e5NXDoGPSjDc+GsvGGU3XsiBcfxOpGB9u/fUXTKFKVPliUjviel6s/01EZomoYJs8eAALHWgdP4Zk3S4aehwzehZ8zIKbyY3hsrE8dFn8Uo5vwM/lR+JVwyolU4=

-----分割线

参数1:: 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
参数2:: 1
解密结果:: {"busiCode":"BASE000","code":1,"content":{"flag":true,"moduleFlag":{"Android_Gold_Path_White_Open":false,"Android_Is_Open_Other_Push":true,"Android_Open_Hot_Fix":false,"Android_Cache_Open":false,"Android_ACTIVITY_INFO_SWITCH":true,"Android_LuckyTrack_Network_Switch":false,"Android_ReactNative_Home_entry":true,"Android_SWITCH_PAY_FINISH_STRATEGY":false,"Android_ReactNative_Profile_entry":false,"Android_ReactNative_Total_entry":false,"Android_Shop_CoffeeMachine_Switch":true,"Android_ReactNative_Balance_entry":false,"Android_Tongdun_Captcha":true,"ANDROID_TONGDUN_ID":true,"shumeng_id":true,"Android_ReactNative_Invoice_entry":false,"Android_LuckyTrack_performance_Switch":false,"Android_New_Product_Detail":true,"ANDROID_TRACK_COMPONENT_OPEN_SWITCH":true,"Android_ReactNative_Cart_Switch":false,"Android_Tencent_HttpDns":true,"Android_SCAN_DEFAULT_QR_MODE":true,"Android_LuckyTrack_hmonitor_Switch":false,"Android_SWITCH_SIGNLE_ELECTRIC_WEB":false,"Android_AR_Switch":false,"Android_OPEN_MENU_CDN":true,"Android_Gold_Path_Black_Open":false,"Android_Luck_Track_Open":true,"ANDROID_RETAIL_MINE_ITEM_ENTRY":false},"moduleStatus":{"Android_Gold_Path_White_Open":0,"Android_Is_Open_Other_Push":0,"Android_Open_Hot_Fix":0,"Android_Cache_Open":0,"Android_ACTIVITY_INFO_SWITCH":0,"Android_LuckyTrack_Network_Switch":0,"Android_ReactNative_Home_entry":0,"Android_SWITCH_PAY_FINISH_STRATEGY":-100,"Android_ReactNative_Profile_entry":0,"Android_ReactNative_Total_entry":0,"Android_Shop_CoffeeMachine_Switch":0,"Android_ReactNative_Balance_entry":0,"Android_Tongdun_Captcha":0,"ANDROID_TONGDUN_ID":0,"shumeng_id":0,"Android_ReactNative_Invoice_entry":0,"Android_LuckyTrack_performance_Switch":0,"Android_New_Product_Detail":0,"ANDROID_TRACK_COMPONENT_OPEN_SWITCH":0,"Android_ReactNative_Cart_Switch":0,"Android_Tencent_HttpDns":0,"Android_SCAN_DEFAULT_QR_MODE":0,"Android_LuckyTrack_hmonitor_Switch":0,"Android_SWITCH_SIGNLE_ELECTRIC_WEB":-2,"Android_AR_Switch":0,"Android_OPEN_MENU_CDN":0,"Android_Gold_Path_Black_Open":0,"Android_Luck_Track_Open":0,"ANDROID_RETAIL_MINE_ITEM_ENTRY":0},"msg":"ok","status":0},"handler":"CLIENT","loginState":0,"msg":"成功","status":"SUCCESS","uid":"bd17e261-c511-4ca6-b8a1-5f3900b733b81771931072631","version":"101","zeusId":"luckycapiproxy-0add6a15-492229-547157"}

接下来就应该去 ida 中进行分析了,看一下这个是什么 aes,是否进行了魔改,是否是白盒 aes,他的算法逻辑也是在 cryptoDD.so 中进行加密的
继续使用 unidbg 对这一部分进行模拟,发起调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void localAESWork4Api(){
	ArrayList<Object> list = new ArrayList<>(10);
	list.add(vm.getJNIEnv());
	list.add(0);
	String arg1 = "{\"regId\":\"\",\"appversion\":\"5001\",\"deviceId\":\"android_lucky_f92362bbcc270362\",\"systemVersion\":\"29\",\"deviceBrand\":\"google\"}";
	ByteArray byteArray = new ByteArray(vm, arg1.getBytes(StandardCharsets.UTF_8));
	list.add(vm.addLocalObject(byteArray));
	list.add(0);
	Number number = module.callFunction(emulator, 0x1b1cd, list.toArray());
	Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "localAESWork4Api");
	System.out.println("result==>" + Base64.getEncoder().encodeToString((byte[]) vm.getObject(number.intValue()).getValue()));
}

以下是调用结果,连环境都不用补
调用结果一致,下面开始进行算法分析
首先由这个名字我们就知道这个是 AES,算法,接着就需要看一下到底是 ECB/CBC 模式?什么填充方式,密钥是什么?iv 是什么?
将参数换成 0123456789abedef0123456789abedef,如果重复了,就代表是 ECB 模式,没有 iv 的参与

好的,这个就是 ECB 模式,相同的明文加密得到相同的密文,没有 iv,接下来应该验证的是什么填充方式,密钥是什么了 跳转到 0x18903


跟进 wbaes_encrypt_ecb,第一个参数为输入的明文,第二个参数为明文的长度,第三个参数为输出,第四个参数为工作模式
对这个函数进行 hook 一下

1
emulator.attach().addBreakPoint(module.base+0x17BD);


首先看到明文的填充方式是 pkcs7,可以由末尾填充的 0x10 进行推测到
接下来继续走,发现有 aes128_enc_wb_coffaes128_enc_wb_xlc 这两个方法,不确定是哪个方法触发了,直接 unidbg 中 hook 一下

1
2
emulator.attach().addBreakPoint(module.base+0x15320); // aes128_enc_wb_coff
emulator.attach().addBreakPoint(module.base+0x15C8C); // aes128_enc_wb_xlc

发现在 aes128_enc_wb_coff 断住了 3 次, aes128_enc_wb_xlc 一次都没断住就出结果了,说明走的是 aes128_enc_wb_coff 这个方法
跟进去之后发现一直在进行查表操作,可以确定就是白盒 AES 了
对于白盒 AES,一般是以下三个步骤:

  • 找轮
  • 找时机,找具体第几轮做故障注入
  • 找 state 对于找轮,可以看见代码里面符号还没去,可以找到 wbShiftRows 这个函数,它在 AES 算法中,十轮运算都会存在,是一个比较好的选择 进入 hook 一下,同时也要注意需要把明文输入改成 16 个字节以内
1
2
3
4
5
6
7
8
9
emulator.attach().addBreakPoint(module.base + 0x14F98, new BreakPointCallback() {
	int count = 0;
	@Override
	public boolean onHit(Emulator<?> emulator, long address) {
		count += 1;
		System.out.println("count==>" + count);
		return true;
	}
});


接下来开始 dfa 攻击,在第 9 轮注入我们的故障密文, wbShiftRows 唯一的参数既做输入又做输出,所以可以针对 0x14F98 这个地址的第一个参数进行修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void dfaAttack(){
	emulator.attach().addBreakPoint(module.base + 0x14F98, new BreakPointCallback() {
		int count = 0;
		UnidbgPointer arg0Pointer;

		@Override
		public boolean onHit(Emulator<?> emulator, long address) {
			count += 1;
			arg0Pointer = emulator.getContext().getPointerArg(0);
//                System.out.println("count==>" + count);
			if (count % 9 == 0) {
				arg0Pointer.setByte(randInt(0,15),(byte) randInt(0,0xff));
			}
			return true;
		}
	});
}

当然,这仅仅只是一个故障密文,为了更好的还原出密钥,需要收集多个密文才行
所有代码如下:

  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
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package com.ruixing;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.HookZzArm32RegisterContext;
import com.github.unidbg.hook.hookzz.WrapCallback;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Random;


public class CryptoDD extends AbstractJni implements IOResolver {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    //构造函数,配置相关信息
    CryptoDD() {
//        emulator = AndroidEmulatorBuilder
//                .for32Bit()
//                .setRootDir(new File("target/rootfs"))
//                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
//        emulator.getSyscallHandler().addIOResolver(this); // 绑定IO重定向接口
//        System.out.println("当前进程PID:" + emulator.getPid());
        emulator = AndroidEmulatorBuilder.for32Bit().build();
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("apks/ruixing/ruixing.apk"));
        new AndroidModule(emulator,vm).register(memory);
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        module = dm.getModule();
        vm.setJni(this);
        vm.setVerbose(false);
        dm.callJNI_OnLoad(emulator);

    }

    public void md5_crypt() {
//        emulator.attach().addBreakPoint(module.base + 0x14D54);
//        emulator.attach().addBreakPoint(module.base + 0x13E3C);
        ArrayList<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
        String s = "cid=210101;q=3uda9DTWh1HzaMzrcZhTxcwe8AgwkxzHjqEegyMDUsEB79NUuaF-yz3Tr95ivqP96KFYlrb4g_m0H9p6GoIAdO7kWUXiBFQsl7SZEgHXBL0bzG5qAdTpfBhia0FXpCUZRxtjFsJ4IkBXUSYqY67IbCR1a-G8iola34uRO1D-R46WTsnmOy4LmWTW2CDNyo9v;uid=bd17e261-c511-4ca6-b8a1-5f3900b733b81771931072631";
        ByteArray byteArray = new ByteArray(vm, s.getBytes(StandardCharsets.UTF_8));
        list.add(vm.addLocalObject(byteArray));
        list.add(1);
        Number number = module.callFunction(emulator, 0x1a981, list.toArray());
        ByteArray result_arr = vm.getObject(number.intValue());
        String result = new String(result_arr.getValue());
        Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "md5_crypt");
        System.out.println("sign==>" + result);
    }


    public static int randInt(int min, int max) {
        Random rand = new Random();
        return rand.nextInt((max - min) + 1) + min;
    }
    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, new BreakPointCallback() {
            int count = 0;
            UnidbgPointer arg0Pointer;

            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                count += 1;
                arg0Pointer = emulator.getContext().getPointerArg(0);
//                System.out.println("count==>" + count);
                if (count % 9 == 0) {
                    arg0Pointer.setByte(randInt(0,15),(byte) randInt(0,0xff));
                }
                return true;
            }
        });
    }
    public static byte[] hexToBytes(String hexString) {
        if (hexString.isEmpty()) {
            return null;
        }
        hexString = hexString.toLowerCase();
        final 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 bytesToHex(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 localAESWork4Api() {
//        emulator.attach().addBreakPoint(module.base+0x17BD4);
//        emulator.attach().addBreakPoint(module.base+0x15320); // aes128_enc_wb_coff
//        emulator.attach().addBreakPoint(module.base+0x15C8C); // aes128_enc_wb_xlc

//        emulator.attach().addBreakPoint(module.base+0x15AD6);
//        emulator.attach().addBreakPoint(module.base+0x14F98);

        ArrayList<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
        String arg1 = "0xnonce";
        ByteArray byteArray = new ByteArray(vm, arg1.getBytes(StandardCharsets.UTF_8));
        list.add(vm.addLocalObject(byteArray));
        list.add(0);
        Number number = module.callFunction(emulator, 0x1b1cd, list.toArray());
//        Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "localAESWork4Api");
        System.out.println(bytesToHex((byte[]) vm.getObject(number.intValue()).getValue()));
    }

    public void wbaes_ecb(){
        MemoryBlock inputBlock = emulator.getMemory().malloc(16, true);
        UnidbgPointer inPtr = inputBlock.getPointer();
        MemoryBlock outBlock = emulator.getMemory().malloc(16, true);
        UnidbgPointer outPtr = outBlock.getPointer();
        byte[] inByteData = hexToBytes("30786e6f6e63650b0b0b0b0b0b0b0b0b");
        assert inByteData != null;
        inPtr.write(0,inByteData,0,inByteData.length);
        module.callFunction(emulator, 0x17bd5, inPtr, 16, outPtr, 0);
        String ret = bytesToHex(outPtr.getByteArray(0, 0x10));
        System.out.println(ret);
        inputBlock.free();
        outBlock.free();


    }

    public static void main(String[] args) {
        CryptoDD cryptoDD = new CryptoDD();
//        cryptoDD.md5_crypt();
//        for (int i = 0; i < 200; i++) {
//            cryptoDD.dfaAttack();
//            cryptoDD.wbaes_ecb();
//        }
        cryptoDD.wbaes_ecb();
//        cryptoDD.localAESWork4Api();
    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("pathname open:" + pathname);
        return null;
    }
}

接着就收集到了多个密文

将所有搜集到的密文放到一个文件中,注意,第一行需要放 dfa 攻击之前的密文

1
2
3
4
import phoenixAES


phoenixAES.crack_file("./tracefile",[],True,False,verbose=2)

然后,就能够得到最后一轮的密钥

用 stark工具恢复出初始密钥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(android_reverse) PS D:\develop\android\CODE\android_reverse> .\aes_keyschedule.exe 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


得到的值跟 unidbg 模拟的值一致