首页
论坛
专栏
课程

[原创]Android某社区加密参数分析

2019-9-13 03:24 2389

[原创]Android某社区加密参数分析

2019-9-13 03:24
2389

前言

一直在索取论坛大佬的帮助,其实我自己也写过一些文章在自己博客,但我总觉得在看雪发表文章应该需要有用的、有技术含量的,就不敢发,怕误导人,这次分享的内容是android一个社区,叫 酷安,应该有人知道;其次也是借这次机会请教几个问题,这篇文章很早就写了,但有些问题一直没去解决。。

.

用到的工具有这些:frida、ida、jeb

.

.

分析过程

先抓个包

 

 

可以看到其中有个请求头 X-App-Token,这就是验证,至于X-App-Device这玩意儿应该是获取你手机信息的,不管它,先看看软件源代码,找到请求方法

1、jeb分析

没加固,好像也没混淆,舒服

 

 

搜索关键字:X-App-Token

 

 

很明显找到了我们要的东西了,(jeb3.0按tab键反编译)

 

 

这个 X-App-Token 是变量 v2_1,v2_1是,一个AuthUtils类里的getAS方法返回的

 

 

 

跟进可以发现这是一个native方法,lib是native-lib

 

 

到这里就没法用jeb分析了,我们先看看参数2 deviceId 是个什么玩意儿

 

可以看到是一个 SystemUtils 类里的 getDeviceID方法返回的,传入一个context参数

 

 

我们去app的application hook这个方法

 

 

我找到一个代码量最少的方法,这样可以帮助我们不破坏原逻辑的情况下hook (这里也许会误导别人,不破坏原逻辑可以直接在hook里调用当前方法,写这篇文章的时候还不是很懂)

 

 

hook的代码很简单

Java.perform(function() {
    var CoolMarket = Java.use('com.coolapk.market.CoolMarketApplication');
    CoolMarket.onLog.implementation = function() {

        var deviceId = Java.use('com.coolapk.market.util.SystemUtils').getDeviceId(this);
        console.log('Device Id: ', deviceId);

        var app_token = Java.use('com.coolapk.market.util.AuthUtils').getAS(this, deviceId);
        console.log('App Token: ', app_token);

        console.log('----------');
        return 1;
    }
})

拿到 deviceId 后分析so

.

.

2、ida分析

解压apk拿到 native-lib.so,用ida打开

 

 

我们已知方法名和参数个数,那么就先搜索方法名

 

Function Window 按 option+t 搜索 getAS,可以看到,毛都没有

 

 

 

那我们就到 IDA View 里搜索,快捷键一样

 

 

找到了这个,参数是两个,但这个不是方法,没法 F5 反编译

 

 

不瞒你们,这个DCB是个什么玩意儿我也不知道.
但是我在 Function Window 瞎翻想找一些我能看得懂的方法名时看到了这个

 

 

我一看,这个方法好像和 getAS 有点关系就顺手 F5 了

 

简单看了看里面的代码,我估计我要找的是这个方法,为什么是这个方法而不是其他的

 

我分析了 X-App-Token 这个验证的组成,它长这样:f2c29a109fde487e9350d3e6b881036a8513efac-09ea-3709-b214-95b366f1a1850x5d024391

 

我之前就获取到了我的 Device Id,我无意间看到了我的 device id就在里面,然后我把它拆分成了这样:

  • f2c29a109fde487e9350d3e6b881036a
  • 8513efac-09ea-3709-b214-95b366f1a185
  • 0x5d024391
 

第一项很明显是md5密文,第二部分就是device id,最后是一个十六进制,不知道什么玩意儿,但我在那个 getAuthString 代码里看到了这段代码

 

 

这是一个字符串拼接的过程,其中 v82 是md5密文,也是字符串的头部,后面接着是 v43(device id)、字符串0x、最后是 hex_time (这是我改了后的命名),所以我就能确定这个方法就是我想要的;

 

上面说的也就是接下来要分析的,其中那个十六进制的东西就是时间戳,我们只需要分析出md5是怎么来的就行了,我们知道md5是 v61v61 的加密代码在这

 

 

加密的内容是v58,v58是一个经过base64编码后的变量

 

 

我懒得去看它是什么了,我直接hook了md5加密类,有三个方法,一个是md5(应该是构造方法把)、update、finalize,hook 代码如下

// 这里算基址是从四哥那学的
var JNI_LOAD_POINTER = Module.getExportByName('libnative-lib.so', 'JNI_OnLoad'); // 首先拿到 JNI_OnLoad方法的地址
// 这里减去的是从so中得到的JNI_OnLoad的地址 0x31A04
var BASE_ADDR = parseInt(JNI_LOAD_POINTER) - parseInt('0x31A04'); // 用程序运行中JNI_OnLoad的绝对地址减去它的相对地址得到基址

// MD5::MD5
Java.perform(function() {
    //  然后用基址 + 要hook的方法的相对地址就得到了绝对地址
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x32168')).toString(16) // 获取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根据方法地址构建NativePointer
    console.log('[MD5::MD5] hook pointer: ', pointer)

    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                console.log('\n')
                console.log('=====> [MD5::MD5] -> [方法调用前]')
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0))) // Memory.readCString 是读取地址为字符串,类似的还有readUtf8String、readUtf16String等
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::MD5] -> [方法调用后]:')
                console.log('返回值: ', retval)
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('\n')
            }
        }   
    )
})



// MD5::update
Java.perform(function() {
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x329AC')).toString(16) // 获取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根据方法地址构建NativePointer
    console.log('[MD5::update] hook pointer: ', pointer)

    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                arg2 = args[2]
                console.log('\n')
                console.log('=====> [MD5::update] -> [方法调用前]')
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('参数3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))

                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::update] -> [方法调用后]:')
                console.log('返回值: ', retval)
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('参数3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('\n')
            }
        }   
    )
})


// MD5::finalize
Java.perform(function() {
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x321C4')).toString(16) // 获取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根据方法地址构建NativePointer
    console.log('[MD5::finalize] hook pointer: ', pointer)
    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                arg2 = args[2]
                arg3 = args[3]
                console.log('\n')
                console.log('=====> [MD5::finalize] -> [方法调用前]')
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('参数3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('参数4: {0} => {1}'.format(arg3, Memory.readCString(arg3)))
                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::finalize] -> [方法调用后]:')
                console.log('返回值: ', retval)
                console.log('参数1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('参数2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('参数3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('参数4: {0} => {1}'.format(arg3, Memory.readCString(arg3)))
                console.log('\n')
            }
        }   
    )
})

运行后得到了那个base64编码过的内容:

dG9rZW46Ly9jb20uY29vbGFway5tYXJrZXQvYzY3ZWY1OTQzNzg0ZDA5NzUwZGNmYmIzMTAyMGYwYWI/MzgyMzIxNWQ5MWQyOWQ5ODg3ZWJjMDVmMGQ3ZmQzMGQkODUxM2VmYWMtMDllYS0zNzA5LWIyMTQtOTViMzY2ZjFhMTg1JmNvbS5jb29sYXBrLm1hcmtldA==

 

经过解码后:

token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?3823215d91d29d9887ebc05f0d7fd30d$8513efac-09ea-3709-b214-95b366f1a185&com.coolapk.market

 

然后根据这段代码

 

 

上面解码后的内容可以拆分为:

  • token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?
  • 3823215d91d29d9887ebc05f0d7fd30d
  • $
  • 8513efac-09ea-3709-b214-95b366f1a185
  • &
  • com.coolapk.market
 

据我分析,只需要的到第二部分的md5加密的来历就行了,继续分析,找到了加密的地方

 

 

根据这图的画线,可以明确的知道这md5就是时间戳,在我hook的输出中也可以看到这个就是时间戳

 

 

 

至于 token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab? 这个是不变的

3、结论

token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab? + md5加密后的时间戳 + $ + device id + & + com.coolapk.market(包名),将其md5加密后得到 第一部分

 

token的来历就是:第一部分 + deivce id + 0x + 十六进制转换后的时间戳

 

简单的测试代码:

import requests
import time
import hashlib
import base64


DEVICE_ID = "8513efac-09ea-3709-b214-95b366f1a185"

def get_app_token():
    t = int(time.time())
    hex_t = hex(t)

    # 时间戳加密
    md5_t = hashlib.md5(str(t).encode('utf-8')).hexdigest()

    # 不知道什么鬼字符串拼接
    a = 'token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?{}${}&com.coolapk.market' \
        .format(md5_t, DEVICE_ID)

    # 不知道什么鬼字符串拼接 后的字符串再次加密
    md5_a = hashlib.md5(base64.b64encode(a.encode('utf-8'))).hexdigest()

    token = '{}{}{}'.format(md5_a, DEVICE_ID, hex_t)
    print(token)
    return token


def request():
    url = "https://api.coolapk.com/v6/main/indexV8?page=1"
    headers = {
        "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/9.2.2-1905301"
    }
    headers = {
        "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/9.2.2-1905301",
        "X-App-Id": "com.coolapk.market",
        "X-Requested-With": "XMLHttpRequest",
        "X-Sdk-Int": "28",
        "X-Sdk-Locale": "zh-CN",
        "X-Api-Version": "9",
        "X-App-Version": "9.2.2",
        "X-App-Code": "1903501",
        "X-App-Device": "QRTBCOgkUTgsTat9WYphFI7kWbvFWaYByO1YjOCdjOxAjOxEkOFJjODlDI7ATNxMjM5MTOxcjMwAjN0AyOxEjNwgDNxITM2kDMzcTOgsTZzkTZlJ2MwUDNhJ2MyYzM",
        "Host": "api.coolapk.com",
        "X-Dark-Mode": "0",
        "X-App-Token": get_app_token(),
    }

    resp = requests.get(url, headers=headers)
    print(resp.text)


if __name__ == '__main__':
    request()

最后

如果文章有误导的地方还望指正。
代码啥的都提交到Github了,CoolapkTokenCrack

 

最后我的问题是:在分析过程中有个疑惑就是为什么在ida中找不到getAS方法,反而变成了getAuthString,我自己分析的时候是随缘F5的。。。



[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-9-13 15:57 被zckuna编辑 ,原因:
最新回复 (12)
LowRebSwrd 2 2019-9-13 06:29
2
0
图片重新贴一下,防止外链失效,保证作品质量,方便读者阅读
zckuna 2019-9-13 15:42
3
0
LowRebSwrd 图片重新贴一下,防止外链失效,保证作品质量,方便读者阅读[em_1]
哦好的
wx_私人账号定制 2019-9-13 18:18
4
0
抓包工具能否分享下
zckuna 2019-9-13 21:14
5
0
wx_私人账号定制 抓包工具能否分享下
https://www.lanzous.com/i65m77c 这个软件是付费的。。
小堆 1 2019-9-14 19:03
6
0
emmm你找的地方不太对。这个app动态注册了getAS这个函数,你进入jni_load函数,重新定义之后,我这边给出关键代码
v1 = (*a1)->GetEnv(a1, &v6, 65542);
v4 = (*v6)->FindClass(v6, "com/coolapk/market/util/AuthUtils");
v3 = v6;
if ( !v4 || (v5 = (*v3)->RegisterNatives(v3, v4, getMethods, 1), result = 65542, v5 < 0) )
   result = -1;
其中,getMethods里面就是对应的动态注册的函数数组。点进去之后可以看到
.data:000DB004 BE 4C 0B 00       getMethods dd offset aGetas   ; DATA XREF: JNI_OnLoad+72↑o
.data:000DB004                                                 ; "getAS"
.data:000DB008 C4 4C 0B 00       dd offset aLandroidConten_1   ; "(Landroid/content/Context;Ljava/lang/St"...
.data:000DB00C 10 E6 0A 00       dd offset getAuthString
第一个是java层函数名
第二个是方法签名
第三个是映射到native的方法名称
真烧鱼 2019-9-16 09:18
7
0
http://www.520monkey.com/archives/1289这里面有讲
Ddddz 2019-9-16 10:41
8
0
zckuna 2019-9-17 00:20
9
0
真烧鱼 http://www.520monkey.com/archives/1289这里面有讲
我看过,但是讲的不多,学得不爽
zckuna 2019-9-17 00:20
10
0
小堆 emmm你找的地方不太对。这个app动态注册了getAS这个函数,你进入jni_load函数,重新定义之后,我这边给出关键代码 v1 = (*a1)->GetEnv(a1, &v6, ...
谢谢讲解
zylyy 2019-10-3 04:00
11
0
zckuna 2019-10-3 19:42
12
0
zylyy [em_13]
mb_lamrzuxu 1天前
13
0
游客
登录 | 注册 方可回帖
返回