安卓逆向百例十-币coin

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
交流群:

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值