记录一次秀动App逆向过程
目标应用:秀动
目标版本:5.2.7
![CleanShot 2024-04-27 at 08.48.58](https://img-blog.csdnimg.cn/img_convert/e3097ad86ff368117ba829ca9486806c.jpeg)
frida启动后发现有检测,尝试使用魔改frida过掉检测
![CleanShot 2024-04-27 at 08.49.56](https://img-blog.csdnimg.cn/img_convert/bcf035e856a37c27856c8e9dfc5caf56.jpeg)
发现可以成功挂起
![CleanShot 2024-04-27 at 08.50.15](https://img-blog.csdnimg.cn/img_convert/fc33a3d0da4e8018ed53c829db104660.jpeg)
![CleanShot 2024-04-27 at 08.50.45](https://img-blog.csdnimg.cn/img_convert/bd48656a3389ebafe2c70aedc0930f36.jpeg)
使用魔改frida成功过掉检测,现在开始分析具体的frida检测。
魔改frida已经随文件打包,大家可以去github支持下作者。
[!tip]
注意,遇到有frida检测的样本尽量要-f挂起,如果-F可能造成手机卡死重启,耽误调试进度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | function hook_open() { / / https: / / blog.csdn.net / a656343072 / article / details / 40539889 / * 函数原型: int open ( const char * pathname, int oflags); int open ( const char * pathname, int oflags, mode_t mode); mode仅当创建新文件时才使用,用于指定文件的访问权限。 pathname 是待打开 / 创建文件的路径名; oflags用于指定文件的打开 / 创建模式,这个参数可由以下常量(定义于 fcntl.h)通过逻辑或构成。 O_RDONLY 只读模式 O_WRONLY 只写模式 O_RDWR 读写模式 以上三者是互斥的,即不可以同时使用。 * / var open_addr = Module.findExportByName( "libc.so" , "open" ) var io_map = Memory.allocUtf8String( "/proc/13585/maps" ); Interceptor.attach(open_addr, { onEnter: function (args) { console.log( 'targetFunction called from:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE) . map (DebugSymbol.fromAddress).join( '\n' ) + '\n' ); if (args[ 0 ].readCString().indexOf( "/proc/" )! = - 1 && args[ 0 ].readCString().indexOf( "maps" )! = - 1 ){ args[ 0 ] = io_map / / ptr(args[ 0 ]).writePointer(Memory.allocUtf8String(args[ 0 ].readCString().replaceAll( / \d + / g, "1" ))) / / args[ 0 ] = Memory.allocUtf8String( "/proc/1/maps" ) / / Memory.protect(ptr(args[ 0 ]), args[ 0 ].readCString().length, 'rwx' ); / / var value_new_str = Memory.allocUtf8String( "/proc/1/maps" ) / / console.log( "args0=" + args[ 0 ].readCString()) / / ptr(args[ 0 ]).writeByteArray([ 0x2f , 0x70 , 0x72 , 0x6f , 0x63 , 0x2f , 0x32 , 0x2f , 0x6d , 0x61 , 0x70 , 0x73 , 0x0 ]) / / console.log( "args0=" + args[ 0 ].readCString()) } this.pathname = args[ 0 ] this.oflags = args[ 1 ] this.mode = args[ 2 ] }, onLeave: function (retval) { console.log( "retval=" + retval + "---" + "pathname=" + this.pathname.readCString() + "---oflags=" + this.oflags) if (this.pathname.readCString().indexOf( "libmsaoaidsec" )! = - 1 ){ / / 过了惠头条 retval.replace( 0xffffffff ) } console.log( "open pathname=" + this.pathname.readCString() + "---oflags=" + this.oflags) if (this.pathname.readCString().indexOf( "/proc/" )! = - 1 && this.pathname.readCString().indexOf( "maps" )! = - 1 ){ retval.replace( 0x0 ) } } }) } |
我们来hook一下open函数 并打印堆栈,自下而上找到frida检测逻辑代码的点,并学习此检测。
![CleanShot 2024-04-27 at 09.13.08](https://img-blog.csdnimg.cn/img_convert/4175780c78e751aa32285583938a9da4.jpeg)
发现有一个so在不断获取/proc下的状态信息
1 2 3 4 5 6 | retval = 0x7b - - - pathname = / proc / 13205 / status - - - oflags = 0x0 open pathname = / proc / 13205 / status - - - oflags = 0x0 targetFunction called from : 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 |
![CleanShot 2024-04-27 at 09.14.38](https://img-blog.csdnimg.cn/img_convert/35ab42bda963235d71a7b41999970304.jpeg)
搜索发现这貌似是一个sdk的附属so,不是作者自写so。
据搜集发现,应该是和webview的实现有关
故pass
[!tip]
逆向工程中搜集信息是一个非常重要的环节,不要吝惜你的谷歌不用
有很多大佬已经给你铺好了路
这句话写给你们,也写给我自己,这真的很重要
[!note]
在寻找frida的各种方法中,我们分为两条路线:
一种是以anti-frida-su.js为主的,去满足正常环境的要求,去各种抹除掉frida注入后的种种痕迹,但是这是致命的,因为frida有无数种特征,你无论如何是抹除不完的,不如重新按照frida写一个自己的HOOK工具。但是幸运的是,有无数大佬为我们铺路,例如非虫大佬的11个patch,能够过掉市场上百分之70的检测,但这是远远不够的。
CRC检测的出现让魔改的frida也无法招架得住。例如,对libart.so的prettymethod的方法的不可规避的注入,让frida无处遁形。 我们该怎么办?
非虫大佬已经给出了答案:修改hook pretymthod的hook时机,这样能过掉百分之5左右的样本(为了节约用户硬件资源,启屏后就会关闭),但是大部分样本还是通过开启线程进行crc循环检测
第一种方法变得更加曲折起来,必须要配合魔改rom以及linux内核来进行进一步隐藏。
但是请注意,crc检测必须要开启线程来检测,我们来讨论第二种过掉的方法,patch线程
在正式操作之前,我想和大家讨论检测粒度问题:
[!note]
请大家思考,frida的检测粒度一般在什么级别,我们先做假设,假
如我是一个开发人员,我在进行界面跳转,再或者数据请求后,加几句话,对23946端口的检测(frida检测一个例子,当然没有那么简单),那么frida检测我们可以认为是语句级别的粒度,因为只有几行代码,我们也无法避免,因为我们要进行操作。
接上部分,我们公司有安全开发人员,给了我一个函数,我不用考虑别的,直接调用即可,那frida检测我们可以认为是函数级别的粒度
如果我们公司没有开发人员怎么办,当然是外包啦,引入一个安全公司的so,打钱打钱。
接上面的讨论,第一种情况几乎不存在,我既要懂业务也要懂安全,除非我自己是全栈(这种适用于mini App)各大H播软件可能会出现。
第二种情况可能存在,也就是业务代码与安全代码掺杂,也是我们不希望看到的情况,一个线程里既有检测代码,也有业务逻辑代码。即patch掉线程业务也无法运行
第三种情况是最常见的,一些安全的sdk,对设备评估,基本数据请求加密,以及反调试的实现。
第三种情况也要分两种方式讨论,第一种,纯检测so,没有任何加密行为
我给他归结为so粒度的,那么直接跳过so加载即可。
第二种,安全公司提供的so,内部有设备id等加密方式计算,那么跳过就不是那么简单了 我们可以定位so加载的头部位置,在怀疑后,检测so的各个段的加载,定位so大致位置,来进行patch反调试
使用spawn方法启动frida(不要使用魔改的,看不到退出时间点)
![CleanShot 2024-04-27 at 09.48.48](https://img-blog.csdnimg.cn/img_convert/1702a82b19776e31c8f6fe6533c6d98e.jpeg)
发现开启
1 | normal find thread func offset libshell - super .com.showstartfans.activity.so 0x765f7450d0 360656 580d0 |
这条线程的时候,软件挂掉了
我们增加脚本的过滤条件
1 2 3 | else if (so_name.indexOf( "libshell-super.com.showstartfans.activity.so" )> - 1 && offset = = 360656 ){ } |
![CleanShot 2024-04-27 at 09.58.11](https://img-blog.csdnimg.cn/img_convert/b92706b847439ba7aa422f8643e4a00d.jpeg)
发现成功过掉了frida检测
失败案例:
没有过滤offset==360656,软件直接发生了崩溃,反正鼓励大家多试试
![CleanShot 2024-04-27 at 09.59.04](https://img-blog.csdnimg.cn/img_convert/e7480b6ef5c7da23221029d12025b9da.jpeg)
ps:本文章不讨论脱qiao,脱qiao好的已经在根目录了,请大家合法使用
打开抓包后提示网络异常,关闭后就不异常了,确定为抓包检测。
[!tip]
这一步抓包检测(非证书教研式),有一种特殊方式,需要软路由。
以及透明代理(非证书教研式),考虑到第一种需要设备,第二种需要docker云手机,这里不做讨论。
如何进行抓包检测定位?首先找到登陆的activity,找到登陆函数,一步一步往下跟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | var jclazz = null; var jobj = null; function getObjClassName(obj) { if (!jclazz) { var jclazz = Java.use( "java.lang.Class" ); } if (!jobj) { var jobj = Java.use( "java.lang.Object" ); } return jclazz.getName.call(jobj.getClass.call(obj)); } function watch(obj, mtdName) { var listener_name = getObjClassName(obj); var target = Java.use(listener_name); if (!target || !mtdName in target) { return ; } / / send( "[WatchEvent] hooking " + mtdName + ": " + listener_name); target[mtdName].overloads.forEach(function (overload) { overload.implementation = function () { / / send( "[WatchEvent] " + mtdName + ": " + getObjClassName(this)); console.log( "[WatchEvent] " + mtdName + ": " + getObjClassName(this)) return this[mtdName]. apply (this, arguments); }; }) } function OnClickListener() { Java.perform(function () { / / 以spawn启动进程的模式来attach的话 Java.use( "android.view.View" ).setOnClickListener.implementation = function (listener) { if (listener ! = null) { watch(listener, 'onClick' ); } return this.setOnClickListener(listener); }; / / 如果frida以attach的模式进行attch的话 Java.choose( "android.view.View$ListenerInfo" , { onMatch: function (instance) { instance = instance.mOnClickListener.value; if (instance) { console.log( "mOnClickListener name is :" + getObjClassName(instance)); watch(instance, 'onClick' ); } }, onComplete: function () { } }) }) } setImmediate(OnClickListener); |
注入监听脚本,并点击按钮
[22041216C::秀动 ]-> [WatchEvent] onClick: com.showstartfans.activity.activitys.login.XDLoginActivity
![CleanShot 2024-04-27 at 10.32.13](https://img-blog.csdnimg.cn/img_convert/d7a20da5f7379ca34bbbf0a55c2c8885.jpeg)
我们可以看到账号密码的位置,往下跟,进入this.Z函数
![CleanShot 2024-04-27 at 10.33.36](https://img-blog.csdnimg.cn/img_convert/93c9b5c3ae6d12efe11ffde777eb6512.jpeg)
发现这是在组装登陆bean 返回上一层
进入j函数
![CleanShot 2024-04-27 at 10.35.37](https://img-blog.csdnimg.cn/img_convert/4c398412bfa8b30d96ea90bac8bf6885.jpeg)
![CleanShot 2024-04-27 at 10.35.53](https://img-blog.csdnimg.cn/img_convert/5e011c8caa4ef1a88d8032f8228aaa6f.jpeg)
继续往下跟
![CleanShot 2024-04-27 at 10.36.12](https://img-blog.csdnimg.cn/img_convert/8faf0819119dbb056ec28efc6ff25b7e.jpeg)
发现进入了okhttp逻辑
我们进入n.j这个函数
![CleanShot 2024-04-27 at 10.40.14](https://img-blog.csdnimg.cn/img_convert/fa3d973e10b0f20cfce6316fbaa77704.jpeg)
判断d函数在组装client,这里可能会设置禁止代理相关函数
[!tip]
建议大家自己开发一个okhttp的应用跟一下,有助于文章理解
![CleanShot 2024-04-27 at 10.41.24](https://img-blog.csdnimg.cn/img_convert/9985b043b5b185fae14f0968df4d4c71.jpeg)
进入g(context)
![CleanShot 2024-04-27 at 10.41.42](https://img-blog.csdnimg.cn/img_convert/21cf61e91bdb1c0ee38ea6d1b91498f0.jpeg)
发现第一处代理检测点
1 2 3 | if (!x0.g()) { builder.proxy(Proxy.NO_PROXY); } |
X0.g是一个函数,疑似代理管理类
![CleanShot 2024-04-27 at 10.42.32](https://img-blog.csdnimg.cn/img_convert/976968cf86788082dfe6525d95fdaa7e.jpeg)
发现确实是这样的,相关的代理检测逻辑都在这里
![CleanShot 2024-04-27 at 10.43.03](https://img-blog.csdnimg.cn/img_convert/495f4a1f83f9fc24f4b6f52f82f09fbd.jpeg)
![CleanShot 2024-04-27 at 10.43.55](https://img-blog.csdnimg.cn/img_convert/7e8e9e1666e2836ddd09c2ecd759d4d4.jpeg)
这里调用后,决定我们是否被检测到是开启代理,核心逻辑在本类的i函数里
1 2 | NetworkCapabilities networkCapabilities; return (network = = null || connectivityManager = = null || (networkCapabilities = connectivityManager.getNetworkCapabilities(network)) = = null || !networkCapabilities.hasTransport( 4 )) ? false : true; |
这个教给大家自己去学习了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Java.perform(function () { var x0Class = Java.use( "i.a0.a.n.x0" ); / / 确保方法存在 if (x0Class.i.overloads.length > 0 ) { / / Hook 所有重载版本(如果有多个的话) x0Class.i.overloads.forEach(function (overload) { overload.implementation = function () { console.log( "i.a0.a.n.x0.i method hooked" ); / / 返回 false return false; }; }); } }); |
![CleanShot 2024-04-27 at 10.48.27](https://img-blog.csdnimg.cn/img_convert/4fed88c9888980dfce1e7ba0aeff9c8d.jpeg)
![CleanShot 2024-04-27 at 10.48.45](https://img-blog.csdnimg.cn/img_convert/37ae7a524dc05f4848570f804c27001d.jpeg)
这次我们打算分析crpsign这个参数
![CleanShot 2024-04-27 at 10.49.34](https://img-blog.csdnimg.cn/img_convert/7f840f667638e703796a8a8238efb186.jpeg)
jadx直接搜就搜到了
![CleanShot 2024-04-27 at 10.49.54](https://img-blog.csdnimg.cn/img_convert/ccc1a88d374f5f6bacfa2dea8ede1ea0.jpeg)
发现来源正是这里的native函数
showstart_net.so
使用龙哥的 ida脚本 findhash一把梭,可以直接找到加密位置,发现是个标准的md5加盐,结合入参就能分析成功
[!note]
值得讨论的点:这个so是rust配合开发的,
下单接口的请求体和相应体都是加密的,就在上面的so中实现,没有找到任何aes的特征,但是找到了rust库的aes包的引用
推测原因1:rust数据结构和标准的c不一样,就连字符串结尾都不是以\0结尾的
导致反编译出的so贼乱
解决办法:开发rust aes 并提取特征点写出插件
在rust和jni交互中,rust似乎实现了自己的一套runtime,有一些系统调用unidbg无法跑起来,希望有大佬一起来研究下!如果unidbg补好了,那么接下来所有rust开发的so都可以进行基本的补环境的能力。
感兴趣的大佬可以继续研究与我交流