5月中旬的时候,猿人学举行了一个APP爬虫大赛,共设10题,主要涉及Android反混淆,双向认证,tls指纹对抗等技术。而且只需要答对一题就有参与奖,即可获得一件猿人学定制T恤。另外第一题不涉及so,仅涉及java层加密。为了T恤,立马去报了名参赛。
比赛开始的时候,想着下载完APP,然后冲完第一题就完事,但结果发现APP安装都成问题。
图1
查看规则才知道,参赛的APP只支持arm64架构的手机,我的Nexus 5
根本不支持。还好身边有一个marry大佬尊贵的荣耀8,成功安装了APP。
图2
但是又出现了另外的问题,由于手机是安卓版本8.0,也没有root,配置完代理无法抓到该程序的包。对于未root抓包的话,也有很多其他的解决方法,例如可以使用VMOS Pro
,也可以使用VirtualXposed
结合xposed插件来抓包,或者利用objection
重新打包,之后就可以使用objection
来进行测试。
实际上不用抓包也是可以做出几道题的,需要搞清楚请求的参数即可,首先来看第一题。
虽说第一题不涉及so,仅包含java层的加密,但这道题做起来也有一点麻烦,需要抠代码和修改。首先来看一下题目:
图3
10道题都是要爬取1-100页之间的数据,然后求和。
由于APP没有加壳,所以可以直接使用jadx来打开,可以看到代码进行了混淆。
使用
adb shell dumpsys activity top
来找到当前打开的界面,从而定位到com.yuanrenxue.match2022.fragment.challenge.ChallengeOneFragment
进入到
com.yuanrenxue.match2022.fragment.challenge.ChallengeOneFragment
中。
图4
可以很容易就发现加密的关键点,调用了com.yuanrenxue.match2022.security.Sign
的sign方法对一些参数进行了加密。这里sign方法传入的参数是sb.toString().getBytes(StandardCharsets.UTF_8)
,而sb
可以从上面获取。主要代码:
1 StringBuilder sb = new StringBuilder(); 2 sb.append("page="); 3 sb.append(this.page); 4 long longValue = c3756OooOO0O.OooO00o().longValue(); 5 sb.append(longValue);
由代码可以看出,sb.toString()
的内容就是page=
拼接当前页码,和当前时间戳组成的。
图5
进入到o0O0ooO.AbstractC4864OooO0O0
的OooO00o
方法。
图6
可以发现http请求的接口和参数,经过拼接后请求的url是:
https://appmatch.yuanrenxue.com/app1
,参数一共有3个。分别为当前页码page
、加密结果sign
以及时间戳t
,重点看一下如何加密获取sign值的。
使用抓包的话很容易就定位到这里,当时没有抓包,经过尝试也是可以获取到请求的URL。
进入
com.yuanrenxue.match2022.security.Sign.sign
方法,这里推荐用idea打开,可以少走一些弯路。可以对比看一下jadx和idea打开的效果,这里就不放图了。下面是使用idea打开的效果:
图7
从群里发现有老师傅发出抓包请求的内容,刚好可以供我们进行测试。请求的内容:page=1&sign=837056ab8650736b103f193d95ebbc3c&t=1652444336
,看起来sign像是md5,经验证发现并不是。如果直接调用反编译后的new com.yuanrenxue.match2022.security.Sign().sign()
方法,结果也是不正确的。
图8
主要原因可能是因为这些内容是反编译过来的,有些内容可能有所变动。这里的思路是把用到的方法拎出来,然后进行修改。扒完之后一共有这几个文件:
图9
当然运行结果也不是正确的。经过对比,用idea和jadx打开的f
方法内容是不一样的:
图10
将内容修改后,可以成功获取到正确的sign值。
图11
剩下的就简单了,请求1-100页,获取到每页的数据然后求和。这里仅演示获取第一页的数值。
图12
做完了第一题后又看了一下其他的题,发现第二题、第三题、第八题通过使用unidbg
可以很直接的得出结果,第五题采用了双向证书,直接抠代码也是可以做出来。第二题、第三题、第八题做题思路一样,所以放一起来说,最后再说第五题。
第二题就涉及到了so,这不禁令人头大。搞了好久so文件才发现规则中允许使用unidbg
,用其调用so的话简单快捷。这里没有好兄弟发请求的数据了,只能自己动手抓包了。
由于要对app进行抓包和hook查看参数,因此objection
将frida-gadget.so
打包进apk中,使用命令为:
objection patchapk --source yuanrenxuem106.apk
。
然后就可以利用 objection
对app进行分析了。
图13
通过查看反编译后的代码,第二题的话请求一共有3个参数:page
、ts
、sign
,page
是页码,ts
是时间戳,sign
是加密的内容。
图14
可以看到sign是经过了调用so的加密结果,使用objection
查看加密传入的参数。
android hooking watch class_method com.yuanrenxue.match2022.fragment.challenge.ChallengeTwoFragment.sign --dump-args
图15
传入的参数是由page
和ts
进行了拼接,中间由:
连接。知道了调用so的传入的参数,下面就开始用unidbg进行调用。
1public class ChallengeTwoFragment extends AbstractJni { 2 private final AndroidEmulator emulator; 3 private final VM vm; 4 private final Memory memory; 5 private final Module module; 6 7 public ChallengeTwoFragment() { 8 emulator = AndroidEmulatorBuilder 9 .for64Bit() 10 .addBackendFactory(new DynarmicFactory(true)) 11 .build(); 12 memory = emulator.getMemory(); 13 memory.setLibraryResolver(new AndroidResolver(23)); 14 vm = emulator.createDalvikVM(new File("file/app/yuanrenxuem106.apk")); 15 vm.setDvmClassFactory(new ProxyClassFactory()); 1617 // 加载so到虚拟内存 18 DalvikModule dm = vm.loadLibrary("match02", true); 19 module = dm.getModule(); 20 vm.callJNI_OnLoad(emulator, module); 21 } 2223 public String callSign(String data) {//通过符号 24 DvmObject<?> object = ProxyDvmObject.createObject(vm, this); 25 DvmObject<?> dvmObject = object.callJniMethodObject(emulator, "sign(java/lang/String;)java/lang/String;", data); 26 String result = (String) dvmObject.getValue(); 27 return result; 28 } 2930}
unidbg调用so很简单,直接根据demo修改一下就行,需要注意的是要启动64位的模拟器。获取第一页的数据:
图16
第三题的话和第二题类似,so文件虽然进行了加密混淆,但是可以直接使用unidbg来调用so文件。
首先找到第三题的请求参数,一共两个参数page
和m
。
图17
参数m
的值是通过crypto
来进行加密的,一共两个参数,类型分别为String
和long
。
图18
查看一下传入的两个参数内容,使用命令:
android hooking watch class_method com.yuanrenxue.match2022.fragment.challenge.ChallengeThreeFragment.crypto --dump-args
图19
发现第一个参数是页码与时间戳乘以1000来进行拼接的,同时如果页码长度不为3时,需要前面补零,第二个参数为时间戳乘以1000。
第三题和第二题是同样的套路,直接修改一下就可以使用,主要代码:
1public String callCrypto(String data,long l) {//通过符号 2 DvmObject<?> object = ProxyDvmObject.createObject(vm, this); 3 DvmObject<?> dvmObject = object.callJniMethodObject(emulator, "crypto(Ljava/lang/String;J)Ljava/lang/String;", data,l); 4 String result = (String) dvmObject.getValue(); 5 return result; 6}
获取第一页的数据:
图20
第八题与第二题、第三题都是类似的,只不过是so多加了一层upx壳,脱壳后可以通过unidbg来调用。
脱壳命令:upx -d libmatch08.so
图21
查看第八题的参数,发现只有一个参数s
。
图22
通过分析参数s
是调用native层的data
方法来进行加密的,传入的是页码。
图23
通过objection来验证一下:
android hooking watch class_method com.yuanrenxue.match2022.fragment.challenge.ChallengeEightFragment.data --dump-args --dump-return
图24
发现data
方法传入的就是页码,然后使用unidbg来直接调用so文件,主要代码:
1public String callData(int i) {//通过符号 2 DvmObject<?> object = ProxyDvmObject.createObject(vm, this); 3 DvmObject<?> dvmObject = object.callJniMethodObject(emulator, "data(I)Ljava/lang/String;", i); 4 String result = (String) dvmObject.getValue(); 5 return result; 6}
获取第一页的数据:
图25
第五题的话也不难,从网上copy一个双向证书请求的代码就可以来完成。但需要注意几点:一是请求的URL有所变化,二是要找到key,三是如果用java来写的话,要注意jdk的版本,当时就是由于jdk的版本导致当时没做出来,换了个jdk版本,立马就出来结果了。
首先来分析参数,可以看出来请求的路径有所变化,参数的话只有一个,就是页码page
。
图26
通过抓包,可以看到URL也变化了:
图27
通过hook查看一下key的值:
android hooking watch class_method javax.net.ssl.KeyManagerFactory.init --dump-args
图28
发送请求的代码:
1public static String appmatch05(String url, int page) throws IOException { 2 3 String result = null; 4 5 InputStream[] insCerArry = new InputStream[]{}; 6 InputStream insJksDir = new FileInputStream("file/cer/clientCA.bks"); 7 8 HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(insCerArry, insJksDir, "MZ4cozY8Qu32UzGe"); 910 OkHttpClient okHttpClient = new OkHttpClient.Builder()11 .hostnameVerifier(new HostnameVerifier() 12 { 13 @Override 14 public boolean verify(String hostname, SSLSession session) 15 { 16 return true; 17 } 18 }) 19 .sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager) 20 .build(); 2122 RequestBody formBody = new FormBody.Builder() 23 .add("page", page + "") 24 .build(); // 表单键值对 25 Request request = new Request.Builder().url(url).post(formBody).build(); // 请求 26 Response response = okHttpClient.newCall(request).execute(); 2728 if (response.isSuccessful()) { 29 result = response.body().string(); 30 } 31 return result; 32 }
HttpsUtils网上搜索一个抄下来就可以用了。请求第一页数据:
图29
需要注意使用的java版本,使用jdk1.8.0_111时,就会爆出下面的错误:
图30
当时比赛的时候用的jdk1.8.0_111,一直报错,导致找了好多关于双向证书的代码都不行,在比赛结束后,换了jdk版本同样的代码,立即就好了。
本文主要是通过一次APP爬虫的比赛,一方面提供了对于Android 7 及以上系统抓包的一种思路,二是unidbg的初级使用,还有就是双向证书的问题。对于其他grpc、quic、tls等,还需要更深入的学习。另外还要感谢王老板提供这次学习的机会。
[1]unidbg:https://github.com/zhkl0228/unidbg
[2]objection:https://github.com/sensepost/objection/wiki/Patching-Android-Applications
[3]比赛地址:http://appmatch.yuanrenxue.com/