一、起步
因为事先知道是练习app,就不做查壳等操作了,一般都是要先进行查壳,脱壳的!
先打开APP,打开第一题,可以发现是要计算数字之和的题目
二、抓包
打开HttpCanary(小黄鸟)抓包(因为第一题没有设置什么抓包对抗,所有Charles代理也是可以抓的)。可以发现请求是携带了4个参数,token是我们的身份信息不会变。
page、t、sign这三个参数是每次都会变的。sign参数是我们需要去逆向出来的。
三、分析
既然知道我们要逆向sign,那么接下来就把jadx给打开,把yuanrenxuem109.apk给拖进去反编译分析一下。
在这里把搜索窗口给打开,搜索我们需要逆向的sign字段
可以看到最下面两个是比较符合我们的要求的,这里直接跟进去看看,跟进去后可以发现,这个POST后面带一个/app1
这个时候回顾一下我们抓包的内容,可以看到携带sign去请求的网址就是app1,那么就继续跟进
直接查找这个名字被混淆了的方法是被谁调用了。
可以看到只有两个调用了,并且仔细观察这两个是一模一样的,所以任选一个进去分析。
在这里可以很明显看到sign的一个标志,那我们继续遵循深度搜索的方法,继续跟进。
跟进去可以大概看出,这是一个加密操作的函数,也跟到了底部,我们这里先不对该函数内部逻辑进行研究,先hook一下这个函数,看看返回的是不是和抓包的结果一致。
我这里是jadx-gui(1.4.7)版本的,有一个复制为frida片段的选项,这里我为了方便直接使用,大家如果是新手还是建议自己多敲敲。
四、HOOK
先创建一个test.js文件,然后构建一个调用框架main函数,然后把代码粘进去。
function main() {
Java.perform(function () {
let Sign = Java.use("com.yuanrenxue.match2022.security.Sign");
Sign["sign"].implementation = function (bArr) {
console.log(`Sign.sign is called: bArr=${bArr}`);
let result = this["sign"](bArr);
console.log(`Sign.sign 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
图中的run是作者调用错了,实际是test
可以看到成功hook上了,并且和抓包内容一致,但是这个传入的bArr是一个byte[]类型的数据,没有办法很直观的看到传入的是什么参数。
回到调用处去看,可以发现是一个String转换成的byte[]数组,那么我们就可以给他反转回去看看到底传入的是什么。
这里打印byte[]类型的数据我使用下面的代码
let ByteString = Java.use("com.android.okhttp.okio.ByteString")
ByteString.of(bArr).utf8()
总的代码:
function main() {
Java.perform(function () {
let ByteString = Java.use("com.android.okhttp.okio.ByteString")
let Sign = Java.use("com.yuanrenxue.match2022.security.Sign");
Sign["sign"].implementation = function (bArr) {
console.log(`Sign.sign is called: bArr=${ByteString.of(bArr).utf8()}`);
let result = this["sign"](bArr);
console.log(`Sign.sign result=${result}`);
return result;
}
})
}
setImmediate(main)
提一下退出上次的frida可以打exit就会退出了。
再启动一次,可以看到这次恢复正常了,参数就是page=11684850913,sign是d8fde916979aeb58d3398ea0c0e455c7和我们之前分析的参数是差不多的结果。
既然知道了参数和加密的函数调用方法,那我们就可以尝试开始主动去调用这个方法传入参数去得到加密结果了。
五、验证
这里采用frida的rpc模式,使用python去调用得到结果。这是python调用rpc的基本固定写法:
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('./agent.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()
# 这是调用rpc的写法,参数为上面看到的page=11684850913
print(script.exports_sync.getsign("page=11684850913"))
sys.stdin.read()
创建agent.js文件,输入以下代码获取sign方法,传入我们上面得到参数去调用。(底下是frida的rpc导出函数的写法)
function getSign(str) {
var result = 0;
Java.perform(function () {
var targetClassName = Java.use("com.yuanrenxue.match2022.security.Sign")
result = targetClassName.sign(str);
})
return result
}
rpc.exports = {
getsign: getSign
};
直接运行可以看到报错,传入的参数不是byte类型的([B)—不懂的可以看Jni方法签名
那么我们就需要把传入的参数和他一样变成byte[]类型格式传入。
这里我采用调用Android自带的库来实现这个字符串转byte的方法。
function stringToBytes(str) {
var data = Java.use("java.lang.String").$new(str);
return data.getBytes();
}
修改代码后:
function getSign(str) {
var result = 0;
Java.perform(function () {
var targetClassName = Java.use("com.yuanrenxue.match2022.security.Sign")
var bytes = stringToBytes(str);
result = targetClassName.sign(bytes);
})
return result
}
function stringToBytes(str) {
var data = Java.use("java.lang.String").$new(str);
return data.getBytes();
}
rpc.exports = {
getsign: getSign
};
再运行一次,会发现报错信息:sign:不能在没有实例的情况下调用实例方法。
检查一下代码可以发现,我们的Java.use(“com.yuanrenxue.match2022.security.Sign”)是需要实例化后才可以调用sign方法的。这是实例化后的代码:
function getSign(str) {
var result = 0;
Java.perform(function () {
var targetClassName = Java.use("com.yuanrenxue.match2022.security.Sign").$new();
var bytes = stringToBytes(str);
result = targetClassName.sign(bytes);
})
return result
}
function stringToBytes(str) {
var data = Java.use("java.lang.String").$new(str);
return data.getBytes();
}
rpc.exports = {
getsign: getSign
};
这次就正常显示数据了,并且和上面的sign相同,这里就完成了rpc调用并且成功对应上结果。