问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
记一次帮丈母娘破解APP,满满的全是思路
移动安全
事情是这样的,家里人从网上买了一个定位器,买之前也没问客服,结果到手之后一看竟然要收费才能使用,由于本来没多少钱后续就没退,然后我听了之后来了兴致,我想着能不能有什么漏洞能白嫖,于是有了本文,虽然最后解决也不难,不过我觉得最重要是满满的全是思路。
记一次帮丈母娘破解APP,满满的全是思路 ==================== 事情是这样的,家里人从网上买了一个定位器,买之前也没问客服,结果到手之后一看竟然要收费才能使用,由于本来没多少钱后续就没退,然后我听了之后来了兴致,我想着能不能有什么漏洞能白嫖,于是有了本文,虽然最后解决也不难,不过我觉得最重要是满满的全是思路。 主要是下面两个东西 - 定位器,里面有一张它出厂带的卡 - 查询位置所用的app一个 定位器这东西,我是没有lot经验所以就先不用看了,直接看安卓app 0x01 初步研究app ------------ 拿出我的测试机,进入到app中会让你登录,登录的账号就是`定位器`的初始id,密码是123,进入后由于没有花钱,会出现一个弹窗,仔细看下图发现在弹窗后边我的设备显示在线,那是不是就意味着我只要让这个倒霉弹窗不再出现,我就可以点到后边我的设备,然后进行相关操作,于是想着关弹窗这种小活这不伸手就来,没想到这么简单,想想还有点小激动  于是有了第一个思路,想着先用`开发助手`定位弹窗组件,然后用`算法助手pro`直接拦截掉,但是当我定位组件的时候发现这个组件没ID,如图这个id名字什么的竟然是空的,神奇。  突然灵光一现,会不会是它不是个弹窗,由于后边的背景色是灰色,想着难不成直接弄了个`Activity`放到了前面 正想着,于是就有了第二个想法,直接用`MT`定位Activity,然后直接拦截这个Activity,太聪明了,没想到这么简单,想想还有点小激动 但是当我定位的时候发现,只有一个`MainActivity`这说明我的想法并不成立,难不成它还是个组件,只不过没名字  算了,正想着着实有点头痛,干脆直接看它代码吧,于是第三个思路,通过`待激活`关键字定位到相关代码,于是我下意识的拿出了我的脱壳机,不过没想到这家伙竟然没壳  正想着原来是我高看它了,那不分分钟手到擒来 直接拖到`jadx`搜索`待激活`,结果竟然没有  搜索`unicode`编码也没有  这让我百思不得其解,再尝试搜索了其余的几个关键字没有结果后,没有办法的我没了办法,于是想着那干脆从流量层面看看吧 0x02 流量侧心酸突破历程 -------------- 直接设置系统代理到我的`Burp`,然后打开发现  666,竟然还有代理检测,于是直接上`Postern`,走vpn代理到茶杯狐`Charles`的socks端口,结果还是不行  没办法了,只好拿出了神器`frida` ```shell adb shell su cd /data/local/tmp/ #测试机,启动frida服务 ./frida-server-16.0.11-android-arm64 ``` 在本地新建了一个过代理检测的通杀脚本(来源:阿呆攻防) ```js function wifi1_proxy_bypass(){ Java.perform(()=>{ var systemCls = Java.use('java.lang.System'); systemCls.getProperty.overload('java.lang.String').implementation = function (val) { var ret = this.getProperty(val); if (val == "http.proxyHost") { return "" } if (val == "http.proxyPort") { return "-1" // 这里改""/"0"/"-1",我这里留的-1是之前金融项目好几家都是-1 } return ret } }) } function wifi2_proxy_bypass(){ Java.perform(function () { var ConnectivityManager = Java.use('android.net.ConnectivityManager'); ConnectivityManager.getLinkProperties.implementation = function (network) { var linkProperties = this.getLinkProperties(network); if (linkProperties) { var ProxyInfo = Java.use('android.net.ProxyInfo'); var proxyInfo = ProxyInfo.$new(null, null, 0); linkProperties.setHttpProxy(proxyInfo); } return linkProperties; }; }); } function vpn1_bypass(){ Java.perform(()=>{ var ConnectivityManager = Java.use('android.net.ConnectivityManager'); ConnectivityManager.getNetworkInfo.overload('int').implementation = function (networkType) { var result = this.getNetworkInfo(networkType); if (networkType === ConnectivityManager.TYPE_VPN.value) { return null; } return result; }; }) } function vpn2_bypass() { Java.perform(() => { // 获取 NetworkCapabilities 类 var NetworkCapabilities = Java.use('android.net.NetworkCapabilities'); // Hook hasTransport 方法 NetworkCapabilities.hasTransport.overload('int').implementation = function (transportType) { // 如果检测到 TRANSPORT_VPN,返回 false if (transportType === NetworkCapabilities.TRANSPORT_VPN.value) { console.log("[*] VPN 检测被绕过"); return false; } // 否则调用原始方法 return this.hasTransport(transportType); }; console.log("[*] NetworkCapabilities.hasTransport 已 Hook"); }); } function bypass_proxy_main(){ wifi1_proxy_bypass() vpn1_bypass() } setImmediate(bypass_proxy_main) ``` 然后直接frida进行hook ```shell .\frida.exe -U [APP进程] -l .\hook.js ``` 发现正常请求走的通了,不过这个时候我的茶杯狐还没有抓取`https`流量,也就是说https的流量会经过茶杯狐,不过没有抓取ssl会导致它并不会利用茶杯狐的ssl证书做中转,我也就看不到对应的`http`报文的明文,看到的都是密文,不过好在软件使用的服务器的域名是知道了  域名到手  既然如此直接装好证书并信任,抓ssl,不点开没事,一抓https又完犊子了,这个时候我尝试了别的https请求是能抓到的,就这个app抓不到  666,竟然有校验,没关系,盲猜连壳都没有的app,撑死一个单向证书校验,把`LSB`的`JustTrustMe`一开,心想这不就成了,没想到这么简单  发现还是不行 难不成`双向证书校验`?由于我知道域名了,直接访问对方域名,发现提示400,不过报错信息跟正常的双向证书不一样,我想着一定是对方伪装了一下,兵法有云,实则虚之虚则实之,小小诡计岂能瞒得过我,双向证书校验,绝对双向证书校验  然后我尝试搜了一下apk解包后有没有常见后缀`p12`、`cer`、`jks`等等的文件,发现并没有,按理说不能,这时想到难不成是伪装成了某个png文件,然后使用的时候在代码中又还原了出来?于是在代码中一顿找最后也没有。 思路转变一下,既然找不到证书,那么它加载本地证书的时候肯定是要读取本地证书文件的 既然如此废话不多说直接上`r0capture`,具体使用方法不多赘述,直接看它项目首页介绍https://github.com/r0ysue/r0capture 恭喜你猜对了又是0收获,既然如此我又用`objection`尝试HOOK了`java.io.File.$init`想着你总要读文件的吧 ```shell objection -g [app项目名] explore --startup-command "android hooking watch class_method java.io.File.$init --dump-args ``` 结果如你所想又是毫无收获,我彻底麻了,我原本以为一个连壳都不上的app能有多难搞,没想到这么强,于是跟朋友调侃了一下,就像斗地主一样,人家牌太好了直接明牌跟你玩不行吗 0x03 流量侧成功突破 ------------ 实在没得办法了,于是又冒出一个想法既然java代码搜不到,是不是在`so`层里面,说着看了眼lib目录,正想着这么多我选那个先分析好呢,突然看到`libflutter.so`等会,我记得flutter不是个语言吗?  于是直接开启上网冲浪模式,一顿冲浪下来,ok有解了 > 先说一下`flutter`,Flutter 是一个由 Google 开发的 **开源 UI 框架**,用于构建跨平台应用,也就是说,用一套代码可以同时生成 **iOS、Android、Web 和桌面(Windows、macOS、Linux)** 应用。 Flutter使用Dart编写,因此它不会使用系统CA存储,Dart使用编译到应用程序中的CA列表,Dart在Android上不支持代理,所以这就是为什么一开始使用系统代理没有生效的原因 当我们的应用存在`libflutter.so`的时候,其实就可以判断大概率为flutter的 还有一种方法是通过`flutter`的日志,如果有输出不仅可以判断出app是flutter写的,还看到对应的日志 ```shell adb shell su logcat |grep flutter ``` 可以看到这个应用的所有请求和响应都在日志中  下面这个函数是`flutter`的证书校验 ```c++ static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session, SSL_HANDSHAKE *hs, uint8_t *out_alert) { *out_alert = SSL_AD_INTERNAL_ERROR; STACK_OF(X509) *const cert_chain = session->x509_chain; if (cert_chain == nullptr || sk_X509_num(cert_chain) == 0) { return false; } SSL *const ssl = hs->ssl; SSL_CTX *ssl_ctx = ssl->ctx.get(); X509_STORE *verify_store = ssl_ctx->cert_store; if (hs->config->cert->verify_store != nullptr) { verify_store = hs->config->cert->verify_store; } X509 *leaf = sk_X509_value(cert_chain, 0); const char *name; size_t name_len; SSL_get0_ech_name_override(ssl, &name, &name_len); UniquePtr<X509_STORE_CTX> ctx(X509_STORE_CTX_new()); if (!ctx || !X509_STORE_CTX_init(ctx.get(), verify_store, leaf, cert_chain) || !X509_STORE_CTX_set_ex_data(ctx.get(), SSL_get_ex_data_X509_STORE_CTX_idx(), ssl) || // We need to inherit the verify parameters. These can be determined by // the context: if its a server it will verify SSL client certificates or // vice versa. !X509_STORE_CTX_set_default(ctx.get(), ssl->server ? "ssl_client" : "ssl_server") || // Anything non-default in "param" should overwrite anything in the ctx. !X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(ctx.get()), hs->config->param) || // ClientHelloOuter connections use a different name. (name_len != 0 && !X509_VERIFY_PARAM_set1_host(X509_STORE_CTX_get0_param(ctx.get()), name, name_len))) { OPENSSL_PUT_ERROR(SSL, ERR_R_X509_LIB); return false; } if (hs->config->verify_callback) { X509_STORE_CTX_set_verify_cb(ctx.get(), hs->config->verify_callback); } int verify_ret; if (ssl_ctx->app_verify_callback != nullptr) { verify_ret = ssl_ctx->app_verify_callback(ctx.get(), ssl_ctx->app_verify_arg); } else { verify_ret = X509_verify_cert(ctx.get()); } session->verify_result = X509_STORE_CTX_get_error(ctx.get()); // If |SSL_VERIFY_NONE|, the error is non-fatal, but we keep the result. if (verify_ret <= 0 && hs->config->verify_mode != SSL_VERIFY_NONE) { *out_alert = SSL_alert_from_verify_result(session->verify_result); return false; } ERR_clear_error(); return true; } ``` 所以我们只需要hook这个函数,然后让其返回值为真即可,操作如下 1. 首先找到lib目录下对应系统架构文件夹下的`libflutter.so`,拖入`ida` 2. 然后看下方左下角等待`ida`加载完毕,如果没加载完毕就搜索很大概率搜不到 3. 先`Shift+F12`打开字符串窗口,然后`Ctrl+F`搜索关键字`ssl_server`  4. 双击跳转过去后,按`Ctrl+X`查找交叉引用,如下图所示,`6C4B4C`就是函数对应地址  5. 编写hook函数 ```js function ssl_attack(){ Java.perform(() => { var base = Module.findBaseAddress("libflutter.so"); console.log("base: " + base); var ssl_crypto_x509_session_verify_cert_chain = base.add(0x6c4b4c); Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, { onEnter: function(args) { }, onLeave: function(retval) { console.log("校验函数返回值: " + retval); retval.replace(0x1); } }); }); } ``` 至此我们可以成功解决了流量侧的对抗问题,整体代码如下 ```js function wifi1_proxy_bypass(){ Java.perform(()=>{ var systemCls = Java.use('java.lang.System'); systemCls.getProperty.overload('java.lang.String').implementation = function (val) { var ret = this.getProperty(val); if (val == "http.proxyHost") { return "" } if (val == "http.proxyPort") { return "-1" // 这里改""/"0"/"-1",我这里留的-1是之前金融项目好几家都是-1 } return ret } }) } function wifi2_proxy_bypass(){ Java.perform(function () { var ConnectivityManager = Java.use('android.net.ConnectivityManager'); ConnectivityManager.getLinkProperties.implementation = function (network) { var linkProperties = this.getLinkProperties(network); if (linkProperties) { var ProxyInfo = Java.use('android.net.ProxyInfo'); var proxyInfo = ProxyInfo.$new(null, null, 0); linkProperties.setHttpProxy(proxyInfo); } return linkProperties; }; }); } function vpn1_bypass(){ Java.perform(()=>{ var ConnectivityManager = Java.use('android.net.ConnectivityManager'); ConnectivityManager.getNetworkInfo.overload('int').implementation = function (networkType) { var result = this.getNetworkInfo(networkType); if (networkType === ConnectivityManager.TYPE_VPN.value) { return null; } return result; }; }) } function vpn2_bypass() { Java.perform(() => { // 获取 NetworkCapabilities 类 var NetworkCapabilities = Java.use('android.net.NetworkCapabilities'); // Hook hasTransport 方法 NetworkCapabilities.hasTransport.overload('int').implementation = function (transportType) { // 如果检测到 TRANSPORT_VPN,返回 false if (transportType === NetworkCapabilities.TRANSPORT_VPN.value) { console.log("[*] VPN 检测被绕过"); return false; } // 否则调用原始方法 return this.hasTransport(transportType); }; console.log("[*] NetworkCapabilities.hasTransport 已 Hook"); }); } function ssl_attack(){ Java.perform(() => { var base = Module.findBaseAddress("libflutter.so"); console.log("base: " + base); var ssl_crypto_x509_session_verify_cert_chain = base.add(0x6c4b4c); Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, { onEnter: function(args) { }, onLeave: function(retval) { console.log("校验函数返回值: " + retval); retval.replace(0x1); } }); }); } function bypass_proxy_main(){ wifi1_proxy_bypass() vpn1_bypass() ssl_attack() } setImmediate(bypass_proxy_main) ``` 再次利用frida进行hook,可以发现大功告成了  进一步利用茶杯狐代理到`Burp`  也是成功获取到了流量  0x04 流量侧成功绕过收费限制 ---------------- 既然抓到流量了,那么接下来分析一下,登录之后可以看到它有一个请求,响应是下方这样,从响应中的参数不难看出,`expired`是否过期、`showRechargeTip`是否展示那个待激活的提示,还有过期时间等等参数  最后经过测试改成下面的响应能成功绕过提示,并且功能点均可正常使用 ```php {"code":200,"msg":"操作成功","data":{"deviceCount":1,"deviceInfo":{"imei":"xxxx","deviceName":"T1-xxxxx","activated":true,"vipService":true,"valueAddedService":false,"expired":false,"deviceStatus":99,"showRechargeTip":false,"serviceTime":99999999,"isNineDevice":false,"trackStorageDays":30,"gpsInstantModeExpiryTime":"2025-09-09"}}} ``` 功能出来了  点击查看定位直接就根据`imei`查询对应设备的坐标信息,不仅没校验是否有会员权限,而且没准还有水平越权,但是这个坐标位置是`0.0`  我猜这个东西的工作原理大概就是每隔一段时间定位器发送给服务器一个坐标,然后app通过这个接口查询,不过现在很明显是定位器有限制,没有上传坐标所以是`0.0` 然后就是后话了,我尝试了将定位器里面的卡换成正常有费有流量的卡,然后还是不行,那么就点到为止了,这一路下来其实想过很多次拉倒了,放弃吧,但是实在是不甘心,然后当时搞到了凌晨4点可算是弄明白了 > 还是那句话哈,有问题的老哥欢迎关注`小惜渗透`公众号后台回复,欢迎师傅们关注交流哈(本文提到的相关工具已经打包,有需要师傅们后台回复“APP测试工具”自取),另外这个文章等审核完后过些天也会同步,所以有想转发到公众号的师傅们等我投完再转发哈  > 参考: > > [https://mp.weixin.qq.com/s/zN3F\_UIqL6rph6-AI0ydZw](https://mp.weixin.qq.com/s/zN3F_UIqL6rph6-AI0ydZw) > > <https://www.freebuf.com/articles/mobile/360282.html>
发表于 2025-05-29 09:00:05
阅读 ( 1201 )
分类:
渗透测试
2 推荐
收藏
1 条评论
c铃儿响叮当
2025-05-29 14:29
求附件工具包
请先
登录
后评论
请先
登录
后评论
小惜渗透
9 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!