Android-猿人学爬虫练习
猿人学爬虫练习
闲着没事找了个2022年的猿人学的安卓逆向对抗比赛apk。
参考自:猿人学安卓逆向对抗比赛(1-5题) - 吾爱破解 - 52pojie.cn
时间冲突
因为是2025年复现的,但比赛当时的服务器环境可能是2022年的,而app运行起来首先会比较时间是否在20s偏差以内,若时间不对会直接退出。
我在修改了本地时间为北京时间后,仍然提示,所以根据提示字符串搜索到校验代码:
1 | public void onSuccess(OooOO0O oooOO0O) throws Throwable { |
其中oooOO0O.OooO00o().longValue()
应该就是服务器返回的时间,而System.currentTimeMillis()
则是本地获取的时间,可能是过了两年的原因,猜测服务器返回时间不一致,所以我们直接把onSuccess
函数给hook掉(通过启动时注入的方式),之后就能正常做题了。
1 | function main() { |
进入主界面:
第一关 java层加密
打开界面如下:
用httpcanary抓包,发现打开第一关会自动发送post请求获取数据(根据page页数回显),然后下滑会显示第2、3页,同时发送对应的page页。
请求包:
响应包:
分析加密逻辑,尝试几次后发现token串是固定的,唯一不变的是sign字段。
jadx打开搜索"sign
字符串,定位到接口处。
1 | public interface OooO0O0 { |
对OooO00o
进行交叉引用,定位到封装请求包的地方。
1 | public /* synthetic */ o00Ooo lambda$initListeners$0(o00O000.OooOO0O oooOO0O) throws Exception { |
所以我们直接hook Sign().sign
:
1 | let Sign = Java.use("com.yuanrenxue.match2022.security.Sign"); |
发现参数就是page=i+时间戳,如page=31750577329 (3是第三页)
但是验证的时候发现不是标准的md5加密。
算法还原
所以我们把java层的sign加密提取出来。
1 | import java.util.ArrayList; |
sign加密的第一步先进行了md5填充,我们hook一下padding函数,发现返回值如下:
1 | [112, 97, 103, 101, 61, 49, 49, 55, 53, 48, 53, 56, 52, 57, 51, 50, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0] |
但是在java里运行打印一下padding,发现结果不一致:
1 | [112, 97, 103, 101, 61, 49, 49, 55, 53, 48, 53, 55, 56, 49, 48, 57, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0] |
代码里的多了个128,但实际运行的没有,猜测可能是jadx反编译错误的原因。
所以我们进行修改,将最后一个128置为0:
1 | ArrayList<Integer> padding = padding(bArr); |
修改后运行sign结果与hook的一致。
协议复现
这里需要用到jpype
库,即在python里导入java的jar包,调用java函数。
使用步骤:
1 | javac Sign.java |
最后的爬虫脚本:
1 | import requests |
但是发现有的时候会爬不下来,并且只能爬取前9页数据,猜测应该也是服务器时间不一致的原因。。
第二关 so层加密
同样的界面,首先先抓包。
请求包:
响应包:
同样搜索"sign
字符串,定位到接口处。
1 |
|
交叉引用找到发包地方。
1 | public /* synthetic */ o00Ooo lambda$initListeners$0(o00O000.OooOO0O oooOO0O) throws Exception { |
查看sign
方法。
1 | public native String sign(String str); |
ida打开查看是md5+base64,参数是page:time
的形式,如1:1750648187
本来打算逆向分析md5魔改了哪些地方的,但是inline hook 寄存器的时候老是崩掉,索性放弃分析了,直接改用unidbg模拟执行拿到加密结果sign
。
Unidbg代码
1 | package com.yuanrenxue.match2022.fragment.challenge; |
因为需要对Unidbg
项目打jar包供python使用jpype
调用,所以默认的加载so的方法行不通:
1 | DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libmatch02.so"), false); |
这种方法用File加载so文件,但是我们打包成jar后,JAR 中的资源不是文件系统中的普通文件,而是压缩包内的条目,无法直接用 File
API 访问。
所以需要创建一个临时文件,将资源流复制到临时文件:
1 | InputStream soStream = getClass().getResourceAsStream("/example_binaries/libmatch02.so"); |
Unidbg打包
打包步骤参考:【JAVA】使用intellij IDEA将项目打包为jar包_idea打包jar文件-CSDN博客
注意:打包的时候需要勾选Include tests
,因为我们的代码是写在test目录下的。
打包完成后把jar包放到py同目录下。
协议复现
1 | import requests |
应该也是服务器时间不一致的原因,也是有的可能爬取失败。
第三关 so层加密带混淆
同第二关,变成了crypto
函数。
1 | public native String crypto(String str, long j); |
ida打开,发现加载特别缓慢,而且导出函数里找不到目标函数,由于混淆的特别严重,直接放弃分析,改用unidbg。
首先需要hook crypto,查看一下参数内容:
1 | let ChallengeThreeFragment = Java.use("com.yuanrenxue.match2022.fragment.challenge.ChallengeThreeFragment"); |
输出如下:
1 | ChallengeThreeFragment param1: 0011750663330000 |
hook结果与请求包m字段一致:
响应包:
所以直接修改unidbg代码。
1 | package com.yuanrenxue.match2022.fragment.challenge; |
运行,发现每次的结果不一致,猜测可能是so层加了随机数作盐值。
尝试python发包请求:
1 | import requests |
结果如下:
依旧是只有前9条数据。。。