typora-root-url: ./pic
安卓逆向百例十-币coin
现在售价依旧是99¥,计划更新100案例,平均一个案例1块钱,要什么自行车!
案例起源:
有位老哥 经过炒币失败后想做一个这种查询接口的app,但是苦于他的资金比较紧缺,就给我几百块,但是架不住苦苦哀求,我就做了,顺便当个案例。
版本: 币coin 4.0.3
包名: com.temperaturecoin
1.抓包分析
url是https://i.bicoin.com.cn/firmOffer/getUserAccountInfoBySecretNew?salt=8&sign=CDGCCEJCPCIGGGLICMVVBQOIEJTNNUQQN&time=1724126970032&userId=894919
位置在:
其中我们今天要去逆向的是 sign的生成,以及data的解密。
2.关键词定位
搜索
查找用例:
进入方法内 我们可以看的 sign是 经过了AESUtil.s
其中我们可以可以得到 :
time 就是时间戳 ,salt是一个 随机的值 sign 是 time + salt + "getUserAccountInfoBySecretNew"
接下来跳转到AESUtil.s 方法里面 跳转到这个位置:
hook验证一下入参:
查看一下加载的so文件是 ns:
打开ida 反编译 libns.so 搜索java我们发现是静态注册的 并且发现了 Java_com_bcoin_ns_S_s这个字眼:
进去看看喽!顺便把fastcall Java_com_bcoin_ns_S_s(int64 a1 的a1改成 JNIEnv *
jstring __fastcall Java_com_bcoin_ns_S_s(JNIEnv *a1, __int64 a2, __int64 a3)
{
const char *v5; // x20
const char *v6; // x21
jstring result; // x0
int v8; // w10
__int64 (__fastcall *v9)(); // x10
char v10; // w24
int v11; // w9
char *v12; // x20
size_t v13; // w0
char *v14; // x21
unsigned __int64 v15; // x10
char *v16; // x11
unsigned __int64 v17; // x9
char *v18; // x10
char v19; // t1
_OWORD *v20; // x12
__int128 *v21; // x13
unsigned __int64 v22; // x14
__int128 v23; // q0
__int128 v24; // q1
_QWORD v25[2]; // [xsp+0h] [xbp-40h] BYREF
v25[1] = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = (*a1)->GetStringUTFChars(a1, a3, 0LL);
v6 = (*a1)->GetStringUTFChars(a1, token, 0LL);
(*a1)->ReleaseStringUTFChars(a1, a3, v5);
(*a1)->ReleaseStringUTFChars(a1, token, v6);
result = 0LL;
if ( v5 && v6 )
{
v8 = idxMethod % 3;
if ( idxMethod % 3 )
{
if ( v8 == 2 )
{
v9 = jointMd5;
v10 = 100;
}
else if ( v8 == 1 )
{
v9 = axor;
v10 = 67;
}
else
{
v9 = chainXor;
v10 = 97;
}
}
else
{
v9 = cxor;
v10 = 98;
}
if ( idxMethod == 2147483646 )
v11 = 0;
else
v11 = idxMethod + 1;
idxMethod = v11;
v12 = (v9)(v5, v6);
__android_log_print(3, "bc", "pre payload: %s", v12);
if ( v12 )
{
v13 = strlen(v12);
v14 = v25 - ((v13 + 1 + 15LL) & 0x1FFFFFFF0LL);
*v14 = v10;
if ( v13 >= 1 )
{
if ( v13 > 0x1FuLL && (v14 + 1 >= &v12[v13] || v12 >= &v14[v13 + 1]) )
{
v15 = v13 - (v13 & 0x1F);
v20 = v14 + 17;
v21 = (v12 + 16);
v22 = v15;
do
{
v23 = *(v21 - 1);
v24 = *v21;
v22 -= 32LL;
v21 += 2;
*(v20 - 1) = v23;
*v20 = v24;
v20 += 2;
}
while ( v22 );
if ( (v13 & 0x1F) == 0 )
goto LABEL_21;
}
else
{
v15 = 0LL;
}
v16 = &v12[v15];
v17 = v13 - v15;
v18 = &v14[v15 + 1];
do
{
v19 = *v16++;
--v17;
*v18++ = v19;
}
while ( v17 );
}
LABEL_21:
v14[v13 + 1] = 0;
free(v12);
return (*a1)->NewStringUTF(a1, v14);
}
return (*a1)->NewStringUTF(a1, "");
}
return result;
}
眼尖的朋友已经看见了 这里有个 __android_log_print 我们可以使用adb 来监控他的输出
在设备上运行日志记录工具 logcat
,这是 Android 提供的一个工具,用于实时查看系统和应用程序的日志。
在终端中执行以下命令启动 logcat
:
adb logcat
这会显示设备上的所有日志信息。如果你想要过滤特定标签(例如 "bc"
),可以使用:
adb logcat -s bc:D
这里的 -s
选项指定了过滤的标签,D
表示只显示 DEBUG
级别及以上的日志。
logcat:524b5a6860bad1a0bbe0712e48a5debb
抓包:d524b5a6860bad1a0bbe0712e48a5debb
我们从中可以发现 其实就是 结果 加了一个d 对吧,但是我们通过观察 logcat 和 抓包会发现有的时候是大写 有的时候是小写而且规律也不一样。他其实和这个有关系
这段代码根据 idxMethod
的值选择一个函数指针,并根据不同的条件设置函数的参数和值。接着调用这个函数,并将结果保存到 v12
中。代码的目的是根据不同的条件选择合适的处理函数,并在每次调用后更新 idxMethod
的值。
代码解释:
result = 0LL; // 初始化一个 long long 类型的变量 result 为 0
if (v5 && v6) // 如果 v5 和 v6 都不为 0(即它们都有效)
{
v8 = idxMethod % 3; // 计算 idxMethod 除以 3 的余数,并将结果赋值给 v8
if (idxMethod % 3) // 如果 idxMethod 除以 3 的余数不为 0
{
if (v8 == 2) // 如果余数是 2
{
v9 = jointMd5; // 将 v9 设置为 jointMd5(假设 jointMd5 是一个函数指针)
v10 = 100; // 将 v10 设置为 100
}
else if (v8 == 1) // 如果余数是 1
{
v9 = axor; // 将 v9 设置为 axor(假设 axor 是一个函数指针)
v10 = 67; // 将 v10 设置为 67
}
else // 如果余数是 0
{
v9 = chainXor; // 将 v9 设置为 chainXor(假设 chainXor 是一个函数指针)
v10 = 97; // 将 v10 设置为 97
}
}
else // 如果 idxMethod 除以 3 的余数为 0
{
v9 = cxor; // 将 v9 设置为 cxor(假设 cxor 是一个函数指针)
v10 = 98; // 将 v10 设置为 98
}
if (idxMethod == 2147483646) // 如果 idxMethod 的值为 2147483646
v11 = 0; // 将 v11 设置为 0
else
v11 = idxMethod + 1; // 否则,将 v11 设置为 idxMethod + 1
idxMethod = v11; // 更新 idxMethod 的值为 v11
v12 = (v9)(v5, v6); // 调用 v9 指向的函数,传入 v5 和 v6 作为参数,并将结果赋值给 v12
}
我们这里可以直接看他走 jointMd5 的位置 然后 直接查看入参不就行了 ,然后固定参数去请求。这样就不用还原 chainXor 和 cxor 函数了。
我们查看joinmd5这个函数 映入眼帘的是 这两个
v8 = strcpy(v6, s); strcat(v8, a1);
所以我直接去hook joinMd5 这个函数:
args1来源token:
抓包和adb 记录的:
所以就是 MD5(token + times + salt + 'getUserAccountInfoBySecretNew') 这个已经验证过了是标准的md5
str = '2c06cef65865431546fdb751f255508b'+str(times)+"6"+'getUserAccountInfoBySecretNew'
# print(str)
sign = 'd'+calculate_md5(str)
所以我们请求的时候 固定salt 就行了
具体代码放在文章最后...
3.返回值解密
一般来说 同一个数据包的加密的位置在都在一块,所以我们就顺藤摸瓜,就找到
hook验证:
去so层看看 :
一进去我们就看到 AES 128 pkcs5 的字眼:
进来后发现:
好家伙 Key = getKey(); IV = getIV(); 这么明显吗?那就不怪我了
上frida hook: hook到key 和 iv了
hook代码如下:
// key
var addr = Module.findBaseAddress('libns.so');
var KEY = addr.add(0x10144);
console.log(KEY);
Interceptor.attach(KEY,{
onEnter:function (args){
console.log("---------key 进入了-----------")
},
onLeave:function(retval) {
console.log("--------------------")
console.log('KEY 返回值:',hexdump(retval,{length:16}))
}
})
// IV
var addr = Module.findBaseAddress('libns.so');
var IV = addr.add(0x10144);
console.log(IV);
Interceptor.attach(IV,{
onEnter:function (args){
console.log("---------IV 进入了-----------")
},
onLeave:function(retval) {
console.log("--------------------")
console.log('IV 返回值:',hexdump(retval,{length:16}))
}
})
至此我们整个流程就已经完成了。
4.python代码还原
import time
from loguru import logger
import requests
import hashlib
from Crypto.Cipher import AES
import base64
import requests
def unpad(data):
"""去除填充"""
pad_length = data[-1]
return data[:-pad_length]
def decrypt_aes_cbc(ciphertext_base64, key, iv):
# key = bytes(key_text, 'utf-8')
# iv = bytes(iv_text, 'utf-8')
ciphertext = base64.b64decode(ciphertext_base64)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
plaintext = unpad(plaintext).decode('utf-8')
return plaintext
def calculate_md5(data):
# 创建一个MD5哈希对象
md5_hash = hashlib.md5()
# 更新哈希对象
md5_hash.update(data.encode('utf-8'))
# 返回MD5哈希的十六进制表示
return md5_hash.hexdigest()
headers = {
xxx
}
times = int(time.time() * 1000)
# 17236209670982
# 1723621530537
# 1723621708604
# print(times)
str = 'token'+str(times)+"6"+'getUserAccountInfoBySecretNew'
# print(str)
sign = 'd'+calculate_md5(str)
logger.info("sign签名{}".format(sign))
# print(sign)
headers['Sign'] = sign
headers['Time'] = f'{times}'
url = "https://i.bicoin.com.cn/firmOffer/getUserAccountInfoBySecretNew"
params = {
"salt": "6",
"sign": f"{sign}",
"time": f"{times}",
"userId": "894919"
}
response = requests.get(url, headers=headers, params=params).json()
print(response)
data = response['data']
key_hex = '8971483f9910300bdffee864cb135f34'
iv_hex = '8971483f9910300bdffee864cb135f34'
data = decrypt_aes_cbc(data, bytes.fromhex(key_hex), bytes.fromhex(iv_hex))
print(data)
#2c06cef65865431546fdb751f255508b17236221106396getUserAccountInfoBySecretNew
#2c06cef65865431546fdb751f255508b17236215305376getUserAccountInfoBySecretNew
交流群: