3469 字
17 分钟
某咖啡app逆向分析

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

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

抓包#

首先抓一下首页的这个包

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

sign#

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

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

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 的结果如下:

Terminal window
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 进去的是什么字符串

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

得到如下的结果:

Terminal window
[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 这个函数,看一下需要哪些参数,但是这儿的参数类型是字节数组的类型,需要进行转换一下

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;
}
})
}

得到如下的结果:

Terminal window
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 一下

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

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

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

使用 unidbg 模拟调用一下,unidbg 用来分析算法感觉挺好用的

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 一下

Terminal window
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 位整数

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 这个方法

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 的结果如下:

Terminal window
参数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 对这一部分进行模拟,发起调用

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 一下

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


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

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 个字节以内
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 这个地址的第一个参数进行修改

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;
}
});
}

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

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 攻击之前的密文

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

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

用 stark工具恢复出初始密钥

Terminal window
(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 模拟的值一致

某咖啡app逆向分析
https://819229.xyz/posts/某咖啡app逆向分析/
作者
0xnonce
发布于
2026-02-24
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...
0/2000

    最后更新于 ,距今已过 187

    部分内容可能已过时