从脱壳到关键代码定位,再从算法还原和协议复现,走了很多弯路,虽然还差最后的总脚本,这里整理一下全部流程.

其中代码定位是对于这种app来说十分困难的事,这里虽然有一些方法和类名混淆,不过好在不多,难道对于大型app的混淆的关键代码定位只能挨个猜测hook吗,这里还有待学习

算法还原

1.数字签名的还原

2.oct判断的还原

3.时间验证,signtime和endtime的还原

协议复现

1.登录验证的代码定位

2.登录时的加密的还原(从抓包的数据逆向的角度获取了传输的信息)

3.发包的还原

脱壳

如果直接把程序拖进jadx里面,会发现有360壳的痕迹, 用查壳工具也证实了猜想。下面就是去dump下壳

脱壳的问题

1.目前我在尝试直接脱壳的时候发现直接闪退了程序,但是我在使用frida-dexdump的时候发现在尝试嘟嘟牛在线可以很好的dump下来。现在想的话,联想后面的开发者模式检测,应该是存在反USB调试的功能

解决方案:

使用的是Termux http://www.topv1.com/termuxdoc/getting_started/来进行局域网连接frida,再使用dex_dump,具体流程如下

对开发者模式的检测

在我开起自由跑的时候直接给我显示关闭开发者模式

为什么要使用Termux去使用这个,而不是USB连而调试,因为

1.自由跑界面有开发者检测

2.dump文件的时候程序直接闪退了,然后我试试这个

在手机执行以下命令

su //在magisk中开起超级用户模式
cd /data/local/tmp
./fsarm64 -l 0.0.0.0:xxxx //xxxx是端口号

然后电脑端实现

frida-端口号 -d -F -o 保存地址

直接把这个文件夹一起拖入jadx,这里没想到真的找到了这个

至于为什么要首先找这个native方法,因为我在关键代码定位中,配合常见发包的类,去hook的时候找到了数字签名的加密,经过的就是这个hs方法

so层dump

打开这个so发现很多都只是数据,根本没有编译出来,这里看学长笔记

https://github.com/lasting-yang/frida_dump

发现是混淆(或者可以说是没有被成功加载出来)而且可以用这个直接dump,开始我不行,

so层dump的问题

我直接使用下面命令会出现下面的错误

可能的原因

1.这个so文件只有进入自由跑才行,而我需要USB调试就必须开发者模式,开发者模式就进不去那个自由跑

2.说是开起开发者模式,那可能就是根本不需要i进入自由跑,可是这里如果不进入自由跑的话,那该怎么弄,直接在主页面运行代码会出现错误(没有被加载)

python dump_so.py libsportaction.so
Traceback (most recent call last):
File "dump_so.py", line 31, in <module>
device = frida.get_device_manager().get_device("192.168.31.73:2333")
File "D:\pyenv\pyenv-win-master\pyenv-win\versions\3.8.10\lib\site-packages\frida\core.py", line 1236, in get_device
return self.get_device_matching(lambda d: d.id == id, timeout)
File "D:\pyenv\pyenv-win-master\pyenv-win\versions\3.8.10\lib\site-packages\frida\core.py", line 86, in wrapper
return f(*args, **kwargs)
File "D:\pyenv\pyenv-win-master\pyenv-win\versions\3.8.10\lib\site-packages\frida\core.py", line 1252, in get_device_matching
return Device(self._impl.get_device_matching(lambda d: predicate(Device(d)), raw_timeout))
frida.InvalidArgumentError: device not found

D:\下载\frida_dump-master\frida_dump-master>

AI说把py代码改成这个

device = frida.get_device_manager().get_device("192.168.31.73:2333")

发现是frida没有连接成功?,可是我的这个下面是连接成功了的啊

D:\下载\frida_dump-master\frida_dump-master>frida -H 192.168.31.73:2333 -n com.ledreamer.zz
____
/ _ | Frida 16.6.6 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . . Connected to 192.168.31.73:2333 (id=socket@192.168.31.73:2333)

[Remote::com.ledreamer.zz ]->

然后查阅发现说可以调用什么Frida RPC(Remote Procedure Call)接口,让 dump_so.js 脚本 dump(导出)指定的 SO 文件。

frida -H 192.168.31.73:2333 -n com.ledreamer.zz -l dump_so.js
rpc.exports.dumpmodule("libsportaction.so")

这样虽然在终端显示了so层的16进制代码,但是不完善,不好看

然后在交互命令窗口返回了这个,我直接全部复制下来,然后去改成.so

文件,还是不行,哭了😭😭😭😭😭

难道是终端能够显示的大小有限?,这里我想到的方法是保存在手机上然后去pull出来,这里卡了很久,不过可以这样做

因为要注意权限的问题

su
chmod 777 /cache/
chmod 777 /data/local/tmp
cd /data/local/tmp
./fsarm64 -l 0.0.0.0:3333

修改dump_so.js 文件如下

'use strict';

rpc.exports = {
findmodule: function (so_name) {
var libso = Process.findModuleByName(so_name);
if (!libso) {
return null;
}
return {
name: libso.name,
base: libso.base.toString(),
size: libso.size
};
},

dumpmodule: function (so_name) {
var libso = Process.findModuleByName(so_name);
if (!libso) {
console.log("[!] 模块未找到:" + so_name);
return -1;
}

console.log("[*] 模块基址: " + libso.base);
console.log("[*] 模块大小: " + libso.size);

try {
// 解除内存保护,防止读不到数据
Memory.protect(ptr(libso.base), libso.size, 'rwx');

var module_data = ptr(libso.base).readByteArray(libso.size);

// 保存到 /sdcard/
var file_path = "/cache/" + so_name + ".dump.so";
var file = new File(file_path, "wb");
file.write(module_data);
file.flush();
file.close();

console.log("[✔] " + so_name + " dump 完成,文件保存到: " + file_path);
return file_path;
} catch (e) {
console.log("[!] dump 失败: " + e.message);
return -1;
}
},

allmodule: function () {
var modules = Process.enumerateModules();
return modules.map(mod => ({
name: mod.name,
base: mod.base.toString(),
size: mod.size
}));
},

arch: function () {
return Process.arch;
}
};

python文件如下

import sys
import frida
import sys
import os



def fix_so(arch, origin_so_name, so_name, base, size):
if arch == "arm":
os.system("adb push android/SoFixer32 /data/local/tmp/SoFixer")
elif arch == "arm64":
os.system("adb push android/SoFixer64 /data/local/tmp/SoFixer")
os.system("adb shell chmod +x /data/local/tmp/SoFixer")
os.system("adb push " + so_name + " /data/local/tmp/" + so_name)
print("adb shell /data/local/tmp/SoFixer -m " + base + " -s /data/local/tmp/" + so_name + " -o /data/local/tmp/" + so_name + ".fix.so")
os.system("adb shell /data/local/tmp/SoFixer -m " + base + " -s /data/local/tmp/" + so_name + " -o /data/local/tmp/" + so_name + ".fix.so")
os.system("adb pull /data/local/tmp/" + so_name + ".fix.so " + origin_so_name + "_" + base + "_" + str(size) + "_fix.so")
os.system("adb shell rm /data/local/tmp/" + so_name)
os.system("adb shell rm /data/local/tmp/" + so_name + ".fix.so")
os.system("adb shell rm /data/local/tmp/SoFixer")

return origin_so_name + "_" + base + "_" + str(size) + "_fix.so"


def read_frida_js_source():
with open("dump_so.js", "r") as f:
return f.read()

def on_message(message, data):
pass

if __name__ == "__main__":
device = frida.get_device_manager().get_device("192.168.31.73:2333")
pid = device.get_frontmost_application().pid
session: frida.core.Session = device.attach(pid)
script = session.create_script(read_frida_js_source())
script.on('message', on_message)
script.load()

if len(sys.argv) < 2:
allmodule = script.exports.allmodule()
for module in allmodule:
print(module["name"])
else:
origin_so_name = sys.argv[1]
module_info = script.exports.findmodule(origin_so_name)
print(module_info)
base = module_info["base"]
size = module_info["size"]
# module_buffer = script.exports.dumpmodule(origin_so_name)
module_buffer = script.exports_sync.findmodule(origin_so_name)
if module_buffer != -1:
dump_so_name = origin_so_name + ".dump.so"
with open(dump_so_name, "wb") as f:
f.write(module_buffer)
f.close()
arch = script.exports.arch()
fix_so_name = fix_so(arch, origin_so_name, dump_so_name, base, size)

print(fix_so_name)
os.remove(dump_so_name)



拖进ida里面发现有大量代码虽然被dump下来了,可还是没有成功解析出来,有大量的代码都被混淆了,而且里面的hs方法也没有找到,这个时候,我想的是hook掉它的检测开发者模式的代码。然后去指向USB调试,再试试dump,不过也有可能dump的时候我的so文件损坏了,按理说,这种远程frida的效果应该一样,不过可能由于我修改了这个js和py代码让他保存下来,可能会有损坏

Java.perform(function() {
console.log("Hello World!");
var SettingsSecure = Java.use("android.provider.Settings$Secure");
var SettingsGlobal = Java.use("android.provider.Settings$Global");
if(!SettingsGlobal) {
console.log("Settings not set.");
}
// Hook Secure.getInt
SettingsSecure.getInt.overload("android.content.ContentResolver", "java.lang.String", "int").implementation = function(contentResolver, key, defaultValue) {
if (key == "adb_enabled" || key == "development_settings_enabled") {
console.log("Bypassing detection for " + key);
return 0; // 返回 0 关闭开发者模式
}
return this.getInt(contentResolver, key, defaultValue);
};

// Hook Global.getInt
SettingsGlobal.getInt.overload("android.content.ContentResolver", "java.lang.String", "int").implementation = function(contentResolver, key, defaultValue) {
if (key == "adb_enabled" || key == "development_settings_enabled") {
console.log("Bypassing detection for " + key);
return 0;
}
return this.getInt(contentResolver, key, defaultValue);
};
});

然后再去进行自由跑,发现成功在开发者模式下进入了自由跑界面!

这个时候再去使用命令

python dump_so.py libsportaction.so

并且这个so文件修复的,比我用termux修复的要好看太多了 ,也成功找到了hs方法!

不过看不懂,悲

代码定位和hook加密

通过寻找字符串和hook的方式,找到了发包的那里的逻辑,但是这里有一点问题,就是一直抓不到hs方法的输入和输出,难道是距离太近了没有成功上传

Java.perform(function () {
console.log("[*] Frida Hook Started...");

// ====================== 1. 绕过开发者模式检测 ======================
var SettingsSecure = Java.use("android.provider.Settings$Secure");
var SettingsGlobal = Java.use("android.provider.Settings$Global");

// Hook Secure.getInt
SettingsSecure.getInt.overload("android.content.ContentResolver", "java.lang.String", "int").implementation = function (cr, key, def) {
if (key === "adb_enabled" || key === "development_settings_enabled") {
console.log(`[+] Secure.getInt Bypassed: ${key}`);
return 0; // 强制返回0,模拟关闭开发者模式
}
return this.getInt(cr, key, def);
};

// Hook Global.getInt
SettingsGlobal.getInt.overload("android.content.ContentResolver", "java.lang.String", "int").implementation = function (cr, key, def) {
if (key === "adb_enabled" || key === "development_settings_enabled") {
console.log(`[+] Global.getInt Bypassed: ${key}`);
return 0;
}
return this.getInt(cr, key, def);
};
console.log("[✔] 开发者模式绕过完成");


// ---------------------- 2.1 Java层Hook ----------------------
var Rengis = Java.use("cn.legym.ai.util.Rengis");
Rengis.hs.overload('java.lang.String').implementation = function (input) {
console.log(`\n======== Java层捕获调用 ========`);
console.log(`[+] hs() 原始输入: ${input}`);

// 调用原方法获取加密结果
var result = this.hs(input);
console.log(`[+] hs() 加密结果: ${result}`);

return result; // 可选择修改返回值
};
console.log("[✔] Java层Hook完成");

// ---------------------- 2.2 Native层Hook ----------------------
var targetLib = "libsportaction.so"; // 注意实际可能带lib前缀
var isHooked = false;

// 加载检测策略:混合轮询 + dlopen监听
var checkInterval = setInterval(function () {
if (Module.findBaseAddress(targetLib)) {
console.log(`[+] ${targetLib} 已加载,开始Hook...`);
clearInterval(checkInterval);
hookNative();
}
}, 500);

function hookNative() {
if (isHooked) return;
isHooked = true;

var funcName = "Java_cn_legym_ai_util_Rengis_hs";
var funcPtr = Module.findExportByName(targetLib, funcName);

if (!funcPtr) {
console.log("[!] 找不到Native函数");
return;
}

console.log(`[+] Native函数地址: ${funcPtr}`);

Interceptor.attach(funcPtr, {
onEnter: function (args) {
// JNI参数顺序:JNIEnv*, jclass, jstring(静态方法)
var jniEnv = args[0];
var jstrArg = args[2];

// 转换jstring为字符串
var input = Java.vm.getEnv().getStringUtfChars(jstrArg).readCString();
console.log(`\n======== Native层捕获调用 ========`);
console.log(`[+] Native输入: ${input}`);
},
onLeave: function (retval) {
// 读取返回值jstring
var jstrRet = retval;
var output = Java.vm.getEnv().getStringUtfChars(jstrRet).readCString();
console.log(`[+] Native输出: ${output}`);
}
});
console.log("[✔] Native层Hook完成");
}
});

使用一下模拟位置的app来跑几步,用不了不知道为什么,这里使用了标枪定位和fakelocation都行不通,无法正确的去执行我们要的模拟定位,这里很神奇

我甚至hook了这个ismock虚拟定位

var Location = Java.use("android.location.Location");
Location.isFromMockProvider.implementation = function () {
console.log("[+] Bypassing Fake GPS Detection...");
return false; // 让应用认为 Fake GPS 是真实 GPS
};

// 兼容 API 18 及以上的检测方法
try {
var Api18Impl = Java.use("androidx.core.location.LocationCompat"); // 需要替换为实际包名
Api18Impl.isMock.implementation = function (location) {
console.log("[+] Bypassing Api18Impl.isMock()");
return false;
};
} catch (e) {
console.log("[!] Api18Impl hook 失败,可能包名不同或 API 低于 18");
}
console.log("[✔] 模拟位置检测绕过完成");

感觉是有检测代码的,但是hook了还是不行,还是不行,崩了,重启了几次

算了,明天抱着电脑去外面走两圈试试。

1.模拟定位

2.分析抓到的包和数据

3.猜测加密方式(aes+sha1感觉,用插件findcript)

4.如何手动实现发包

5.整理代码

在不能用虚拟定位的前提下,我usb调试,绕过开发者检测,去坐车实验了一下,果真是我跑少了,所以一直没有数据,这次跑得多,有了数据

======== RunningViewModel Hook ========
参数列表:
info=RunRealTimeInfo(runType=0, millisUntilFinishTime=6246816, averagePace=637581, stepMinute=0, gpsTotalDistance=1.4950, realTimePace=
1234568, stepCount=0, runtimeMillis=953184, realTimeSpeed=0.81, isFence=true, invalidDistance=0, effectiveDistance=1.4950, signPointIndexArray=[],
signPointIdArray=[], signPointIdOriginArray=[], latLngList=[lat/lng: (30.831617,104.188117), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,10
4.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng:
(30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.1881
32), lat/lng: (30.83165,104.188132), lat/lng: (30.83165,104.188132), lat/lng: (30.831489,104.188068), lat/lng: (30.831489,104.188068), lat/lng: (30
.831585,104.188348), lat/lng: (30.831465,104.188438), lat/lng: (30.831465,104.188438), lat/lng: (30.831359246712005,104.18848643366067), lat/lng: (
30.83139462473545,104.18848180365103), lat/lng: (30.83136679674631,104.1884721296779), lat/lng: (30.83137274046375,104.18849958072377), lat/lng: (3
0.831373013157425,104.18849817326101), lat/lng: (30.831373493261918,104.18849826312271), lat/lng: (30.83137890343733,104.18849983056934), lat/lng:
(30.831384484609252,104.18850089891924), lat/lng: (30.831384894458367,104.18850110856279), lat/lng: (30.83138443420263,104.1885011085409), lat/lng:
(30.83138454386692,104.18850132815227), lat/lng: (30.83138507325961,104.18850182728234), lat/lng: (30.831384931522173,104.18850274562861), lat/lng
: (30.83138954031491,104.18850483210633), lat/lng: (30.831373673465272,104.18852035351618), lat/lng: (30.831354783948573,104.18851427351886), lat/l
ng: (30.831330104809435,104.1885006667446), lat/lng: (30.831301738411874,104.18850655483335), lat/lng: (30.831267479934073,104.18852839403887), lat
/lng: (30.83122790816084,104.18853381243814), lat/lng: (30.83119371925286,104.18853930096854), lat/lng: (30.831167574840606,104.18853369976549), la
t/lng: (30.83112445993781,104.18852868669971), lat/lng: (30.83104252013225,104.1885532088304), lat/lng: (30.83098232862255,104.18855767795083), lat
/lng: (30.830916608645627,104.18854852123486), lat/lng: (30.830855998896045,104.18854081216806), lat/lng: (30.830788677462433,104.18852643473882),
lat/lng: (30.83074031566108,104.1885125573191), lat/lng: (30.830676059212223,104.18850190336362), lat/lng: (30.830619399242778,104.18847339180233),
lat/lng: (30.830546041445913,104.18843855081298), lat/lng: (30.830475173580336,104.18841558864024), lat/lng: (30.830424144315042,104.1883884349433
9), lat/lng: (30.83034944122262,104.18837825964876), lat/lng: (30.830282634634557,104.18834478656032), lat/lng: (30.830222880877592,104.18832851293
489), lat/lng: (30.83014654203383,104.1883154926798), lat/lng: (30.830077425217876,104.18829804073108), lat/lng: (30.83002286457487,104.18827611751
242), lat/lng: (30.82996981224486,104.18826112192508), lat/lng: (30.829913031581107,104.18822747967114), lat/lng: (30.829838317443976,104.188190263
00321), lat/lng: (30.829759143387296,104.18815716872881), lat/lng: (30.82968061588803,104.18810961051477), lat/lng: (30.829627901559135,104.1880792
7257785), lat/lng: (30.829572524289777,104.18802766280821), lat/lng: (30.829532707472403,104.18798610571646), lat/lng: (30.82948109644847,104.18793
775032886), lat/lng: (30.829437933304252,104.18790419870533), lat/lng: (30.829443372884132,104.18788954536295), lat/lng: (30.829394227016095,104.18
788325435504), lat/lng: (30.829365101853504,104.18785512365544), lat/lng: (30.829327789776503,104.18783382027048), lat/lng: (30.829342462423316,104
.18784236556859), lat/lng: (30.82929559336395,104.1878219900592), lat/lng: (30.82926521636663,104.18782206847011), lat/lng: (30.829227240269255,104
.18781381154382), lat/lng: (30.829191128665254,104.18780364814414), lat/lng: (30.829135479658923,104.18778607716999), lat/lng: (30.829077537388496,
104.18776409404668), lat/lng: (30.829025709863448,104.18775239267812), lat/lng: (30.828929446928576,104.18773008831744), lat/lng: (30.8288587253399
63,104.18769832224997), lat/lng: (30.828785693096993,104.18767723681263), lat/lng: (30.82871673123734,104.18764616958946), lat/lng: (30.82864720460
8245,104.18763408808123), lat/lng: (30.82857629246637,104.1876246217881), lat/lng: (30.828503742001175,104.18760271787134), lat/lng: (30.8284375390
80642,104.18758410831492), lat/lng: (30.828367923185606,104.18756609751901), lat/lng: (30.828299389810176,104.18755253873839), lat/lng: (30.8282287
56370642,104.18753287089424), lat/lng: (30.828165943736337,104.18751514991082), lat/lng: (30.82809849336774,104.18750027357098), lat/lng: (30.82803
030056764,104.18749201524102), lat/lng: (30.82796612760906,104.18747406461692), lat/lng: (30.827899406054584,104.1874545666718), lat/lng: (30.82783
912389786,104.18743186482956), lat/lng: (30.8277725131635,104.18741745769722), lat/lng: (30.827703291163512,104.18739195051167), lat/lng: (30.82763
3588600914,104.18738861322318), lat/lng: (30.827562716007108,104.18737393638324), lat/lng: (30.827490708687797,104.18735051530598), lat/lng: (30.82
743132825844,104.18734365488329), lat/lng: (30.827379184382814,104.18732989728166), lat/lng: (30.827335835239335,104.18728322964286), lat/lng: (30.
82728066121938,104.18725214326716), lat/lng: (30.827231873355665,104.18722511985335), lat/lng: (30.82718316853455,104.18719095937458), lat/lng: (30
.827180680041128,104.18716190187224), lat/lng: (30.82714627259262,104.18715552178625), lat/lng: (30.827131293362307,104.18715054009428), lat/lng: (
30.827114434636528,104.18712822969982), lat/lng: (30.827074874900745,104.18712153993184), lat/lng: (30.827071362897936,104.18709410945807), lat/lng
: (30.827060158552868,104.18707112057835), lat/lng: (30.827036464664186,104.1870660486397), lat/lng: (30.82701555406115,104.18705452851977), lat/ln
g: (30.82698678996397,104.1870370488286), lat/lng: (30.826961823574575,104.18701100484738), lat/lng: (30.82692031506443,104.18699207718329), lat/ln
g: (30.82687538469703,104.18697861944207), lat/lng: (30.82683311482011,104.18697119089684), lat/lng: (30.826807323249138,104.18694226212946), lat/l
ng: (30.826785946859335,104.18688440606316), lat/lng: (30.82677100161162,104.18684434804133), lat/lng: (30.82677140290228,104.18677795857651), lat/
lng: (30.826764164561105,104.18672003342627), lat/lng: (30.826769385158425,104.18665005082143), lat/lng: (30.826769214888696,104.18656797999725), l
at/lng: (30.826760008092602,104.18648677724404), lat/lng: (30.82675609426467,104.18639464471697), lat/lng: (30.826748676692638,104.1863199503635),
lat/lng: (30.826749565542922,104.18624428823541), lat/lng: (30.826747875274325,104.18617285833645), lat/lng: (30.82674291621565,104.1861107313754),
lat/lng: (30.826721861203442,104.18604700660656), lat/lng: (30.826711469518024,104.18596286965048), lat/lng: (30.826682694461514,104.1859077988196
1), lat/lng: (30.8266414638165,104.18585202870663), lat/lng: (30.82659928467093,104.18580608063688), lat/lng: (30.826560796058054,104.1857773313350
6), lat/lng: (30.826513543183697,104.18573190211369), lat/lng: (30.826454116158736,104.18571233504959), lat/lng: (30.826343104426954,104.1856906693
6638), lat/lng: (30.826332366992027,104.18565331715877), lat/lng: (30.826254043230836,104.1856480829954), lat/lng: (30.826200710611054,104.18563886
730671), lat/lng: (30.82613526145014,104.18562411117534), lat/lng: (30.826065274523973,104.18559586950084), lat/lng: (30.825988351476823,104.185579
77527743), lat/lng: (30.82593246629208,104.18554394817114), lat/lng: (30.825884297980764,104.18552305411373), lat/lng: (30.82584534222543,104.18553
07881001), lat/lng: (30.825797183515153,104.18549377355554), lat/lng: (30.82575897245004,104.18548828178635), lat/lng: (30.825723320986302,104.1854
6743824632), lat/lng: (30.82569843924123,104.18546088905072), lat/lng: (30.825674693983697,104.18545659577906), lat/lng: (30.825659856954193,104.18
545050622176), lat/lng: (30.825591626537044,104.1854192801508), lat/lng: (30.825378901128488,104.18538536212844), lat/lng: (30.825324417847714,104.
1853816263726), lat/lng: (30.82526257400522,104.18535420366123), lat/lng: (30.825188553185082,104.18533236015357), lat/lng: (30.82510949564283,104.
18531266247825), lat/lng: (30.82502163438558,104.18528145548252), lat/lng: (30.82497002994467,104.18525745701194), lat/lng: (30.824891656948594,104
.18524094354719), lat/lng: (30.824823776656267,104.18520979741722), lat/lng: (30.824752699779165,104.18519215637686), lat/lng: (30.82467930819832,1
04.18516550176174), lat/lng: (30.82460396139977,104.1851356629025), lat/lng: (30.824543755502056,104.18512076735757), lat/lng: (30.824461471175855,
104.18509294448222), lat/lng: (30.824386104507763,104.1850739158057), lat/lng: (30.824340856321538,104.18505933041493), lat/lng: (30.82429052160639
3,104.18504133105014), lat/lng: (30.824215135365968,104.18503294285053), lat/lng: (30.8241552676479,104.18501371529065), lat/lng: (30.8240876558753
2,104.18499436758576), lat/lng: (30.824024336545282,104.18496955013354), lat/lng: (30.82395477499711,104.1849386335915), lat/lng: (30.8238808164101
63,104.18492109228102), lat/lng: (30.823812258147605,104.18490499857221), lat/lng: (30.823737219786178,104.1848924480476), lat/lng: (30.82367773681
7267,104.1848707750275), lat/lng: (30.82362282712047,104.18485956300836), lat/lng: (30.823541491261995,104.18483833812941), lat/lng: (30.8234863462
9247,104.18481906092526), lat/lng: (30.82342617034986,104.18479337526433), lat/lng: (30.823388804844548,104.18476343851405), lat/lng: (30.823324184
50314,104.1847659607935), lat/lng: (30.823253842168384,104.1847461338893), lat/lng: (30.82321908535964,104.18471769452452), lat/lng: (30.8231691008
5722,104.18469972518534), lat/lng: (30.82311520305476,104.18466609446996), lat/lng: (30.823032178775133,104.18463268197482), lat/lng: (30.822958389
624,104.18458840991562), lat/lng: (30.822903690950774,104.18458757883477), lat/lng: (30.82286030763357,104.18458180738568), lat/lng: (30.8228123883
4528,104.18457249224227), lat/lng: (30.822758073927357,104.18456404519682), lat/lng: (30.82269044629895,104.18457505171266), lat/lng: (30.822629320
280107,104.18461968666021), lat/lng: (30.822565808130193,104.18466151667418), lat/lng: (30.822506415691507,104.18471017434672), lat/lng: (30.822453
480531237,104.1847622560571), lat/lng: (30.822405455419627,104.18481036533123), lat/lng: (30.82237252837357,104.1848966552033), lat/lng: (30.822336
285008692,104.18496368037019), lat/lng: (30.82231091720908,104.18504183567379), lat/lng: (30.822291086705423,104.18511227546256), lat/lng: (30.8222
69914329603,104.18517791404612), lat/lng: (30.822263484145083,104.18524680741261), lat/lng: (30.82225287555027,104.18529727440014), lat/lng: (30.82
2233548595815,104.18533879738703), lat/lng: (30.822219132072462,104.1853543482072), lat/lng: (30.82218851754583,104.18538027924198), lat/lng: (30.8
22191456661354,104.1853598667765), lat/lng: (30.82218766648642,104.18533704840286), lat/lng: (30.822198470751296,104.18531075709552), lat/lng: (30.
82221545131857,104.18531554912394), lat/lng: (30.822216483056984,104.18528770021142), lat/lng: (30.822217721583584,104.18527254804319), lat/lng: (3
0.822219822238832,104.18525649756536), lat/lng: (30.82222368553836,104.18523953883955), lat/lng: (30.82223423417936,104.18521080203223), lat/lng: (
30.822238407014062,104.18519419268888), lat/lng: (30.822240770357332,104.18517676476353), lat/lng: (30.822241790392592,104.18516073421428), lat/lng
: (30.822243559705573,104.1851453225682), lat/lng: (30.822255561507163,104.18508812807488), lat/lng: (30.82225671111179,104.18507237703415), lat/ln
g: (30.822259050144684,104.18505729482528), lat/lng: (30.822264083714526,104.18504055578855), lat/lng: (30.822271070982765,104.18502782948197), lat
/lng: (30.82227830095731,104.18501370575409), lat/lng: (30.822285870781474,104.18499433168402), lat/lng: (30.822290897350157,104.18497596564566), l
at/lng: (30.822292877868886,104.18495997510325), lat/lng: (30.822306244514376,104.18491690484353), lat/lng: (30.822315351679514,104.18490487737114)
, lat/lng: (30.822317924868294,104.18488761919785), lat/lng: (30.822323443574767,104.18485738501012), lat/lng: (30.822323675864375,104.184839917119
15), lat/lng: (30.822343415923488,104.18478063701455), lat/lng: (30.82235915842152,104.18475685149251), lat/lng: (30.822373801857204,104.1847430874
9738), lat/lng: (30.822393342631024,104.18472680836277), lat/lng: (30.822417370738478,104.18470789429071), lat/lng: (30.822430735768872,104.1846983
1254988), lat/lng: (30.822444136019527,104.18469133602021), lat/lng: (30.82245896761237,104.18468406010997), lat/lng: (30.822470418774557,104.18467
600547133), lat/lng: (30.822487144319307,104.18465087250094), lat/lng: (30.822498615920836,104.18462630824004), lat/lng: (30.8225047139275,104.1846
0751311325), lat/lng: (30.822511152345687,104.18458859822675), lat/lng: (30.82251679639062,104.18457181937478), lat/lng: (30.822522314574616,104.18
454733470331), lat/lng: (30.822526621027144,104.18452342891447), lat/lng: (30.822529259808793,104.18450318630745), lat/lng: (30.82253325962102,104.
18448279404514), lat/lng: (30.82253882685923,104.18446426842439), lat/lng: (30.822546595134074,104.18441331264444), lat/lng: (30.82254916538814,104
.18439768156139), lat/lng: (30.822563469397906,104.18434016821165), lat/lng: (30.822568472182464,104.18432389843329), lat/lng: (30.822574264668145,
104.18430802795936), lat/lng: (30.822576411167432,104.18428889333067), lat/lng: (30.822572424445926,104.18427508858753), lat/lng: (30.8225676392566
1,104.18426021577724), lat/lng: (30.822564457072794,104.18424419516192), lat/lng: (30.822554029818818,104.18420167307288), lat/lng: (30.82254683788
029,104.18418432472427), lat/lng: (30.822539755617562,104.18416718599724), lat/lng: (30.822532734109537,104.1841496579941), lat/lng: (30.8225240284
48285,104.18413387669122), lat/lng: (30.822510167771277,104.18410844295386), lat/lng: (30.822490543487717,104.18407782850801), lat/lng: (30.8224789
87866557,104.18406117868314), lat/lng: (30.822469694515387,104.18404938999683), lat/lng: (30.82246061318183,104.18403657321963), lat/lng: (30.82245
4153739237,104.18402351701114), lat/lng: (30.822449980225883,104.18400793557514), lat/lng: (30.822485878853005,104.18394905600132), lat/lng: (30.82
2504904998137,104.1839351825298), lat/lng: (30.822522538974233,104.1839274776011), lat/lng: (30.8225408920009,104.18392050136148), lat/lng: (30.822
55672609132,104.18391760746215), lat/lng: (30.82258255717831,104.18389768549197), lat/lng: (30.82258065814577,104.18388036739177), lat/lng: (30.822
577267258545,104.18386360819069), lat/lng: (30.82256949560242,104.18381351038605), lat/lng: (30.822567347107558,104.18379583295206)], startTime=2025-03-03 09:45:17, pauseMillisUntilFinishTime=900000),
totalDistance=1.4950,
effectivePart=0,
i2=90,
endTime=2025-03-03 10:01:16,
userInfo=UniUserInfoBean(id=8a97807690772d2c0191b347504a066e, uuasId=null, organizationVerified=1, identity=STUDENT, organizationId=402
881ea7c39c5d5017c39d134ea0418, organizationName=机电工程学院, schoolId=402881ea7c39c5d5017c39d134c30395, schoolName=西南石油大学, organizationUserN
umber=202431036019, nickName=null, realName=张航瑜, avatar=null, gender=1, birthDay=null, height=172.0, weight=63.0, faceId=null, campusId=402881ea
7c39c5d5017c39d143a9062f, year=2024, accountNumber=null, mobile=13540469537, accessToken=0cf15dd2-40fb-4dc6-b4d6-193c0d0b3764, tokenType=bearer, re
freshToken=0c388ba2-faba-424d-b142-bc890c995e97, userId=null, semesterId=8a97806a9505a31d0195276933f70506, paceRange=0.7, campusName=null, faceImag
e=https://garp-private-prod.oss-cn-chengdu.aliyuncs.com/faceImages/402881ea7c39c5d5017c39d134c30395/2024/10/07/37da0c6d96ba40c08751b397b7c80775.jpg
?Expires=1740903014&OSSAccessKeyId=LTAI5tMDMqk18ST9995xNRoV&Signature=Q8IqMPYMdMjSJhfCvC6zFfQy9OA%3D, accessTokenExpires=1741009502757, faceStatus=2, authorities=[]),
serverRuleCondition=ServerRuleCondition(limitationsGoalsSexInfoId=402888da7c3a16bb017c3a17041d000c, serMaxPace=690000, serMinPace=21000
0, serMaxStepMinute=300, serMinStepMinute=0, serCurMaxDistance=2.4, serCurMinDistance=1, serWeekDistance=15, serDailyDistance=2.4, scoringType=1, s
egmentMileage=1, weekDistance=0.00, serRangeThreshold=0.2, pointNumber=0, pointOrder=0, weeklyParagraph=null, totalWeekMileage=0.00, totalDayMileag
e=0.00, totalDayPart=0, effectiveParagraphEnd=0, effectiveParagraphStart=0, dailyParagraph=0, totalWeekPart=0, recordEffectiveMileageMsg=, recordEffectiveMileage=true),
z=true,
unEffectiveReason=

HOOK : dfc006ce0248d065ef52c08c3f76cd5810f9af90

比对签名,一致,为什么这个签名如此重要,我们要发包的时候,必须要伪造时间,虽然经纬度也就是路线可以和原来一样,不过时间必须改变,好吧,这里算法复现一下它的数字签名,而且经过我的java层分析,如果数字签名对不上,好像会有一个“检测到作弊行为,已发送给学校处理”发字符串,真的牛逼

算法复现数字签名

通过AI发现说,为什么要把输入和 itauVfnexHiRigZ6 字符串拼接再进行加密是为了什么防止彩虹表和哈希碰撞攻击,这个字符串就是所谓的salt盐值。

这个就是所谓的字符表,用来字符映射

结合so文件里面的字符串

通过前面

我想的是直接通过我frida的数据去对比参数,然后看看实现加密后产生的结果是不是相同,这部分就十分的枯燥了

uploadRunInfoReqVo.getEffectiveMileage() + uploadRunInfoReqVo.getEffectivePart() + uploadRunInfoReqVo.getStartTime() + uploadRunInfoReqVo.getCalorie() + ((int) uploadRunInfoReqVo.getAvePace()) + uploadRunInfoReqVo.getKeepTime() + uploadRunInfoReqVo.getPaceNumber() + uploadRunInfoReqVo.getTotalMileage() + ((int) uploadRunInfoReqVo.getTotalPart())

这是传入进行数字签名的参数,下面是这个uploadRunInfoReqVo的构造方法

new UploadRunInfoReqVo(averagePace, i2, doubleValue, intValue, endTime, j2, doubleValue2, str, intValue2, paceRange, latLngList, k2, str2, info.createSignIDMap(), totalDistance.doubleValue(), d3, RunTypeEnum.Companion.a(info.getRunType()).getTitle(), unEffectiveReason, e.a.c(), "3.10.0", eVar.a() + ' ' + eVar.b(), "", MathKt__MathJVMKt.roundToInt(info.getRuntimeMillis() / 1000.0d), "", "");
import hashlib

effectiveDistance=1.495 #getEffectiveMileage
effectivePart=1 #getEffectivePart
startTime="2025-03-03 09:45:17" #getStartTime
calorie=90 #getCalorie
averagePace=637581.0
#averagePace=averagePace-100*953 #(int)info.averagePace #getAvePace
Keeptime =953#MathKt__MathJVMKt.roundToInt(info.getRuntimeMillis() / 1000.0) #getKeepTime
stepCount=0 #pacenumber
totalDistance=1.495 #totalmillage
totalpart=0.0

#java里面字符串和数字拼接,数字会被自动转换为字符串
all=str(effectiveDistance)+str(effectivePart)+str(startTime)+str(calorie)+str(int(float(averagePace)))+str(int(Keeptime)) +str(stepCount)+str(totalDistance)+str(int(totalpart))
key= "itauVfnexHiRigZ6"
result=hashlib.sha1((all+key).encode("utf-8")).hexdigest()
print(result)
#dfc006ce0248d065ef52c08c3f76cd5810f9af90

经过不断的看代码,终于结合抓包和hook的数据实现了算法复现,比对签名,值一样,算法复现成功,但是我这个代码,扩展性不好,这里看才知道这个python字典的灵活,

这里得注意一下,卡了很久,就是我hook出来的数据和抓包的数据有差距,这个差距就是小数点的0,不知道为什么hook的没有小数点的0,应该是由于类型的原因吧,所以按照抓包的数据为准就行

抓到的包的oct是什么

这里本来都没注意,发现我hook的时候,数据都在,不过看了学长笔记才发现我抓到的包多了一串类似base加密的痕迹,就是oct,hook的时候数据还没有,而且我没有看到这里的oct的有关逻辑…..

oct就是他3.10相较于3.9额外加的一个判断,分析方式和3.9的签名一样。

🤔

找到了类似于发包的逻辑,不对好像是什么签到,错误

这里想了想,与其一股脑想去找什么抓包逻辑,不如再看看这个把我们的数据传入的so层方法是什么

竟然没有混淆,也可以搜到方法,爽了

发现了oct,看来它是在把我的输入传入之后进行拆分,这个so层反编译为什么会丑到这种地步,只有边猜边做

类似构造新的时间戳?目前猜测这个函数是在构造什么oct,用于新的签名,应该是为了双重保险吧

key获取在native层,加密感觉就是AES+base64,结果就是oct

这里看连同AI分析,说是目的是从一个java对象通过JNI封装到native字段,然后保存到JSON对象中,修改了字段名

不过有些JSON对象字段名没有成功反编译出来,看汇编看到了

signtime和endtime

这个时间计算我一直以为是通过开始时间再经过持续时间的运算得到endtime,可是我一直用这种算法无法还原结束时间,而且最神奇的是,我抓包的endtime结束的signtime对不上,难道在跑步的后endtime还会变?

signtime在endtime的基础上加了几秒

import time
import ctypes

# 假设 v16 是从某处获取的时间字符串
v16 = "2025-03-03 10:01:16" # 示例时间字符串
v13 = 953 # 示例值,具体来源未知

# 将时间字符串转换为时间戳
try:
tp = time.strptime(v16, "%Y-%m-%d %H:%M:%S")
v17 = int(time.mktime(tp)) * 1000 # 转换为毫秒
except ValueError:
v17 = 0

# 计算新的时间
new_timestamp = v17 + 1000 * (v13 % 11)
new_time = time.localtime(new_timestamp / 1000)

# 格式化新的时间
new_time_str = time.strftime("%Y-%m-%d %H:%M:%S", new_time)

# 打印结果
print("原始时间:", v16)
print("新时间:", new_time_str)

为了研究是不是根这个时间计算有关,我重新写了一遍这个时间计算,wc,真的是这样改变而来的,这个时间计算根本不是开始时间到结束时间,而是endtime到signtime

回过神来才发现,md这个字符串已经提示了

在已知,可能传入的参数是一个用户id和学校id之后,这里的密钥生成应该就和这两个有关

from datetime import datetime, timedelta, time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
#oct的实现
#rt ->type lcs->gpsMileage bs->paceNumber bf->paceRange zlc->totalMileage
#tp ->totalPart em->effectiveMileage ep->effectivePart uer->uneffectiveReason
#st->startTime et->endTime kill->calorie ap->avePace
#xq->semesterId jf->scoringType lid->limitationsGoalsSexInfoId sv->systemVersion
#app->appVersion dt->deviceType kt->keeptime

oct = {
"rt": "自由跑", # rt -> type
"lcs": 1.495, # lcs -> gpsMileage
"bs": 0, # bs -> paceNumber
"bf": 0, # bf -> paceRange
"zlc": 1.495, # zlc -> totalMileage
"tp": 0, # tp -> totalPart
"em":1.495 , # em -> effectiveMileage
"ep": 1, # ep -> effectivePart
"uer": "", # uer -> uneffectiveReason
"st": "2025-03-03 09:45:17", # st -> startTime
"et": "2025-03-03 10:01:16", # et -> endTime
"kill": 90, # kill -> calorie
"ap": 6375810.0, # ap -> avePace
"xq": "8a97806a9505a31d0195276933f70506", # xq -> semesterId
"jf": 1, # jf -> scoringType
"lid": "402888da7c3a16bb017c3a17041d000c",# lid -> limitationsGoalsSexInfoId
"sv": "11", # sv -> systemVersion
"app": "3.10.0", # app -> appVersion
"dt": "google Pixel 3 XL", # dt -> deviceType
"kt": 953 # kt -> keeptime
}
oct_str = json.dumps(oct, separators=(',', ':')) # 变成紧凑 JSON
oct_bytes = oct_str.encode() # 转换为字节
#应该完成了oct的赋值,下一步就是获得密钥 getRunningKey
userid="8a97807690772d2c0191b347504a066e"
schoolid="402881ea7c39c5d5017c39d134c30395"
key=userid[3:6]+schoolid[4:7]+userid[9:12]+"3e0783d6891a4a3e9521dcb6bb341560"
key=key[:32]
key=key.ljust(32,'\x00').encode()
cipher=AES.new(key,AES.MODE_ECB)
padtixt=pad(oct_bytes ,AES.block_size)
ciphertext=cipher.encrypt(padtixt)
result=base64.b64encode(ciphertext).decode('utf-8')
print(result)

撕,多次比对数据,可是结果还是不一样

根据抓包数据还有分行符这里得注意一下

from datetime import datetime, timedelta, time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
#oct的实现
#rt ->type lcs->gpsMileage bs->paceNumber bf->paceRange zlc->totalMileage
#tp ->totalPart em->effectiveMileage ep->effectivePart uer->uneffectiveReason
#st->startTime et->endTime kill->calorie ap->avePace
#xq->semesterId jf->scoringType lid->limitationsGoalsSexInfoId sv->systemVersion
#app->appVersion dt->deviceType kt->keeptime
import base64

toBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
toBase64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"


class CustomBase64Encoder:
def __init__(self, is_url=False, linemax=-1, do_padding=True):
self.is_url = is_url
self.linemax = linemax
self.do_padding = do_padding
self.base64_table = toBase64URL if is_url else toBase64

def encode(self, data: bytes) -> str:
encoded = ""
padding = "=" if self.do_padding else ""

for i in range(0, len(data), 3):
chunk = data[i:i + 3]
bin_str = ''.join(f'{byte:08b}' for byte in chunk)
bin_str = bin_str.ljust(24, '0')

for j in range(0, 24, 6):
index = int(bin_str[j:j + 6], 2)
encoded += self.base64_table[index]

pad_len = (3 - len(data) % 3) % 3
if pad_len:
encoded = encoded[:-pad_len] + padding * pad_len

if self.linemax > 0:
encoded = '\n'.join(encoded[i:i + self.linemax] for i in range(0, len(encoded), self.linemax))

return encoded

oct = {
"rt": "自由跑", # rt -> type
"lcs": 1.495, # lcs -> gpsMileage
"bs": 0, # bs -> paceNumber
"bf": 0, # bf -> paceRange
"zlc": 1.495, # zlc -> totalMileage
"tp": 0.0, # tp -> totalPart
"em":1.495 , # em -> effectiveMileage
"ep": 1, # ep -> effectivePart
"uer": "", # uer -> uneffectiveReason
"st": "2025-03-03 09:45:17", # st -> startTime
"et": "2025-03-03 10:01:16", # et -> endTime
"kill": 90, # kill -> calorie
"ap": 6375810.0, # ap -> avePace
"xq": "8a97806a9505a31d0195276933f70506", # xq -> semesterId
"jf": 1, # jf -> scoringType
"lid": "402888da7c3a16bb017c3a17041d000c",# lid -> limitationsGoalsSexInfoId
"sv": "11", # sv -> systemVersion
"app": "3.10.0", # app -> appVersion
"dt": "google Pixel 3 XL", # dt -> deviceType
"kt": 953 # kt -> keeptime
}
# oct_str = json.dumps(oct, separators=(',', ':')) # 变成紧凑 JSON
# oct_text = oct_str.encode() # 转换为字节

oct_text = "{\n" + \
"\t\"rt\":\t\"{}\",\n".format(oct["rt"]) + \
"\t\"lcs\":\t{},\n".format(oct["lcs"]) + \
"\t\"bs\":\t{},\n".format(oct["bs"]) + \
"\t\"bf\":\t{},\n".format(oct["bf"]) + \
"\t\"zlc\":\t{},\n".format(oct["zlc"]) + \
"\t\"tp\":\t{},\n".format(oct["tp"]) + \
"\t\"em\":\t{},\n".format(oct["em"]) + \
"\t\"ep\":\t{},\n".format(oct["ep"]) + \
"\t\"uer\":\t\"{}\",\n".format(oct["uer"]) + \
"\t\"st\":\t\"{}\",\n".format(oct["st"]) + \
"\t\"et\":\t\"{}\",\n".format(oct["et"]) + \
"\t\"kill\":\t{},\n".format(oct["kill"]) + \
"\t\"ap\":\t{},\n".format(oct["ap"]) + \
"\t\"xq\":\t\"{}\",\n".format(oct["xq"]) + \
"\t\"jf\":\t{},\n".format(oct["jf"]) + \
"\t\"lid\":\t\"{}\",\n".format(oct["lid"]) + \
"\t\"sv\":\t\"{}\",\n".format(oct["sv"]) + \
"\t\"app\":\t\"{}\",\n".format(oct["app"]) + \
"\t\"dt\":\t\"{}\",\n".format(oct["dt"]) + \
"\t\"kt\":\t{}\n".format(oct["kt"]) + \
"}"


#应该完成了oct的赋值,下一步就是获得密钥 getRunningKey
userid="8a97807690772d2c0191b347504a066e"
schoolid="402881ea7c39c5d5017c39d134c30395"
key=userid[3:6]+schoolid[4:7]+userid[9:12]+"3e0783d6891a4a3e9521dcb6bb341560"
key=key[:16]
key=key.ljust(16,'\x00').encode()
cipher=AES.new(key,AES.MODE_ECB)
padtixt=pad(oct_text.encode('utf-8') ,AES.block_size)
ciphertext=cipher.encrypt(padtixt)
encoder=CustomBase64Encoder()
result=encoder.encode(ciphertext)
print(result)


直接用JSON制表不行,反而需要这样手动制表

JSON制表

这里说明一下oct的json的制表为什么不一样,为什么需要手动制表

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
oct = {
"rt": "自由跑", # rt -> type
"lcs": 1.495, # lcs -> gpsMileage
"bs": 0, # bs -> paceNumber
"bf": 0, # bf -> paceRange
"zlc": 1.495, # zlc -> totalMileage
"tp": 0, # tp -> totalPart
"em":1.495 , # em -> effectiveMileage
"ep": 1, # ep -> effectivePart
"uer": "", # uer -> uneffectiveReason
"st": "2025-03-03 09:45:17", # st -> startTime
"et": "2025-03-03 10:01:16", # et -> endTime
"kill": 90, # kill -> calorie
"ap": 637581, # ap -> avePace
"xq": "8a97806a9505a31d0195276933f70506", # xq -> semesterId
"jf": 1, # jf -> scoringType
"lid": "402888da7c3a16bb017c3a17041d000c",# lid -> limitationsGoalsSexInfoId
"sv": "11", # sv -> systemVersion
"app": "3.10.0", # app -> appVersion
"dt": "google Pixel 3 XL", # dt -> deviceType
"kt": 953 # kt -> keeptime
}
oct_str = json.dumps(oct, separators=(',', ':')) # 变成紧凑 JSON
oct_text = oct_str.encode() # 转换为字节
print(oct_text)

得到的结果

感觉应该是java和python格式的原因,所以需要手动制表

抓取的数据和加密后的数据有一部分不一样,有一部分一样,太神奇了

AES的ECB模式加密是分块进行密钥加密,无扩散性,所以应该密钥没问题,明文我写的时候出现了问题,这里准备把密文进行分组,观察观察是哪里出了问题。

这里有了加解密的经验,我直接逆向这个抓包数据,写了解密

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad

# Base64 编码表
toBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
toBase64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

class CustomBase64Encoder:
def __init__(self, is_url=False, linemax=-1, do_padding=True):
# 初始化编码器,可选择是否使用 URL 安全的 Base64 编码,设置每行最大字符数和是否添加填充
self.is_url = is_url
self.linemax = linemax
self.do_padding = do_padding
self.base64_table = toBase64URL if is_url else toBase64

def encode(self, data: bytes) -> str:
# 对输入的字节数据进行 Base64 编码
encoded = ""
padding = "=" if self.do_padding else ""

for i in range(0, len(data), 3):
chunk = data[i:i + 3]
bin_str = ''.join(f'{byte:08b}' for byte in chunk)
bin_str = bin_str.ljust(24, '0')

for j in range(0, 24, 6):
index = int(bin_str[j:j + 6], 2)
encoded += self.base64_table[index]

pad_len = (3 - len(data) % 3) % 3
if pad_len:
encoded = encoded[:-pad_len] + padding * pad_len

if self.linemax > 0:
encoded = '\n'.join(encoded[i:i + self.linemax] for i in range(0, len(encoded), self.linemax))

return encoded

class CustomBase64Decoder:
def __init__(self, is_url=False):
# 初始化解码器,可选择是否使用 URL 安全的 Base64 编码
self.is_url = is_url
base64_table = toBase64URL if is_url else toBase64
self.decode_table = {char: index for index, char in enumerate(base64_table)}

def decode(self, encoded: str) -> bytes:
# 对输入的 Base64 编码字符串进行解码
encoded = encoded.replace('\n', '')
encoded = encoded.rstrip('=')

bin_str = ""
for char in encoded:
if char in self.decode_table:
bin_str += f'{self.decode_table[char]:06b}'

decoded_bytes = []
for i in range(0, len(bin_str), 8):
byte_str = bin_str[i:i + 8]
if len(byte_str) == 8:
decoded_bytes.append(int(byte_str, 2))

return bytes(decoded_bytes)

class AESECBDecrypter:
def __init__(self, key: bytes):
# 初始化 AES ECB 模式的解密器,检查密钥长度是否符合要求
if len(key) not in [16, 24, 32]:
raise ValueError("AES key must be 16, 24, or 32 bytes long.")
self.cipher = AES.new(key, AES.MODE_ECB)

def decrypt(self, ciphertext: bytes) -> bytes:
# 对输入的密文进行 AES ECB 模式解密并去除填充
# 检查数据长度是否为 16 的整数倍,如果不是则进行填充
if len(ciphertext) % AES.block_size != 0:
ciphertext = pad(ciphertext, AES.block_size)
decrypted_data = self.cipher.decrypt(ciphertext)
return unpad(decrypted_data, AES.block_size)

def base64_aes_ecb_decrypt(encoded_base64: str, aes_key: bytes, is_url=False):
# 先进行 Base64 解码,再进行 AES ECB 模式解密
base64_decoder = CustomBase64Decoder(is_url=is_url)
base64_decoded = base64_decoder.decode(encoded_base64)
aes_decrypter = AESECBDecrypter(key=aes_key)
return aes_decrypter.decrypt(base64_decoded)

def main():
try:
# 获取用户输入的 Base64 编码的密文
base64_encoded_ciphertext = "DMvBc6oQP9xwfR4lfOKAj6moJ3OcgSXuLkxobeyzW5zkHSQ4m4QtA9x75JBGqdj3MzyXas7BZQUG\nglq3Dlq8oriUxxszrShbw2laycxpIthfPvMAhusFJrQMC/yBJYVkvBPKIH7kBaMbf0ZBDCT9WkiH\nNy1OGx7jkdrLyY3w8BDxiY1NroYOjp+vOdu3ciyRRdwGpo7qM+LecoQp5GVgdCM4VpifGpCUH9So\nsKlvSIvXxSQPxL15cm8yGDfljkl9DAr+IvKkcmo2yiNp4w2isQuTx/JtT7VoCx+aUSq8yHlSHs3O\nYeXFpy/n7n+g8PuH3LXN17Nhik5oKbC2ZIaniEdkSa0XYXtirKoY2gn8KD4udE4HKhRQ5Zmhxch1\nmgdiREP+gRoykUS9ojwkSySb04gblRGY5DE6DNDHAHbamiUVF8bxAHenVCd5CYqyQ0hYDjUx16zh\n8c6PuBH1oZBEOYKpLc6izTiLJxUEIjeRXpTO5E3YZCJYegAzYii1S29w\n"
# 获取用户输入的 AES 密钥
aes_key_str = "78081e0773e0783d"
aes_key = aes_key_str.encode()

# 检查密钥长度是否符合要求
if len(aes_key) not in [16, 24, 32]:
print("错误:AES 密钥长度必须为 16、24 或 32 字节。")
return

# 询问用户是否使用 URL 安全的 Base64 编码
is_url_input = input("是否使用 URL 安全的 Base64 编码?(y/n): ").strip().lower()
is_url = is_url_input == 'y'

# 进行解密操作
decrypted_data = base64_aes_ecb_decrypt(base64_encoded_ciphertext, aes_key, is_url)
print("解密后的明文: ", decrypted_data.decode(errors='ignore'))

except ValueError as e:
print(f"值错误: {e}")
except Exception as e:
print(f"发生未知错误: {e}")

if __name__ == "__main__":
main()

好家伙,还是根之前一样的0.0和0的区别,影响了加密的数据🙂气到了

from datetime import datetime, timedelta, time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
#oct的实现
#rt ->type lcs->gpsMileage bs->paceNumber bf->paceRange zlc->totalMileage
#tp ->totalPart em->effectiveMileage ep->effectivePart uer->uneffectiveReason
#st->startTime et->endTime kill->calorie ap->avePace
#xq->semesterId jf->scoringType lid->limitationsGoalsSexInfoId sv->systemVersion
#app->appVersion dt->deviceType kt->keeptime
oct = {
"rt": "自由跑", # rt -> type
"lcs": 1.495, # lcs -> gpsMileage
"bs": 0, # bs -> paceNumber
"bf": 0, # bf -> paceRange
"zlc": 1.495, # zlc -> totalMileage
"tp": 0, # tp -> totalPart
"em":1.495 , # em -> effectiveMileage
"ep": 1, # ep -> effectivePart
"uer": "", # uer -> uneffectiveReason
"st": "2025-03-03 09:45:17", # st -> startTime
"et": "2025-03-03 10:01:16", # et -> endTime
"kill": 90, # kill -> calorie
"ap": 637581, # ap -> avePace
"xq": "8a97806a9505a31d0195276933f70506", # xq -> semesterId
"jf": 1, # jf -> scoringType
"lid": "402888da7c3a16bb017c3a17041d000c",# lid -> limitationsGoalsSexInfoId
"sv": "11", # sv -> systemVersion
"app": "3.10.0", # app -> appVersion
"dt": "google Pixel 3 XL", # dt -> deviceType
"kt": 953 # kt -> keeptime
}
# oct_str = json.dumps(oct, separators=(',', ':')) # 变成紧凑 JSON
# oct_text = oct_str.encode() # 转换为字节

oct_text = "{\n" + \
"\t\"rt\":\t\"{}\",\n".format(oct["rt"]) + \
"\t\"lcs\":\t{},\n".format(oct["lcs"]) + \
"\t\"bs\":\t{},\n".format(oct["bs"]) + \
"\t\"bf\":\t{},\n".format(oct["bf"]) + \
"\t\"zlc\":\t{},\n".format(oct["zlc"]) + \
"\t\"tp\":\t{},\n".format(oct["tp"]) + \
"\t\"em\":\t{},\n".format(oct["em"]) + \
"\t\"ep\":\t{},\n".format(oct["ep"]) + \
"\t\"uer\":\t\"{}\",\n".format(oct["uer"]) + \
"\t\"st\":\t\"{}\",\n".format(oct["st"]) + \
"\t\"et\":\t\"{}\",\n".format(oct["et"]) + \
"\t\"kill\":\t{},\n".format(oct["kill"]) + \
"\t\"ap\":\t{},\n".format(oct["ap"]) + \
"\t\"xq\":\t\"{}\",\n".format(oct["xq"]) + \
"\t\"jf\":\t{},\n".format(oct["jf"]) + \
"\t\"lid\":\t\"{}\",\n".format(oct["lid"]) + \
"\t\"sv\":\t\"{}\",\n".format(oct["sv"]) + \
"\t\"app\":\t\"{}\",\n".format(oct["app"]) + \
"\t\"dt\":\t\"{}\",\n".format(oct["dt"]) + \
"\t\"kt\":\t{}\n".format(oct["kt"]) + \
"}"

print(oct_text)
#应该完成了oct的赋值,下一步就是获得密钥 getRunningKey
userid="8a97807690772d2c0191b347504a066e"
schoolid="402881ea7c39c5d5017c39d134c30395"
key=userid[3:6]+schoolid[4:7]+userid[9:12]+"3e0783d6891a4a3e9521dcb6bb341560"
key=key[:16]
key=key.ljust(16,'\x00').encode()
print(key)
cipher=AES.new(key,AES.MODE_ECB)
padtixt=pad(oct_text.encode('utf-8') ,AES.block_size)
ciphertext=cipher.encrypt((padtixt))
result=base64.b64encode(ciphertext)
print(result)


oct算法复现完毕,那么请求包应该没有了,主要的实现点就三个

1.数字签名signdigital

2.signtime加密

3.oct参数

协议复现+数字签名还原

通过请求头的一些数据,现在去找找请求头的流程吧

移动端 OAuth2 令牌刷新机制

前面算法还原了,下一步就是协议复现,要把我们的数据手动发送到服务端,首先先登录,可以直接通过发送请求,不过登录肯定也有加密,所以当下就是去找登录的加密代码

上一个图片是抓到的登陆包

搜索关键字:在反编译后的代码中搜索常见的网络操作关键字,如 HttpURLConnection、OkHttp、Socket、OutputStream、write 等,这些往往就是数据发送的入口

通过抓包的关键字pyd t我发现了这个代码,t就是时间戳,加密的是t,这里我先frida hook一下t是什么东西

hook失败了,无论去hooknative层的函数,还是这个convert方法都没有得到想要的值,不知道为什么,没办法,登录的时候是把用户名和密码进行加密,这里暂且认为是把他们拼接在一起然后去加密吧

这里的key和数字签名的key的获取方式不同,所以这里复现一下这个key的算法

def getdykey(times):
# 假设传入的时间戳为字符串形式
a1 = str(times)

# 根据 C 代码逻辑提取部分字符串
dest = a1[2:5] # 从索引 2 开始提取 3 个字符
nptr = a1[4:8] # 从索引 4 开始提取 4 个字符
v2 = a1[-1] # 最后一个字符
v1 = int(dest)
n = int(nptr)

v3 = v1 - n

v4 = abs(v3)

v5 = v4 << int(v2)

# 拼接字符串
fixed_string = "402881ea7c39c5d5017c39d143a8062b"
final_key = f"{v5}{fixed_string}"

return final_key[:16]

print(getdykey("53535343"))

这里竟然我不能hook到值,那我就逆向,根据抓包的数据去逆向,找到发包的数据有哪些

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

def getdykey(timestamp):
# 假设传入的时间戳为字符串形式
a1 = str(timestamp)

dest = a1[2:5] # 从索引 2 开始提取 3 个字符
nptr = a1[4:8] # 从索引 4 开始提取 4 个字符
v2 = a1[-1] # 最后一个字符
v1 = int(dest)
n = int(nptr)

v3 = v1 - n
v4 = abs(v3)
v5 = v4 << int(v2)

# 拼接字符串并转换为字节
fixed_string = "402881ea7c39c5d5017c39d143a8062b"
final_key = f"{v5}{fixed_string}"[:16].encode('utf-8') # 关键修正:转换为字节

return final_key

# 测试
a = "1741503713908"
pyd = """4iWM0gLqnTU6jDdlgTGFMbCXku0ps62+gi77ocn3+2nvalJdC60HoRQAs/ObG6pQzP++8NzWZW17
D8yfaSXXoLDxdaUsCgLKMMMvXv1NakKn4U3rlKExraTgjqmluxylZvFRkkaEHo6siAZVTN6ZCj3h
Z1xuALVvbTbYy0fRa9mi+hIdnGPjFeUgQhAxM2BTffLfegwb7UNhVHGhsSIPytB+IyB+WdnVjQSg
n2tfaG1/zGy+/ZyjTKF15w85bkENCYmJWg2+6UWCZMbYf8wROFtUzQAEyQYhyVqfv7co/rNah6Wk
yzjTh6bHNXzlAgaK"""

# **获取 AES 密钥**
key = getdykey(a)
print(f"AES 密钥: {key} (长度: {len(key)})")

# **Base64 解码(处理换行符)**
rpyd = base64.b64decode(pyd.replace('\n', ''))

# **创建 AES 解密对象**
cipher = AES.new(key, AES.MODE_ECB)

# **解密**
plaintext_padded = cipher.decrypt(rpyd)

# **去除填充**
try:
plaintext = unpad(plaintext_padded, AES.block_size)
except ValueError:
plaintext = plaintext_padded

# **输出解密结果**
print("解密后的数据:", plaintext.decode('utf-8', errors="ignore"))
#AES 密钥: b'1183232402881ea7' (长度: 16)
#解密后的数据: {"entrance":"1","password":"Zzhy19502372074.","phone":"13540469537","signDigital":"5480ff3dae0cd7f5d635f0d76aae2bda152a9dd9","userName":"13540469537","authType":"Bearer","baseUrl":"https://cpes.legym.cn/","page":"","type":""}

逆向完毕,知道了登录的时候传入的数据,接下来就是手动实现发包就可以了

找到了这个数字签名时候的算法,跟跑步数字签名加密的方法一样,协议复现完毕。python不够熟练导致这玩意写起来太费劲了

import hashlib
a={"entrance":"1","password":"Zzhy19502372074.","phone":"13540469537","signDigital":"5480ff3dae0cd7f5d635f0d76aae2bda152a9dd9","userName":"13540469537","authType":"Bearer","baseUrl":"https://cpes.legym.cn/","page":"","type":""}
username=a["userName"]
password=a["password"]
entrance=a["entrance"]
#java里面字符串和数字拼接,数字会被自动转换为字符串
all=username+password+entrance
key= "itauVfnexHiRigZ6"
result=hashlib.sha1((all+key).encode("utf-8")).hexdigest()
print(result)
#5480ff3dae0cd7f5d635f0d76aae2bda152a9dd9

比对数字签名,一样,over

token获取

在这里我以为万事大吉的时候突然想到,发送数据包的协议头有个token,这个还没有具体了解

在安卓逆向中,token 可能是从以下几种方式获取或生成的:

  1. 本地存储获取(SharedPreferences、SQLite、文件)
  2. 加密存储(KeyStore、加密数据库)
  3. 动态计算(MD5/SHA1/HMAC、AES加密)
  4. 服务器下发(登录时从服务器返回)
  5. 系统 API 或设备信息(Android ID、IMEI、Serial Number)

配合抓包后的数据,推测这个token是由服务器下发的

既然是服务器下发的,那就回到登录时候的响应包,配合逆向的解密操作,获得了数据

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
def getdykey(timestamp):
# 假设传入的时间戳为字符串形式
a1 = str(timestamp)

# 根据 C 代码逻辑提取部分字符串
dest = a1[2:5] # 从索引 2 开始提取 3 个字符
nptr = a1[4:8] # 从索引 4 开始提取 4 个字符
v2 = a1[-1] # 最后一个字符
v1 = int(dest)
n = int(nptr)

v3 = v1 - n
v4 = abs(v3)
v5 = v4 << int(v2)

# 拼接字符串并转换为字节
fixed_string = "402881ea7c39c5d5017c39d143a8062b"
final_key = f"{v5}{fixed_string}"[:16].encode('utf-8') # 关键修正:转换为字节

return final_key

# 测试
a = "1741578609258"
pyd="oy/ZC5TLFKt/xTc7Yr5UMPEUEbAlBV3QRTldTKyPeu/8H/sxEa7iGBVDCZRfvfqdDH5c1Xi9EbXPHdzjIdCqxCvued0R/GqIdfELDo1r96149B+0DQvPKNliR2efoRS71wzjmZbZ0KuzWzTLV78VvkXKZgaFmvl55W4NVrHu5GRmWbstBs11O8sySIpIXgSiYSxyZBvxQg84hYtpF2LoGL3XuLnwoJ785ABRhwPObSoNFfWhW8klXy3v371ZHNuIjhJB2nyJhe2ckS7VRAGanvup79HUcZ2c4DkzzZGN5VJIuK0Yan75zcGrCrxTYFLJoZClN05yQF9hpbcwdwPTdfmDBajsPAhCRwXhNLemWkBFZkzTx89Q7mbFkZSSsAfnDb0it6ByW9PmxvMImlmqvaDArriMKl4QwjXpExpJ8lm/PWQRHaG3ThudAvEItGaos4UUIq3YTPpIDfkOJ8Oz4cQ6qBeWruSjnChjuzpxUBBA2c3VWsy7Ff9avwuS/nCbAoNfhA5ylP/h6/IcRR7iHa4BsnNcxvXAkEBE3wLmNxUjOCrPIpZMS0z2eZIt+4+Op5LKtIcMMzcrB+5iSEsXdj3VcIdR+Qyum4q4s8m2y0ZEu72TcDD658L4zeSEBsrhP4EsJ7/x4nJH34wCqTw3hRavr9QINSAfzQ84hSjz0dRbHxOK417ZfkWhpoMfKm+GkF1jNcOQwxbn87Tu7AFVJNzvLDfehImZmzTxeAe4IhCH3EAN6GSkw/w8LDHBFV/T+mlJ8WCicKGcTFHv+rrZVWTx8f5R+4MVREnU14DwUhuJSYWmpeg3OwbHgKQvNTHKSgskoPMMWb8uAZ1R93tsn3GTYX0xQlj/Nxag3voNPgaDDFfx55pUQuwj3Vgvmi5I4HAwLfZYyIOgpD9WWZJBOF3QskvUJWHBw9K6RO2P9e4WWTxJWb5PYgOCSjqqN2PXY/y3sVyqjSaYi3P46c2UlRaUQ1edzJ49MEqrscvWcULCh9+FLmJ0JJY/fAww71bTD0iJ1NKR51SPIfFFXCdVi0AeJSGsh0HOdKOY7JwAFEzwMCSZ20uo71Cp82MncHh0+oHKEkVifdSFT3RBLTHnvhDeTZBUXR2lio8djdBvpI48GVlwRgbqmC2m6n5BrwvvRBAyNsmiQnWHZyOYg60dMOFu80ktwxGe7K4gBucjk665DdBXH1/KstC9S6fZ1YEgP00cFjQvxz5QCxxdFOLfW9hAt5t7gzSBvnCp17o1t3Bl4EcUhmUNAL9sy7OiXmmpfqKQ5aqDo9OjztcJKYgK/6IrkZS8oLwcgtI4gTnJSPh7O2LgxftbsTjrJa5O6z/eapt8Y4Z4QvnmgIHUFJftW9t0pKy2zNkoWfCQZn0tiL1xTdBsEt8j10GjnOmIjlq0qzE+wcAzZ+/m/KziyyahQVBCN0Ccu5mK4NzGw8cKMgoc1CWzWpIZxoHQWAPxG0lGb+M76eeJ3wDvm6nnMegd2y9VD770vwkyuFHE6hTIyiA="
# **获取 AES 密钥**
key = getdykey(a)
print(f"AES 密钥: {key} (长度: {len(key)})")

# **Base64 解码(处理换行符)**
rpyd = base64.b64decode(pyd.replace('\n', ''))

# **创建 AES 解密对象**
cipher = AES.new(key, AES.MODE_ECB)

# **解密**
plaintext_padded = cipher.decrypt(rpyd)

# **去除填充**
try:
plaintext = unpad(plaintext_padded, AES.block_size)
except ValueError:
plaintext = plaintext_padded

# **输出解密结果**
print("解密后的数据:", plaintext.decode('utf-8', errors="ignore"))

里面有token,schoolid等等,不过我我这里比较了抓包的schoolid和userid,发现没有什么区别,如果有服务器的数据虽然更保险

现在的问题是refreshtoken和token的区别是什么

所以就是解析后的token数据

总结:

如果对于整个项目做个伪造数据包发送的程序的话,首先是登录,(AES+Base64),key的值是时间戳与后面的数字加密的key方法不一样,这个登录的数字签名的盐值就是后面的发包的盐值一样,最后伪造发包数据就行