App信息
包名:com.lucky.luckyclient
过检测
root检测
使用狐妖面具隐藏
frida检测
使用小工具绕过
脱壳
使用脱壳网站进行脱壳
抓包
1 | url: |
又抓了个登录包
1 | url: |
请求参数sign
,q
,响应体都是加密的
又抓了个包
1 | utl: |
定位
jadx打开脱壳后的dex
Java通过System.load()和System.loadLibrary()来加载动态库,直接搜索System.loadLibrary
发现一个可疑类
双击进去看到so的名字是加密的,还有四个native方法和三个用于加解密的方法
主动调用getString2()方法来解密so名字
1 | function call_getString2(){ |
调用了,但是输出结果是乱码
用hook_RegisterNatives打印动态注册函数及其所在的so
然后找到了libcryptoDD.so
分析
看了源码这几个native方法中只有localAESWork
和md5_crypt
被调用了
猜测sign是由md5_crypt
生成的
q
和响应体是由localAESWork
生成的
接下来对localAESWork
、md5_crypt
、a
、b
、c
进行hook
1 | function bArrToString(bArr) { |
结果
1 | CryptoHelper.md5_crypt is called! |
1 | function bArrToString(bArr) { |
结果
1 | CryptoHelper.c is called: str={"mobile":"17888888888","countryNo":"86","validateCode":"123456","appversion":"5265","type":1,"deviceId":"android_lucky_ef5722c5-96ff-4521-8096-27530a0365b2","systemVersion":"29","blackBox":"oGPHE1741688414xYaSQkj72m7","uniqueCode":"DUQKlOhFPQSnh57_KB6erjHdTADHhPk26j60RFVRS2xPaEZQUVNuaDU3X0tCNmVyakhkVEFESGhQazI2ajYwc2h1","regionId":"CO0001","regId":"","deviceBrand":"google","isSecurityVerify":false} |
c方法对一堆参数加密,加密结果为q值,md5_crypt对q及其他内容进行签名,b方法对响应体和q解密
c方法
1 | public synchronized String c(String str) { |
这里面有一个字段a10
需要找它的来源,它来源于a方法,而它是个接口方法
找它的实现类com.lucky.lib.http2.b
这里面实现了a
方法
接下来通过主动调用获取它的值
1 | function get_a10(){ |
输出
1 | a10: qY69eWyYxkLQDygfcMavdEkJ+lDaV2IQcqdsfGGuNDs= |
接下来看这部分代码,弄清楚这里面的值和逻辑
1 | str2 = new String(Base64.encode(localAESWork(str.getBytes(), 2, Base64.decode(a10.replace('-', '+').replace('_', '/').getBytes(), 2)), 2)); |
看我们hook到的结果协助分析
1 | CryptoHelper.c is called: str={"mobile":"17888888888","countryNo":"86","validateCode":"123456","appversion":"5265","type":1,"deviceId":"android_lucky_ef5722c5-96ff-4521-8096-27530a0365b2","systemVersion":"29","blackBox":"oGPHE1741688414xYaSQkj72m7","uniqueCode":"DUQKlOhFPQSnh57_KB6erjHdTADHhPk26j60RFVRS2xPaEZQUVNuaDU3X0tCNmVyakhkVEFESGhQazI2ajYwc2h1","regionId":"CO0001","regId":"","deviceBrand":"google","isSecurityVerify":false} |
unidbg调用
初始环境搭建
1 | package com.luckincoffee.safeboxlib; |
运行一下,报错了,看打印的信息
1 | JNIEnv->NewByteArray(416) was called from RX@0x12043ae4[libcryptoDD.so]0x43ae4 |
在0x43b08
这个地方报错了,IDA分析一下
在下面这个地方,内存无法写入
用unidbg处理一下
1 | public void patchFreeByConsoleDebugger(){ |
然后可以输出正确结果了
1 | ret=5f3d11f367d2e3ce5bdb76daf35edee03b1b6ce1b3c1846a175a1ec87bc6c9ddc66d65a239b38f7b7727befc6a17c44a3fa7921c901c66596ca7cedacc48a08db0cb0931efcbb9a7afbf9ab379d992ba66f84c3e529d4e2f96072db2212bc9ab4225a162ee55d4ea0e9648d49d532735fe628c175e6b81163b05bf97bd030634c09dc45f0e536b75dd1f29f8ea328eb60907c08ff1c309d7ea64257aad490fe7a50f06c86d33a6cfcd1494ec5ae0b07fc4d1dc5094a6161985ebee8bac53057e07603836b0bcc15b34dcaa4f8ff361b5df5855e5f5f9d4c8b8c5c0f9c936f697c37795ac6ef7fb66513216f2019d08fd477ef712b4cf4036441a0a56e429f3fc440bbfce851936683e74024fc1abcfeb50480f747fcaabf346eebca3d5881ecbbaf639c96a6db5f22d362223bc766e39fc0a4472b197a5be78f2bf947afe15660b97c9b496d0e39103ed77672327ab36c7920e6d763f1271bb017615c49afcb50429218b6f3d76156129095eb1082f33bbc2f732f7ba76584b04cd388aadc16e0faaf872eecb1810a6bb25ae9532a600bf2b597e2e50bb97d0df5fa4a7457f53 |
打印详细日志信息
1 | import org.apache.log4j.Level; |
把Logger.getLogger(DalvikVM64.class).setLevel(Level.DEBUG);
添加到localAESWork()
方法里
使用简单的明文测试
1 | 明文用:bileton |
输出
1 |
|
根据日志看到结果是在0x12399010
这个地方来的
trace 0x12399010
1 | emulator.traceWrite(0x12399010,0x12399010+0x10); |
这是trace下俩的数据
1 | [12:07:18 602] Memory WRITE at 0x12399010, data size = 8, data value = 0x0000000000000000, PC=RX@0x121bc688[libc.so]0x1c688, LR=RX@0x12043970[libcryptoDD.so]0x43970 |
这里看到数据写入的位置是在0x18940
,数据是以小端存储的LR寄存器的值是0x1892c
1 | 0x243d5ae408d32bfd |
使用IDA分析0x1892c
反汇编
分析sub_189C4()
下断点打印参数
1 | emulator.attach().addBreakPoint(module.base+0x189C4); |
打印X0寄存器的值
如下是输入的明文
打印X1寄存器的值
如下是一段空值
根据入参的值,看它被传进了哪个函数里找到AES加密的位置
找到一个函数sub_21BEC()
它的内容比较可疑
对这个函数进行hook
1 | emulator.attach().addBreakPoint(module.base + 0x21BEC, new BreakPointCallback() { |
如下打印了10次
先保留一份正确的密文
1 | fd2bd308e45a3d24b2c715b54b92713f |
然后在第九轮的时候进行故障注入
故障注入差分
故障注入1
对这几个寄存器进行故障注入
1 | emulator.attach().addBreakPoint(module.base + 0x21E8C, new BreakPointCallback() { |
循环10次
1 | public static void main(String[] args) { |
循环10次,输出密文
1 | fd2bbc08e43b3d24eec715b54b927120 |
差分1
1 | import phoenixAES |
输出
1 | Round key bytes recovered: |
故障注入2
换一个寄存器
1 | emulator.attach().addBreakPoint(module.base + 0x21E90, new BreakPointCallback() { |
输出
1 | fd2bd34ae45afb24b22315b5a192713f |
差分2
1 | import phoenixAES |
输出
1 | Round key bytes recovered: |
故障注入3
修改hook地址和寄存器
输出
1 | fa2bd308e45a3dc7b2c737b54b48713f |
差分3
输出
1 | Round key bytes recovered: |
故障注入4
1 | fdd7d308275a3d24b2c715bf4b929d3f |
差分4
1 | Round key bytes recovered: |
好了,这就是第10轮的轮密钥
由第10轮的轮密钥推算初始密钥
推出初始密钥为:32395842753445546A487830596E6643
验证下,如下加密结果正确
CyberChef中处理不了字符串的replace,如下
Python生成q
1 | from Crypto.Cipher import AES |
unidbg调用md5_crypt
1 | public void md5_crypt(){ |
输出并打印详细日志
1 | JNIEnv->SetByteArrayRegion([B@0x0000000000000000000000000000000000000000000000000000000000000000000000000000, 0, 38, RW@0x12393030) was called from RX@0x120441a8[libcryptoDD.so]0x441a8 |
使用简单明文参数bileton
1 | 76453193612323879985881527471282403340 |
根据可以看出数据来源于0x441a8
这个地址处,到IDA里找到这个地址
到IDA里看没发现什么
反汇编后,看传入的明文参数的传递过程,到了sub_4095C()
通过断点调试查看参数,发现数据后面加了盐29XBu4ETjHx0YnfC
md5
这里后面分析不下来了,看了别人的文章了解到对md5的结果又进行了处理,这里直接把算法搬过来
1 | def getsign(): |
Python实现md5_crypt
1 | import hashlib |
Python实现响应体解密
1 | from Crypto.Cipher import AES |
输出
1 | {"busiCode":"BASE001","code":7,"content":null,"handler":"USER","msg":"验证码无效","status":"BASE_ERROR","uid":"1f0080f8-238f-483a-be04-9a7e2ca4c0e61741688413791","version":"101","zeusId":"luckycapiproxy-0ade0957-483805-214303"} |
整合代码,实现完整流程
1 | from Crypto.Cipher import AES |
输出
1 | {"bizId":"","busiCode":"BASE001","code":7,"content":null,"handler":"USER","msg":"验证码无效","shardingId":"","status":"BASE_ERROR","uid":"6e1c943b-cc1f-41ea-8ebd-b5d06ddae5c11741928800889","version":"101","zeusId":"luckycapiproxy-0ade7194-483869-687159"} |