教程
- 前言
- 一、起步
- 二、抓包
- 三、分析
- 四、验证
- 五、HOOK
- 借鉴
前言
前面2-3题和第一题解题思路基本上一样的,这里就不出教程了,这篇文章比较繁琐,基本上描述了我做这题的思路,有很多走不通的地方也有对应的方法,所以会比较长,没有耐心的朋友可以看看其他人较为简短的文章(逆向这东西主要看思路,代码量其实对比后端没多少的)
一、起步
先打开APP,打开第四题,可以发现是要计算数字之和的题目
二、抓包
打开HttpCanary(小黄鸟)抓包
嗯,上面是一串我们看不懂的返回结果,请求头很明显的告诉了我们这是grpc请求,那我们先去看看这个乱码是怎么传进去的。
三、分析
把jadx给打开,把yuanrenxuem109.apk给拖进去反序列化分析一下。老样子编译好后,进行搜索上面请求的网址关键字段SayHello,看看能不能找到一些什么。
结果只有一行,是下面这个函数,大致看了一下,太多混淆的变量名了,直接先hook一波这个方法的返回结果看看。
一样复制为frida片段
创建test.js文件输入frida代码:
function main() {
Java.perform(function () {
let OooO00o = Java.use("oOO00O.OooO00o");
OooO00o["OooO00o"].implementation = function () {
console.log(`OooO00o.OooO00o is called`);
let result = this["OooO00o"]();
console.log(`OooO00o.OooO00o result=${result}`);
return result;
};
}
)
}
setImmediate(main)
启动frida
>adb shell
>su
>cd data/local/tmp
>./frida-server-16.0.2-android-arm64 -l 0.0.0.0:8881
转发端口
>adb forward tcp:8881 tcp:8881
frida调用hook代码
> frida -H 127.0.0.1:8881 -f com.yuanrenxue.match2022 -l test.js
最后输出结果:
可以看到没有我们想要的参数信息,那就换一个参数搜索分析。回顾上面抓包详情,我们这次搜h2字段。
点进去分析后可以发现,这实则是一个Protocol协议的内容,没有办法继续跟了。
那我们捋清思路,回想第一次打开APP第四题的时候会有个第四题的字样,那么这次搜一下第四题字样看看
点进去分析后,第一个只是字符串信息,没有什么有用的价值,第二个是一个类,我们可以慢慢看看这个类底下的方法
往下慢慢浏览可以观察到一个很敏感的单词sign
老样子先验证一下,复制frida片段,然后拖到test.js里面去跑起来看看
可以看到是跑出一个加密值了,此时对数据敏感度高的读者应该就能联想到最开始抓包那串乱码后面也有一串数字,看起来和这个长度很像,既然想到了,那就打开抓包和他验证一番。
- 可以看到是一模一样的,但是前面那串乱码又是什么东西呢。
- 从截图可以知道这个以grpc框架为基础发送的请求,grpc是以Protobuf序列化协议开发的,发送的数据包的格式也都是Protobuf协议规定的格式,具体Protobuf的一些基础使用可以看完上篇文章。(grpc详情看这里)
- 那我们怎么把这个乱码结果给复制出来,并且用Protobuf去解析呢。用HttpCanary的保存按钮,选择保存请求体,给文件夹命名,然后request_body.bin(这个文件就代表了Protobuf数据文件,不懂的可以看我上篇文章的序列化后保存的文件,也是一个bin格式的)就会存在我们的HttpCanary/你命名的文件夹下/
在这里我使用手机USB连接电脑,直接打开我们刚刚存bin文件的路径,把它复制出来到自己随便哪一个文件夹。然后用上篇文章Protobuf说的无.proto语法文件进行反编译试一下。
会直接发现,是显示错误输入,按照道理来说是不可能的啊。这个时候我们就需要去看看问文件内容到底长啥样,和我们之前的bin文件差别在哪里。打开request_body.bin和上次自己序列化出来的my_example.bin文件对比一下。我这里用的是010Editor软件中文汉化版打开两个文件。
可以大致的看到0801前数据都是不一样的,那么我们就把request_body.bin前面修改成my_example.bin前面一样看看是否符合格式去反序列化。
直接双击就可以修改了,再次运行还是报错。
这个时候我突然想起来了,一份编码规范Protocol Buffers编码详解,例子,图解,在里面介绍了这种情况
再回顾一下,上一篇文章的proto语法,对照这个int32的key,是不是就代表了my_example.bin文件中08后面的才是真的数据,我们直接把my_example.bin文件中的0A15删除,反编译看一下
可以看到成功编译出来了,0A15其实是可以不需要的
那么我们也可以去把request_body.bin文件的0A15删除,看看能否得到数据
可以看到成功解析出数据了
那么之前的0A15是什么呢,这是没去掉0A15的例子,打印出来的是多了一个1{}
对比了一下之前写的.proto语法文件可以发现,其实0A15对应的是下面框选出来的message!至于request_body.bin的1B是什么暂时不得而知。毕竟是不知道proto语法文件格式反编译出来的。
四、验证
经过了上面的反编译结果,我们大概是知道了request_body.bin文件只有3个字段,那么我们可以开始编写.proto语法文件了,然后用grpc去尝试请求看看对不对。
安装一下grpc需要的依赖环境
pip install grpcio
pip install grpcio-tools
创建一个.proto语法文件,根据上面反序列化request_body.bin文件得到的3个字段,键名自己随便取,看他像什么取什么。
// 请求是http://180.76.60.224.9901/challenge.Challenge/SayHello
// 指定版本为proto3,默认为proto2
syntax = "proto3";
// 根据请求定义包名
package challenge;
// 定义请求结构
message RequestMessage {
// 使用 int32 类型的字段,字段编号为 1
int32 page = 1;
// 使用 int64 类型的字段,字段编号为 2
int64 time = 2;
// 使用 string 类型的字段,字段编号为 3
string sign = 3;
}
// 根据请求定义一个名为 Challenge 的服务
service Challenge {
// 根据请求定义定义一个名为 SayHello 的远程过程调用(RPC)方法
rpc SayHello(RequestMessage) returns (ResponseMessage) {}
}
// 定义返回内容的数据结构
message ResponseMessage {
//item为data子集, 可以拥有多个item
repeated Item data = 1;
}
message Item {
string value = 1;
}
然后通protoc.exe编译生成_pb2.py文件和_pb2_grpc.py文件
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. myprotobuf.proto
接着编写main.py文件,去将我们之前反序列化出来的数据,组合成编写的.proto语法结构数据,给发送过去,然后打印结果。
import grpc
import myprotobuf_pb2
import myprotobuf_pb2_grpc
from google.protobuf import json_format
def run(page_num, now_time, sign):
with grpc.insecure_channel('180.76.60.244:9901') as channel:
setup = myprotobuf_pb2_grpc.ChallengeStub(channel)
requests_data = myprotobuf_pb2.RequestMessage()
requests_data.page = page_num
requests_data.time = now_time
requests_data.sign = sign
response = setup.SayHello(requests_data)
json_string_request = json_format.MessageToJson(response)
return json_string_request
if __name__ == '__main__':
data = run(1, 1684980930731, "16ec5c17c7f7eadb")
print(data)
可以看到数据成功打印出来,并且对应上了。
五、HOOK
接下来就是使用frida配合grpc实现一个翻页功能了。把上次分析的sign的frida代码拿过来,改写成主动调用。这是未改写前的
function main() {
Java.perform(function () {
let ChallengeFourFragment = Java.use("com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment");
ChallengeFourFragment["sign"].implementation = function (str, j) {
console.log(`ChallengeFourFragment.sign is called: str=${str}, j=${j}`);
let result = this["sign"](str, j);
console.log(`ChallengeFourFragment.sign result=${result}`);
return result;
};
}
)
}
setImmediate(main)
结果如下图所示,很明显参数是1:1684991187938和1684991187938,对应上我们的app,就是(页数:时间戳,时间戳组)成的参数,最后得到sign参数
那我们接下来主动调用一下:
function main() {
let result = 0;
Java.perform(function () {
let ChallengeFourFragment = Java.use("com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment").$new();
result = ChallengeFourFragment["sign"]("1:1684991187938",1684991187938 )
console.log(result)
}
)
return result;
}
setImmediate(main)
结果如下,和上面的数据一模一样,那么就可以开始进行python的rpc调用了。
function getSign(str, j) {
let result = 0;
Java.perform(function () {
let ChallengeFourFragment = Java.use("com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment").$new();
result = ChallengeFourFragment["sign"](str, j)
}
)
return result;
}
rpc.exports = {
getsign: getSign
};
import frida
import sys
host = "127.0.0.1:8881"
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
with open('./test.js') as f:
test_js = f.read()
# 启动方式1
manager = frida.get_device_manager()
# process = manager.add_remote_device(host).get_frontmost_application()
# print(process)
process = manager.add_remote_device(host).attach("猿人学2022")
script = process.create_script(test_js)
script.on('message', on_message)
script.load()
print(script.exports_sync.getsign("1:1684991187938",1684991187938))
sys.stdin.read()
完成了rpc调用后得到sign后,我们可以开始加上我们的grpc代码进行翻页功能了。
import sys
import time
import myprotobuf_pb2
import myprotobuf_pb2_grpc
import frida
import grpc
from google.protobuf import json_format
host = "127.0.0.1:8881"
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
def run(page_num, now_time, sign):
"""
gprc连接
:param page_num:
:param now_time:
:param sign:
:return:
"""
with grpc.insecure_channel('180.76.60.244:9901') as channel:
setup = myprotobuf_pb2_grpc.ChallengeStub(channel)
requests_data = myprotobuf_pb2.RequestMessage()
requests_data.page = page_num
requests_data.time = now_time
requests_data.sign = sign
response = setup.SayHello(requests_data)
json_string_request = json_format.MessageToJson(response)
return json_string_request
def load_frida():
"""
加载frida
:return:
"""
with open('./test.js') as f:
test_js = f.read()
# 启动方式1
manager = frida.get_device_manager()
process = manager.add_remote_device(host).attach("猿人学2022")
script = process.create_script(test_js)
script.on('message', on_message)
script.load()
return script
if __name__ == '__main__':
script = load_frida()
for page in range(1, 3):
now_time = int(time.time() * 1000)
sign = script.exports_sync.getsign(f"{page}:{now_time}",now_time)
print(f"page: {page}, now_time: {now_time}, sign: {sign}")
data = run(page, now_time, sign)
print(data)
成功翻页,至此第四题结束:
借鉴
Protocol Buffers编码详解,例子,图解
猿人学-Android端爬虫比赛第四关【grpc】解题笔记