职场中,一直有“金三银四、金九银十”的说法,这指的是一年中求职的两个高峰期.
招聘软件十大排行榜 BOSS直聘、前程无忧51Job、智联招聘、58同城、猎聘、赶集网、拉勾招聘、店长直聘、聘达人、实习僧
如果你想整理一座城市的招聘信息,手动搜索属实是太麻烦了,如果你会爬虫的话,几行代码的事就可以轻松整理出一份城市与岗位的excel表,再配合数据可视化或者词云图可以看的更直观.
这期的内容是上面10大软件中的一个,之前有一期出了个市面上知名度很高的招聘软件,直接被开发警告了,火速删文,对方才得以撤回一条律师函,所以这个名字也是不能透露的.这期主要的是分析登录以及搜索接口的加密参数,涉及的内容有so的加密以及webview的滑块和无感验证码逆向,前面那块是安卓逆向中的内容,后面是js逆向的内容了.
我创建了个js 安卓 ios逆向交流群,欢迎加入交流技术!在最后面,7天有效.
学习交流,请勿违法使用!!!
登录
抓包
中间有4个值是极验的产品,校验通过后返回的geetestValidate,这个验证码有时候是滑块,有时候是无感.
密码我输入的是123456,加密成了767367756c66,视乎看不出来什么规律,换个密码再加密一遍,123456123456,加密成767367756c666366656b6e69,长度变成了,可能是对称加密算法,比如des或者aes.目前来看就是需要逆向掉极验的验证码还有密码加密就可以自动登录了.
password
先拿算法助手hook看一下是不是java层的加密,小公司可以先验证一下,大公司就别想了,肯定是so的.
军哥的算法助手地址 https://github.com/Xposed-Modules-Repo/com.junge.algorithmaide
需要先在xposed环境下安装好,xp里面也要勾选上目标应用,然后就是算法助手内部也勾上.
这些就不介绍了,字面意思应该也能看懂.往下滑还有需要开启的地方.
webview可调式以及弹窗定位,无感或者是滑块都有一个弹窗,可以通过这个定位到关键函数.webview可调式开启后,如果app使用的是webview的滑块那就需要开启浏览器远程调试才能逆向,后面再说这个,默认app是会关掉这个webview可调式的,当然也可以用frida来hook掉,只不过xp更持久一点.
setTimeout(function(){
Java.perform(function() {
var Webview = Java.use("android.webkit.WebView")
Webview.loadUrl.overload("java.lang.String").implementation = function(url) {
console.log("\n[+]Loading URL from\n", url);
console.log("[+]Setting the value of setWebContentsDebuggingEnabled() to TRUE");
this.setWebContentsDebuggingEnabled(true);
this.loadUrl.overload("java.lang.String").call(this, url);
}
});
},0);
都勾上后重启app,登录一下,在算法助手的日志里搜索一下123456加密成的767367756c66
没搜到,可能是java层的自写算法或者是so的加密,先来尝试下定位到关键函数.
查壳
不过这个pkid作者不更新了,这个版本太老了,很多新的安全产商没有融入进去比如网易易盾,几维安全等等,如果你反编译的代码感觉很少或者缺少关键类很有可能是加固了但是没有检测出来,推荐用mt管理器查看一下,因为mt管理器是一直更新的.这个我看了下确实是没有加固的,可能是觉得有极验可以保护他.
代码定位
没加固直接扔到jadx里就好了,你可以尝试搜索"password",但是这样结果很多的话就显得麻烦了.
hook hashmap.put
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {
if(a!=null && a.equals("password")){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
console.log("hashMap.put: ", a, b);
}
return this.put(a, b);}
额,什么输出也没有,那应该不是通过hashmap添加到表单里的.接下来就不是很好定位了,因为它既没有base64,也没有拼接什么特殊字符串,无法hook base64或者stringbuilder.你可以返回去尝试最原始的定位方式,搜索大法,只不过会比较费时间,还不一定能定位到.
我觉得它有可能是so的算法,所以我hook了NewStringUTF,前提是结果通过字符串返回去的,如果是字节数组就不是那么好hook了.也可以hook下GetStringUTFChars,这个是jstring转cstring的,可以刷选下jstring里面是否有传入的明文123456来定位.这两种方法对这个app都有效.
NewStringUTF
var addrNewStringUTF = null;
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) {
addrNewStringUTF = symbol.address;
console.log("NewStringUTF is at ", symbol.address, symbol.name);
}
}
if (addrNewStringUTF != null) {
Interceptor.attach(addrNewStringUTF, {
onEnter: function (args) {
var c_string = args[1];
var dataString = c_string.readCString();
if (dataString) {
if (dataString=='767367756c66') { //这里写要对字符串的筛选
console.log(dataString);
//读取当前是在那个so文件,那个地址
// console.log(Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n');
//先用下面的
// console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
}
},
});
}
结果
NewStringUTF is at 0x74d81bf680 _ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc
767367756c66
java.lang.Throwable
at com.zhaopin.social.jni.NdkTool.encryptPwd(Native Method)
at com.zhaopin.social.passport.usecase.CPwdLoginUseCase.c(CPwdLoginUseCase.kt:8)
at com.zhaopin.social.passport.usecase.CPwdLoginUseCase$invoke$1.invokeSuspend(Unknown Source:16)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:4)
at kotlinx.coroutines.v0.run(DispatchedTask.kt:18)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
优先看最上面的堆栈,com.zhaopin.social.jni.NdkTool下的encryptPwd方法.
是一个native方法,直接右键复制frida片段看看入参是什么
let NdkTool = Java.use("com.zhaopin.social.jni.NdkTool");
NdkTool["encryptPwd"].implementation = function (str) {
console.log(`NdkTool.encryptPwd is called: str=${str}`);
let result = this["encryptPwd"](str);
console.log(`NdkTool.encryptPwd result=${result}`);
return result;
};
入参就是输入的,结果也是包里面的. so文件是_zhaopin_v1.0
只有64位的,用ida64打开
so分析
搜一下java 有结果,静态注册,点过去
先改jnienv对象,这样有些jni的方法可以显示出来,燃弧入参改一下input,这样更明显
没改jnienv对象前,看不出来这个是jni方法
改完之后就可以看到GetStringUTFChars和NewStringUTF了,分别是jstriing转cstring和cstring转jstring.
没有混淆,直接看静态代码配合frida来hook应该问题不大.
这里是主程序逻辑,这伪c代码写得很清晰了.中间有个sub_EFE4函数,frida hook看一下,它在一个while循环里,可以添加一个数字看他调用了几次
// EFE4
var soAddr = Module.findBaseAddress("lib_zhaopin_v1.0.so");
var funcAddr = soAddr.add(0xEFE4) //32位的话记得+1
var num = 0
Interceptor.attach(funcAddr,{
onEnter: function(args){
num+=1
console.log(`第${num}次:`)
console.log('onEnter arg[0]: ',args[0])
console.log('onEnter arg[3]: ',args[3])
// this.arg0 = args[0]
},
onLeave: function(retval){
// console.log('onLeave arg[]: ')
// console.log('onLeave result: ',retval)
}
});
在此之前,可以先写一个java层的主动调用,这样就可以让手机息屏了,或者是关掉wifi,这样可以减少网络请求,如果有很多请求走这个函数就会输出很混乱,主动调用可以确保就调用一次.
function call(){
Java.perform(function (){
let NdkTool = Java.use("com.zhaopin.social.jni.NdkTool");
var res = NdkTool["encryptPwd"]('123456')
console.log('res:',res)
})
}
终端call()一下,报错不能调一个方法而没有实例
上面看到了native方法没有加static,所以这个是一个实例方法,需要先实例化一个对象,加一个.$new()即可
function call(){
Java.perform(function (){
let NdkTool = Java.use("com.zhaopin.social.jni.NdkTool");
var res = NdkTool.$new()["encryptPwd"]('123456')
console.log('res:',res)
})
}
主动调用后就有结果了,接下来配合着主动调用来hook so的sub_EFE4函数
入参1是一个地址,入参4就是password加密的结果未拼接之前,最终结果是767367756c66,这个是入参并不是执行后的结果,看下入参4是什么
就是aEa4a7024279346[v9 & 0x3F] + v7,v7来着v4也就是入参,123456第一位的1的十六进制是0x31,
aEa4a7024279346[v9 & 0x3F]是在根据索引取数组的值,v9刚开始是0,循环一次加1,最后与0x3f位与.
很清晰了,刚开始0 & 0x3f = 0,aEa4a7024279346的第0个是什么?
28F46偏移处都是aEa4a7024279346的,第0个是0x45,0x45+0x31=0x76,对上了,后面的也是这样的流程,相信不需要我多说了.
验证码
app端的滑块怎么分析呢?
App端滑块一般由原生组件或在WebView 中使用 Web 技术来实现,为了不重复构建代码做到跨平台兼容性,一般都是使用webview实现滑块,并且app端滑块与web端滑块加密一般都相同,在加密的入参上可能会有一些区别;
那么针对webview滑块该如何进行分析呢?在 Android 应用程序中,WebView 可以理解为一个嵌入式的浏览器引擎。和我们分析web端滑块一样,在谷歌开发者工具中进行断点调试就行。但是大多数情况下,apk打包出来都会禁止WebView调试,这时我们需要hook WebView让它可调式。在最上面已经说过了webview可调式.
devtools开启调试
手机usb连上电脑,浏览器地址栏输入chrome://inspect/#devices,edge浏览器就换成edge://inspect/#devices,其他的类似.等待一会.
点击inspect进去
之后就和浏览器调试一样了,相信不需要我多说了,做js逆向的应该都懂,极验滑块应该是js逆向接触的第一种滑块,网上教程很多,应该不需要我这里多说了.
无感webview
由于无感太快了,2到3秒内就验证完成了,但此时浏览器的inspect页面还没有出现可调式就结束了,这个怎么解决呢
我尝试了用charles添加断点把极验的几个请求都打上了断点
天真的我以为他会像浏览器一样直接断住
charles中是断住了,但是后续一会app没有收到响应直接网络超时后弹窗就消失了,这样也开启不了调试,只可以去hook java中的函数了,让这个弹窗刚出现的时候hook上让他延时加载,这样理论就有足够的时间等待浏览器的inspect界面出现可调式,hook这个弹窗前面的算法助手就可以hook上,再回溯堆栈就能定位到弹窗的位置了.点击inspect进去的时候可能需要科学上网环境,需要访问谷歌的站点下载些组件.
搜索接口
需要先登录才能搜索
抓了两个搜索的包,初看似乎没有什么加密参数
总结
解决了登录和搜索的问题就可以越快的编写爬虫程序了,账号风控的话可能还需要多账号.本章主要就是介绍一下webview的滑块,以及so的逆向,虽然你看我上面说的似乎挺简单的,但是到你自己独立去弄一个app的so的时候没有别人写的分析文章可能就有些吃力了,所以还是需要自己多练,毕竟这个也只能算是个比较简单的自写算法,也没有混淆什么的.
说到混淆就必须看看这个图了,上图,自行欣赏!
最后
知识星球
微信公众号
技术交流群