拿到一个要 逆向的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
| 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
|
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']) 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
|
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) 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
| 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就分析到这里了