此app为猿人学2020年的赛题,需要得到0-99所有id值对应的返回结果之和。抓包查看到接口为/api/match/11/query
,参数是id值以及sing值,返回数据就是id值对应的data值。
反编译apk得到发起请求的函数为query
,其调用getSign1
以id值为参数生成sign值,onResponse
函数会接收到请求的返回
主动调用请求函数
如果只是为了解题的话就可以直接通过frida主动调用query
请求0-99所有id值对应的data,并hook onResponse
函数得到返回结果计算和。对应的frida脚本如下:
function main(){
Java.perform(function(){
var ma_instance = 0;
Java.choose("com.yuanrenxue.onlinejudge2020.MainActivity", {
onMatch:function(instance){
ma_instance = instance;
console.log(ma_instance);
},onComplete:function(){}
})
var sum = 0;
Java.use("com.yuanrenxue.onlinejudge2020.MainActivity$4").onResponse.overload("okhttp3.Call", "okhttp3.Response").implementation = function(arg1, arg2){
var response_string = arg2.body().string();
console.log(response_string);
var JsonObject = Java.use("org.json.JSONObject").$new(response_string);
sum = sum + JsonObject.optInt("data");
}
for(var i = 0; i < 100; i++){
var num = Java.use("java.lang.Integer").$new(i);
ma_instance.query(num);
Thread.sleep(1);
}
console.log("sum is :", sum);
})
}
setImmediate(main)
但是在脚本执行的过程中发现每次请求都要等待很多时间,一开始以为是服务器响应的慢,但是手动点击请求确响应的很快。
查看一下getSign1
函数做了哪些操作,此函数会调用native函数getSign
。查看jni_onload函数得到此函数对应的native为sub_5C8A8
分析sub_5C8A8
函数发现其会调用myLooper
函数,如果返回null就会sleep,因为frida主动调用应该是native线程通过反射调用的,所以调用myLooper
会返回null并调用sleep,这也是之前脚本每次请求都需要等待很多时间的原因。
修改一下之前的脚本,replace sleep函数并直接返回。
var sleep_addr = Module.findExportByName("libc.so", "sleep");
console.log("sleep_addr is:", sleep_addr);
// 使用Interceptor.replace替换sleep函数的实现
Interceptor.replace(sleep_addr, new NativeCallback(function (seconds) {
return 1;
}, "int", ["int"]));
再次运行脚本就可以正常获取到sum值为483974
分析sign算法
另一种方法就是分析sign生成算法,getSign
对应的native为sub_5C8A8
。此函数会先调用nextInt(10000)
和nextInt(1000000)
生成两个随机值。
然后将两个随机值和id值格式化输出到%d:yuanrenxue2020:%ld:randomClientId%sReplaceWithYourTeamNameIfYouCrackedToHere
字符串中,接着在字符串开头再增加一个随机值并对字符串其余元素与0x14进行异或运算。
最后调用sub_5E950
函数对前面的字符串再次进行编码,此函数是一个魔改的base64(四个一组的字符,第二个和第三个的位置发生交换),码表为9+#FvwxNG78pqrghijCDEWXy4oAd56kQHlmBuOPYz0cstef1IJKLM23ZabnRSTUV
。
frida hook各个函数查看sign生成的整个过程。
知道了sign的生成算法就可以直接编写python脚本了
import httpx
import random
from urllib.parse import quote_plus
# 定义base64字符表
BASE64_CHARS = "9+#FvwxNG78pqrghijCDEWXy4oAd56kQHlmBuOPYz0cstef1IJKLM23ZabnRSTUV"
def my_base64_encode(data):
# 将数据转换为字节串
data = data.encode()
# 初始化编码后的字符串
encoded = ""
# 对每三个字节进行处理
for i in range(0, len(data), 3):
# 获取当前三个字节
chunk = data[i:i+3]
# 计算当前三个字节对应的24位二进制数
bits = 0
for j in range(len(chunk)):
bits += chunk[j] << (16 - j * 8)
# 对每六位二进制数进行编码,得到四个base64字符,并拼接到编码后的字符串中
for k in range(4):
if i * 8 + k * 6 < len(data) * 8:
index = bits >> (18 - k * 6) & 0x3F
encoded += BASE64_CHARS[index]
else:
encoded += "=" # 补齐等号
# 对每四个字符一组的字符中的第2个与第3个互换
swapped = ""
for i in range(0, len(encoded), 4):
group = encoded[i:i+4]
if len(group) == 4:
group = group[0] + group[2] + group[1] + group[3]
swapped += group
return swapped
def main():
sum = 0
num = 0
client = httpx.Client(http2=True, verify=False)
url = "https://match.yuanrenxue.com/api/match/11/query?id="
while num < 100:
n1 = random.randint(0, 10000)
n2 = random.randint(0, 1000000)
n3 = random.randint(0, 120)
encrypt_sign = str(n1) + ":yuanrenxue2020:" + str(num) + ":randomClientId" + str(n2) + "ReplaceWithYourTeamNameIfYouCrackedToHere"
for i in encrypt_sign:
i = ord(i) ^ 0x14
encrypt_sign = encrypt_sign + chr(i)
encrypt_sign = str(chr(n3)) + encrypt_sign;
encrypt_sign = my_base64_encode(encrypt_sign)
response = client.get(url + str(num) + "&sign=" + quote_plus(encrypt_sign))
sum = sum + response.json()['data']
num = num + 1
print(response.text)
print(sum)
if __name__ == '__main__':
main()
最后运行的结果也是483974