某赚钱APP逆向

拿到一个要 逆向的APP 目标是大厅中的数据接口,可以不登录

抓包分析

首先不登录的情况对APP 进行抓包

我常用的是mitmproxy的sock5模式+Postern vpn 转发

看到post请求加密了 header中有timespan和时间戳相关的参数和authorization 参数

然后复制为curl 然后通过工具转换成python 的requests的请求格式 运行下代码发现返回如下结果:

1
{"StateCode":1051,"errorMessage":null,"InnerData":null,"PropId":0,"PropType":0,"ShowDelay":0,"FinishAchieveMent":false,"FinishAchieveMentName":null,"AppendAchieveId":0}

不支持重放的请求中post 携带的数据应该有时间戳参数

使用frida 的Hook 脚本进行Hook 看看用下函数加密用的的是什么算法


加密参数是也正是请求参数

1
{"categoryIds":[],"hallType":2.0,"hideTask":false,"isFilterSubmited":false,"pageIndex":1.0,"pageSize":20.0,"treatmentType":1.0,"ClientTime":20220525145054}

使用脚本没有Hook到IV向量,最终要确定下加密模式和有无IV,这个可以使用加密前后的数据来和doFinal 的结果对比一下可以算出来

逆向分析

使用jadx 进行反编译出错, 换了1.2,1.3,1.4的版本之后之后还是不行.编译出来的函数没有办法正常显示.

开始我还以为有啥反爬新招式但是在github上找了下好像是版本的问题,jadx issue 1385

接着使用jeb3对函数进行反编译跟踪 通过抓包定位出来的函数Authorization参数定位到关键函数

HTTP request 中的Header最终发请求的时候是要转换成大写的,搜索的时候可以大小写都试一下,然后看下这个包是不是目标APP的包就可以了. 比如 alipay.sdk 或者tecent
开头的包,如果你不是抓这样两个公司相关的产品可以忽略不看的,只关注这个包是怎么写的.

最终定位到如下的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import android.text.TextUtils;
import com.cq.jdcover.app.b;
import java.io.IOException;
import okhttp3.Interceptor$Chain;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

class bc0$a implements Interceptor {
bc0$a(bc0 arg1) {
super();
}

public Response intercept(Chain arg7) throws IOException {
Request v0 = arg7.request();
String v1 = b.provideDemoRepository().getToken();
String v2 = b.provideDemoRepository().getAchievementIds();
String v4 = "0";
if(((TextUtils.isEmpty(((CharSequence)v1))) || (v1.equals(v4))) && (arg7.request().url().url().getPath().toLowerCase().contains("wecatbound"))) {
v1 = b.provideDemoRepository().getTempToken();
}

if(TextUtils.isEmpty(((CharSequence)v1))) {
}
else {
v4 = v1;
}

if(v2 == null) {
v2 = "NoData";
}

return arg7.proceed(v0.newBuilder().header("Authorization", v4).header("AchievementIds", v2).build());
}
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

package com.cq.jdcover.app;

import bc0;
import di;
import ei;
import fi;
import vh;

public class b {
public b() {
super();
}

public static vh provideDemoRepository() {
vh v0 = vh.getInstance();
if(v0 != null) {
return v0;
}

return vh.getInstance(di.getInstance(bc0.getInstance().create(ei.class)), fi.getInstance());
}
}


provideDemoRepository() 应该是一个运行时的实例,去找函数定义的时候没有找到实现,就直接使用RPC的方式来调用

RPC 调用实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//hook.js
function sign() {
let sign;
Java.perform(function () {
const b = Java.use("com.cq.jdcover.app.b");
sign = b.provideDemoRepository().getToken();
})

return sign
}

rpc.exports = {
sign: sign
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# server.py

import frida
from flask import Flask

hook = open('hook.js', 'r', encoding='utf-8').read()


def on_message(message, data):
if message['type'] == 'send':
print(f"send message:{message['payload']}")
elif message['type'] == 'error':
print(message['stack'])


deviceManage = frida.get_usb_device()
front_app = deviceManage.get_frontmost_application()

print("===正在运行的应用为:", front_app)
process = deviceManage.attach(front_app.pid)

script = process.create_script(hook)
script.on('message', on_message)
script.load()

app = Flask(__name__)


@app.route('/sign', methods=['get']) # data解密
def sign():
_sign = script.exports.sign()

return {'token': _sign}


if __name__ == '__main__':
app.run()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# -*- coding:utf-8 -*-

import base64

from Crypto.Cipher import AES


class EncryptDate:
def __init__(self, key):
self.key = key # 初始化密钥
self.length = 16 # 初始化数据块大小
self.aes = AES.new(self.key, AES.MODE_ECB) # 初始化AES,ECB模式的实例
self.unpad = lambda date: date[0:-ord(date[-1])] # 截断函数,去除填充的字符

def pad(self, text):
"""
#填充函数,使被加密数据的字节码长度是block_size的整数倍
"""
count = len(text.encode('utf-8'))
add = self.length - (count % self.length)
entext = text + (chr(add) * add)
return entext

def encrypt(self, encrData): # 加密函数
a = self.pad(encrData)
res = self.aes.encrypt(a.encode("utf-8"))
msg = str(base64.b64encode(res), encoding="utf8")
return msg

def decrypt(self, decrData): # 解密函数
res = base64.decodebytes(decrData.encode("utf-8"))
msg = self.aes.decrypt(res).decode("utf-8")
return self.unpad(msg)


def enc_data(text):
aes_key = "SFV2fb8D09jreH2Xdf2M0FGk5Di2DX5O"
eg = EncryptDate(aes_key.encode("utf-8"))
encrypt_data = eg.encrypt(text)
return encrypt_data


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# client.py
from datetime import datetime

import requests

from enc import enc_data

url = 'http://127.0.0.1:5000/sign'

t = datetime.now()
ts = t.timestamp()
str_t = str(t)
day, time_ = str_t.split()
day = day.replace('-', '')
time_ = time_.split('.')[0].replace(":", '')
daytime_ = day + time_
timespan = int(ts * 1000)

res = requests.get(url)
token = res.json()['token']

headers = {
'channel': 'jiandan',
'pkgname': 'com.cq.jdcover',
'encryption': '1',
'timezoom': 'GMT+08:00',
'timespan': str(timespan),
'authorization': token,
'achievementids': 'NoData',
'content-type': 'application/json; charset=UTF-8',
'user-agent': 'okhttp/3.10.0',
}

para = '{"categoryIds":[],"hallType":2.0,"hideTask":false,"isFilterSubmited":false,"pageIndex":1.0,"pageSize":20.0,"treatmentType":1.0,"ClientTime":' + f'{str(daytime_)}' + '}'
enc_data = enc_data(para)

res = requests.post('https://www.cqsslhj.com/Tasks/NewTask/GetHallTasks', headers=headers,
data=enc_data)

print(res.text)

发请求的时候注意下timespan 和daytime 的格式就可以了 这个APP就分析到这里了


某赚钱APP逆向
https://kingjem.github.io/2022/05/25/某赚钱APP逆向/
作者
Ruhai
发布于
2022年5月25日
许可协议