0%

逆向学习 0x0D 算法转发

frida的python库使用

安装配置frida的时候是下载了两个frida的相关库,frida 和 frida-tools 之前使用的 frida-ps 等更多的是用来手工调试的阶段,如果要用代码自动化处理,还需要其他语言的接入,比如python爬虫

后续的一些 frida 算法转发方案和 frida 的rpc也需要用到python,如果进行算法转发的话,就不需要对app的算法进行还原了,通过主动调用让app成为加密的工具

frida可以实时与python进行数据交互,可以把数据发送到python,等待python处理完毕之后,接收返回值,frida继续执行

包名注入

使用 frida.get_usb_device() 来通过 USB 连接,下面写入需要注入的代码,然后调用 load 方法进行注入,最好方式命令行关闭可以使用 sys.stdin.read()

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
# -*- coding: utf-8 -*-

import frida, sys

JScode = """
Java.perform(function() {
var RequestUtils = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtils.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(str, key, iv) {
console.log("data", str)
console.log("key", key)
console.log("iv", iv)
var retval = this.encodeDesMap(str, key, iv);
console.log("retval", retval)
return retval;
}

var Utils = Java.use('com.dodonew.online.util.Utils')
Utils.md5.implementation = function(str) {
console.log("md5", str)
var retval = this.md5(str);
console.log("retval", retval)
return retval;
}
});
"""

# 通过包名注入
process = frida.get_usb_device().attach('com.dodonew.online')
script = process.create_script(JScode)
script.load()
print("开始运行")
sys.stdin.read()

pid注入

和包名注入差不多,先查一下PID

1
frida-ps -U

image-20250604175941556

image-20250604180615326

spawn

spawn附加就是和之前js的命令附加差不多,注意这里的代码和上面的相比多了恢复进程这一步

1
2
3
4
5
6
7
8
9
10
11
# 通过spawn附加
device = frida.get_usb_device()
print("device:", device)
pid = device.spawn(['com.dodonew.online']) # 以挂起的方式创建进程
process = device.attach(pid)

script = process.create_script(JScode)
script.load()
device.resume(pid) # 加载完脚本之后回复进程,如果不恢复进程软件无法启动
print("开始运行")
sys.stdin.read()

上面相当于

1
frida -U -f com.dodonew.online

通过非标准端口连接

frida-server 开启的默认端口是 27042,启动 frida-server的时候可以指定端口的

1
frida-server -l 0.0.0.0:8888

image-20250605151522222

这种方法是可以进行多设备连接 hook 的,前提是都正常打开了 frida-server,然后根据IP和端口来对多个设备进行hook。但是要操作多个设备,还是整个 gadget 方便点吧。对模拟器进行更改端口时,需要端口转发,因为模拟器和本机的IP地址是一样的

frida与python的交互

send

使用send进行交互,例如要将md5返回值 send 返回给python,不进行打印了,首先要在 js 中更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function() {
var RequestUtils = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtils.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(str, key, iv) {
console.log("data", str)
console.log("key", key)
console.log("iv", iv)
var retval = this.encodeDesMap(str, key, iv);
console.log("retval", retval)
return retval;
}

var Utils = Java.use('com.dodonew.online.util.Utils')
Utils.md5.implementation = function(str) {
console.log("md5", str)
var retval = this.md5(str);
send(retval) // console.log("retval", retval)
return retval;
}
});

send只能接收一个参数,将参数包装成一个字典,如果接收多个参数就会报错,type字段为 error,而且send还需要注册一下 ,第一个参数固定是 message 第二个参数是函数名。

send最重要的作用就是,将js的数据交给python来处理

1
script.on('message', messageTest)

image-20250605170819355

recv

send是将js里的数据传递给python,recv是等待python里的数据传递给js,传递的时候则通过 create_script 方法创建的那个 script.post 来传递

recv().wait() wait()方法会阻塞方法,等待python的返回

通过recv更改一下返回值

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
44
45
46
import frida, sys

JScode = """
Java.perform(function() {
var RequestUtils = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtils.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(str, key, iv) {
console.log("data", str)
console.log("key", key)
console.log("iv", iv)
var retval = this.encodeDesMap(str, key, iv);
console.log("retval", retval)
return retval;
}
var Utils = Java.use('com.dodonew.online.util.Utils')
Utils.md5.implementation = function(str) {
console.log("md5", str)
var retval = this.md5(str);
send(retval)
recv(function(obj) {
console.log("python:", obj.data);
retval = obj.data
}).wait();
return retval;
}
});
"""

def messageTest(message, data):
if message['type'] == 'send':
print(u"[*] {0}".format(message['payload']))
script.post({ "data": "7588343d9969bf6e7f9438f7a081d36d"} )
else:
print(message)


device = frida.get_usb_device()
print("device:", device)
pid = device.spawn(['com.dodonew.online']) # 以挂起的方式创建进程
process = device.attach(pid)

script = process.create_script(JScode)
script.on('message', messageTest)
script.load()
device.resume(pid) # 加载完脚本之后回复进程,如果不恢复进程软件无法启动
print("开始运行")
sys.stdin.read()

通过执行结果可以看到确实是返回的更改之后的数值

image-20250605175916772

一般来说除非数据特别复杂,不然有点多此一举了,有点麻烦,js中又不是不可以处理一些数据

frida rpc 远程调用

回忆一下主动调用,之前了解到的主动调用的方式有点呆板,只能一次调用,是写死了的。这个JS代码也是注入到程序中的,不能再更改了,现在就可以将他拿到python中来,可以跟服务器,爬虫对接等等,修改和调用就相对自由。

image-20250605210956090

使用 rpc 导出一个接口让python调用,这种方法可以看到提示有点过时了

1
2
3
4
5
6
7
# js代码中
rpc.exports = {
rpcfunc: test
}

# 注意如果 exports 中的方法名是大写,下面调用大写字母前就要后下划线_,这个和 js python 规范化的命名方式不同
result = script.exports.rpcfunc()

image-20250605212822610

看下面的提示,提示要异步操作,这样即可正常调用

1
result = script.exports_sync.rpcfunc()

rpc.exports 中也可以直接写方法,也可以给出写好的方法名,这个是无所谓的

1
2
3
4
5
6
7
8
9
rpc.exports = {
rpcfunc: function(data) {
var res = "";
Java.perform(function() {
res = Java.use('com.dodonew.online.util.Utils').md5(data);
});
return res;
}
}

虽然这样写还是要手动进行调用,但是是再python中,就可以结合爬虫,或者监听什么的操作稍微灵活一些了

frida 算法转发

写一个主动调用加密的算法,跳过算法分析的步骤,再结合之前写过的协议还原,将计算好的发送到服务端,再主动调用解密算法将服务端的数据解密,这样就可以实现将app作为加密机器,完全不用什么操作

image-20250605220252529

利用 rpc 进行算法转发在python中直接调用方法,还可以开放一个本地端口,支持其他的语言和python进行交互

1
2
pip install fastapi
pip install uvicorn

get

getEchoApi 函数上面的注解的意思是,如果访问到这个路由,就调用下面这个方法,比如

http://127.0.0.1:8888/get 如果发送了这个请求,就会调用方法,后面再加上参数

http://127.0.0.1:8888/get?item_id=100&item_user=13112345678&item_pass=123asad

python代码是可以被导包的,如果这个文件被导入会被先执行一遍,在main中的方法是不在这个时候被执行的

image-20250606114306422

有了这个东西,就可以使用其他语言,通过一个 get 请求来获取加密后的内容了,也是一个将app中的算法拿出来使用的操作,省去算法还原的操作了

post

上面app.get 是get请求app.post 是post请求,面对大量数据(图片)的时候 get 请求长度是不太够的,需要用到post 请求,写post需要用到一个 BaseModel,新建一个calss类

image-20250609161728954

fiddler数据转发

fiddler数据转发:使用fiddler拦截数据包,把数据包发送到指定服务器

fiddler是一个抓包工具,直接从官网下载exe,默认安装即可

半汉化插件:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1853941&highlight=fiddler%BA%BA%BB%AF

在选项这里将 https 中的三个选项勾选上,中间会进行证书安装,全部yes即可

image-20250610203858228

抓包工具配好了,但是要实现数据转发还需要用到 fiddler script 。这个是 fiddler 功能的扩展,用代码去控制 fiddler

image-20250610205707598

找了一下介绍 fiddler script 中的几个方法的文章: https://www.kancloud.cn/ncymkj/development_document/2055244

名称 作用
OnBeforeRequest 每次请求之前调用
OnBeforeResponse 每次响应之前调用
OnBoot 启动时调用
OnShutdown fiddler关闭时调用
OnAttach fiddler注册成系统代理时调用
OnDetach fiddler 取消注册系统代理时调用
Main 在每次fiddler启动时和编译CustomRules.js 脚本时调用。

既然是数据转发那么就需要在 OnBeforeResponse 方法中做点手脚了

这里以抖音为例,尝试拦截一下评论内容

image-20250610215749558

搜索一下 刘招华原型 文本信息

找到了一条请求,根据这条请求筛选 url 和字段

image-20250610215824213

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
44
45
46
47
48
49
50
51
if (oSession.uriContains("/monitor_browser/collect/batch"))
{
FiddlerObject.log("POST 请求");
var html = oSession.GetRequestBodyAsString()
FiddlerObject.log("捕获请求:" + html);
if(html.Contains("resultKeywords")){
//数据统计开始:把内容通过ajax http发送其它地方
var _xhr = new ActiveXObject('Microsoft.XMLHTTP');
var url = 'http://127.0.0.1:8000/sendData';
//发送的数据参数
var jsonString = oSession.GetResponseBodyAsString();
var requestHeaders = oSession.oRequest.headers.ToString();
var responseHeaders=oSession.oResponse.headers.ToString();

var str='{}';//构造自己的JSON http请求的信息及返回的结果
var data = Fiddler.WebFormats.JSON.JsonDecode(str);

data.JSONObject["requestHeaders"]=requestHeaders;
data.JSONObject["responseHeaders"]=responseHeaders;
data.JSONObject["responseBody"] = jsonString;
data.JSONObject["url"] = oSession.fullUrl;
data.JSONObject["response_code"] = oSession.responseCode;

if(oSession.oRequest.headers.Exists("Cookie")){
data.JSONObject["requestCookie"] = oSession.oRequest.headers['Cookie'];
}else{
data.JSONObject["requestCookie"] = 'request no Cookie';
};

if(oSession.oResponse.headers.Exists("Cookie")){
data.JSONObject["responseCookie"] = oSession.oResponse.headers['Cookie'];
}else{
data.JSONObject["responseCookie"] = 'response no Cookie';
};
jsonString = Fiddler.WebFormats.JSON.JsonEncode(data.JSONObject)

FiddlerObject.log(jsonString);

_xhr.onreadystatechange=function(){
if (_xhr.readyState==4){
FiddlerObject.log(_xhr.responseText);
}
};
_xhr.open('POST', url, true);
_xhr.send(jsonString);
//----数据统计结束-----
}else{
//弹窗报错
FiddlerObject.alert("抓取出错!");
} // if end
}

将特定url的数据发送到本机的 8000 端口,然后在使用 fastAPI 开启本地端口

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
44
45
46
47
48
49
50
51
52
53
FiddlerObject.log("xiaojianbang1");
//过滤无关请求,只关注特定请求
if (oSession.fullUrl.Contains("relationalSearch"))
{
FiddlerObject.log("xiaojianbang2");
var html = oSession.GetResponseBodyAsString()
FiddlerObject.log(html);
if(html.Contains("resultKeywords")){
//数据统计开始:把内容通过ajax http发送其它地方
var _xhr = new ActiveXObject('Microsoft.XMLHTTP');
var url = 'http://127.0.0.1:8000/sendData';
//发送的数据参数
var jsonString = oSession.GetResponseBodyAsString();
var requestHeaders = oSession.oRequest.headers.ToString();
var responseHeaders=oSession.oResponse.headers.ToString();

var str='{}';//构造自己的JSON http请求的信息及返回的结果
var data = Fiddler.WebFormats.JSON.JsonDecode(str);

data.JSONObject["requestHeaders"]=requestHeaders;
data.JSONObject["responseHeaders"]=responseHeaders;
data.JSONObject["responseBody"] = jsonString;
data.JSONObject["url"] = oSession.fullUrl;
data.JSONObject["response_code"] = oSession.responseCode;

if(oSession.oRequest.headers.Exists("Cookie")){
data.JSONObject["requestCookie"] = oSession.oRequest.headers['Cookie'];
}else{
data.JSONObject["requestCookie"] = 'request no Cookie';
};

if(oSession.oResponse.headers.Exists("Cookie")){
data.JSONObject["responseCookie"] = oSession.oResponse.headers['Cookie'];
}else{
data.JSONObject["responseCookie"] = 'response no Cookie';
};
jsonString = Fiddler.WebFormats.JSON.JsonEncode(data.JSONObject)

FiddlerObject.log(jsonString);

_xhr.onreadystatechange=function(){
if (_xhr.readyState==4){
FiddlerObject.log(_xhr.responseText);
}
};
_xhr.open('POST', url, true);
_xhr.send(jsonString);
//----数据统计结束-----
}else{
//弹窗报错
FiddlerObject.alert("抓取出错!");
} // if end
} // if controll end

在 Fiddler 的脚本(如 FiddlerScript 或自定义规则)中,oSession.GetResponseBodyAsString()oSession.GetRequestBodyAsString() 是用于获取 HTTP 消息体内容的方法,oSession.GetResponseBodyAsString() 用于获取服务端返回给客户端的HTTP响应oSession.GetRequestBodyAsString() 用于获取客户端发送给服务端的HTTP请求

因此如果获取不到html信息的话可以,换一个API尝试,我对抖音进行测试的时候,使用oSession.GetRequestBodyAsString() 没有获取到body的信息 oSession.GetResponseBodyAsString() 反倒可以

image-20250612174708583