如何用fiddler抓包参考:https://www.cnblogs.com/JKding233/p/16649489.html
先介绍一些前置知识点和环境配置
Frida 安装和运行
pip3 install frida frida-tools
查看模拟器版本:
adb shell
getprop ro.product.cpu.abi
>>> x86_64
下载对应 frida-server:(小于或等于本地frida版本)
解压到adb文件夹,推到手机里面:
adb push frida-server-15.1.10-android-x86_64 /data/local/tmp/frida-server
启动 frida:
adb shell
su
cd /data/local/tmp
chmod 755 frida-server
./frida-server
配置接口转发,注意每次启动都需要配置:
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
开启另一个cmd,运行:frida-ps -R
,连接成功:
Frida Python 脚本
hook 脚本示例
import frida
import sys
device = frida.get_remote_device() # adb devices 查看
process = device.attach("猿人学2022") # 进程名
src = """
function main(){
console.log("脚本加载成功");
Java.perform(function() {
var clazz = Java.use('com.yuanrenxue.match2022.security.Sign');
clazz.sign.implementation = function() {
console.log('进入函数内部');
console.log("参数为:", arguments)
console.log("结果为:",clazz.sign.apply(this, arguments));
return clazz.sign.apply(this, arguments);
}
});
}
setImmediate(main)
"""
def on_message(message, data):
if message["type"] == "send":
print(f"send >>> {message['payload']}")
else:
print(message)
script = process.create_script(src)
script.on("message", on_message)
script.load()
sys.stdin.read()
- 一直没有输出的话,先退出重新启动app,再运行脚本
- 一直进入不了函数内部的话,可以使用 rpc 的方式
- 不需要 on_message 也可以打印
rpc 脚本示例
import frida
device = frida.get_remote_device() # adb devices 查看
process = device.attach("猿人学2022")
rpc_code = """
var result
function main() {
console.log("Script loded successfully")
console.log()
Java.perform(function () {
// 主要是修改这里面
var Sign = Java.use('o00OO.OooO00o')
console.log("调用函数成功")
// var sign = Sign.$new()
// console.log("创建实例成功")
result = Sign.OooO00o(-274893829204)
console.log(result)
})
return result
}
rpc.exports = {
main: main
}
"""
def on_message(message, data):
if message["type"] == "send":
print(f"send >>> {message['payload']}")
# bytes2str(message['payload'])
else:
print(message)
script = process.create_script(rpc_code)
script.on("message", on_message)
script.load()
result = script.exports.main()
print(result)
- 看情况是否要创建实例,
public static
开头的静态函数直接调用就行
题目分析:
登录APP后,显示要强制更新,
http://download.python-spider.com/yuanrenxuem107.apk
那就先把 apk 文件下载到本地,方便后面分析,抓包看一下分析请求
只有 sign 是变动的,page、t、token 都是一些普通的参数,直接整个文件拖进 jadx 分析,搜索关键词 第一题
很快定位到 com.yuanrenxue.match2022.fragment.challenge.ChallengeOneFragment
这个类,直接搜索 sign
可以看出,参数主要是 sb 转 string 再转 bytes 传入的,先用 frida hook 一下sign函数的参数和结果:
把参数 send 出来,转成字符串,python bytes 列表转字符串方法如下
def bytes2str(ascii_lst):
string = ''.join(map(chr, ascii_lst))
print(string)
var clazz = Java.use('com.yuanrenxue.match2022.security.Sign');
clazz.sign.implementation = function() {
console.log('进入函数内部');
console.log("参数为:", arguments[0]);
send(arguments[0]);
console.log("结果为:",clazz.sign.apply(this, arguments));
return clazz.sign.apply(this, arguments);
}
on_message 函数改一下:
def on_message(message, data):
if message["type"] == "send":
print(f"send >>> {message['payload']}")
bytes2str(message['payload'])
else:
print(f"log >>> {message}")
打印结果:
可见参数为:page=x 加 时间戳
进入 sign 函数内部,只引入了一个依赖,可以考虑扣 java代码,觉得麻烦的话就直接用rpc,这里两个方案的实现都写一下
方案一 调用 java 代码
既然要扣代码,就得先解决第三方依赖的问题,看一下调用点:
传入的是一个长整型数字,结合 String.format() 函数的使用 ,这里很可能是一个固定值,用于字符转换
本来想用 hook,结果死活进不去函数,无奈改用rpc调用的方式,脚本我已经放在上面了,计算结果为:%02x%02x%02x%02x
简单介绍一下,在 java中,
String.format("%02X", 11)
就是 11 转 十六进制的意思,不足的部分补0,所以输出:0B
同理
String.format("%03X", 11)
输出 00B
抠出来的java代码如下:
import java.util.ArrayList;
/* compiled from: proguard-dict.txt */
/* loaded from: classes2.dex */
public class Sign {
/* renamed from: A */
private static final int f276A = 1732584193;
/* renamed from: B */
private static final int f277B = -271733879;
/* renamed from: C */
private static final int f278C = -1732584194;
/* renamed from: D */
private static final int f279D = 271733878;
/* renamed from: f */
private static int m21f(int i, int i2, int i3) {
return ((~i) & i3) | (i2 & i);
}
/* renamed from: ff */
private static int m20ff(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + m21f(i2, i3, i4) + i5, i6);
}
/* renamed from: g */
private static int m19g(int i, int i2, int i3) {
return (i & i3) | (i & i2) | (i2 & i3);
}
/* renamed from: gg */
private static int m18gg(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + m19g(i2, i3, i4) + i5 + 1518565785, i6);
}
/* renamed from: h */
private static int m17h(int i, int i2, int i3) {
return (i ^ i2) ^ i3;
}
/* renamed from: hh */
private static int m16hh(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + m17h(i2, i3, i4) + i5 + 1859775393, i6);
}
private ArrayList<Integer> padding(byte[] bArr) {
long length = bArr.length * 8;
ArrayList<Integer> arrayList = new ArrayList<>();
for (byte b : bArr) {
arrayList.add(Integer.valueOf(b));
}
arrayList.add(128);
while (((arrayList.size() * 8) + 64) % 512 != 0) {
arrayList.add(0);
}
for (int i = 0; i < 8; i++) {
arrayList.add(Integer.valueOf((int) ((length >>> (i * 8)) & 255)));
}
return arrayList;
}
private static int rotateLeft(int i, int i2) {
return (i >>> (32 - i2)) | (i << i2);
}
public String sign(byte[] bArr) {
ArrayList<Integer> padding = padding(bArr);
int i = f276A;
int i2 = f277B;
int i3 = f278C;
int i4 = f279D;
for (int i5 = 0; i5 < padding.size() / 64; i5++) {
int[] iArr = new int[16];
for (int i6 = 0; i6 < 16; i6++) {
int i7 = (i5 * 64) + (i6 * 4);
iArr[i6] = (padding.get(i7 + 3).intValue() << 24) | padding.get(i7).intValue() | (padding.get(i7 + 1).intValue() << 8) | (padding.get(i7 + 2).intValue() << 16);
}
int[] iArr2 = {0, 4, 8, 12};
int i8 = i;
int i9 = i2;
int i10 = i3;
int i11 = i4;
int i12 = 0;
while (i12 < 4) {
int i13 = iArr2[i12];
i8 = m20ff(i8, i9, i10, i11, iArr[i13], 3);
int m20ff = m20ff(i11, i8, i9, i10, iArr[i13 + 1], 7);
i10 = m20ff(i10, m20ff, i8, i9, iArr[i13 + 2], 11);
i9 = m20ff(i9, i10, m20ff, i8, iArr[i13 + 3], 19);
i12++;
i11 = m20ff;
}
int[] iArr3 = {0, 1, 2, 3};
int i14 = i8;
int i15 = i11;
for (int i16 = 0; i16 < 4; i16++) {
int i17 = iArr3[i16];
i14 = m18gg(i14, i9, i10, i15, iArr[i17], 3);
i15 = m18gg(i15, i14, i9, i10, iArr[i17 + 4], 5);
i10 = m18gg(i10, i15, i14, i9, iArr[i17 + 8], 9);
i9 = m18gg(i9, i10, i15, i14, iArr[i17 + 12], 13);
}
int[] iArr4 = {0, 2, 1, 3};
int i18 = i14;
int i19 = 0;
while (i19 < 4) {
int i20 = iArr4[i19];
int m16hh = m16hh(i18, i9, i10, i15, iArr[i20], 3);
i15 = m16hh(i15, m16hh, i9, i10, iArr[i20 + 8], 9);
i10 = m16hh(i10, i15, m16hh, i9, iArr[i20 + 4], 11);
i9 = m16hh(i9, i10, i15, m16hh, iArr[i20 + 12], 15);
i19++;
i18 = m16hh;
}
i += i18;
i2 += i9;
i3 += i10;
i4 += i15;
}
return String.format("%02x%02x%02x%02x", Integer.valueOf(i), Integer.valueOf(i2), Integer.valueOf(i3), Integer.valueOf(i4));
}
}
有一个地方需要注意,int length = bArr.length * 8
替换为 long length = bArr.length * 8
jpype的使用,上面有介绍,可以比对 hook结果进行验证
我这里就直接上爬虫脚本了
# -*- coding: utf-8 -*-
# time: 2022/10/1 13:44
# author: Chen
import requests
import jpype
jvm_path = jpype.getDefaultJVMPath()
jpype.startJVM(jvm_path, "-ea", "-Dfile.encoding=utf-8", convertStrings=True)
JDClass = jpype.JClass("Sign")
javaInstance = JDClass()
# jpype.shutdownJVM()
results = []
for page in range(1, 101):
token_url = "https://appmatch.yuanrenxue.com/time?token=MuuqC7kHE79hZhN3L3Ip3u2e1jp%252BgLY1V2S1%252FKWTbpYZMqchIHNxll%252BE06%252BEuHQ5"
resp_json = requests.get(token_url).json()
ts = resp_json["time"]
raw_sign = f"page={page}{ts}"
url = "https://appmatch.yuanrenxue.com/app1"
data = {
"page": page,
"sign": javaInstance.sign(raw_sign.encode()),
"t": ts,
"token": "MuuqC7kHE79hZhN3L3Ip3u2e1jp gLY1V2S1/KWTbpYZMqchIHNxll E06 EuHQ5"
}
resp_json = requests.post(url, data=data).json()
num_lst = [int(d['value'].strip()) for d in resp_json["data"]]
print(f"第 {page} 页:", num_lst)
results.extend(num_lst)
print("100页面总和为:", sum(results))
运行结果:
方案二 Frida rpc
参考:https://www.cnblogs.com/JKding233/p/16649489.html 原理就不多介绍了,主要是把sign函数暴露出来
# -*- coding: utf-8 -*-
# time: 2022/10/1 14:29
# author: Chen
import frida
import requests
rpc_code2 = """
var bArr
var result
function main(bArr) {
console.log("Script loded successfully")
console.log(bArr)
Java.perform(function () {
var Sign = Java.use('com.yuanrenxue.match2022.security.Sign')
console.log("调用函数成功")
var instance = Sign.$new()
console.log("创建实例成功")
result = instance.sign(bArr)
console.log(result)
})
return result
}
rpc.exports = {
main: main
}
"""
device = frida.get_remote_device() # adb devices 查看
process = device.attach("猿人学2022")
script = process.create_script(rpc_code2)
script.load()
results = []
for page in range(1, 101):
token_url = "https://appmatch.yuanrenxue.com/time?token=MuuqC7kHE79hZhN3L3Ip3u2e1jp%252BgLY1V2S1%252FKWTbpYZMqchIHNxll%252BE06%252BEuHQ5"
resp_json = requests.get(token_url).json()
ts = resp_json["time"]
raw_sign = f"page={page}{ts}"
bArr = [ord(c) for c in raw_sign]
url = "https://appmatch.yuanrenxue.com/app1"
data = {
"page": page,
"sign": script.exports.main(bArr),
"t": ts,
"token": "MuuqC7kHE79hZhN3L3Ip3u2e1jp gLY1V2S1/KWTbpYZMqchIHNxll E06 EuHQ5"
}
resp_json = requests.post(url, data=data).json()
num_lst = [int(d['value'].strip()) for d in resp_json["data"]]
print(f"第 {page} 页:", num_lst)
results.extend(num_lst)
print("100页面总和为:", sum(results))
挑战成功:
整个逆向过程花费的时间挺多的,不过总算搞懂了 frida的使用,以后要逆向其他的app就方便多了 😁✨👌