记一次帮丈母娘破解APP,满满的全是思路

事情是这样的,家里人从网上买了一个定位器,买之前也没问客服,结果到手之后一看竟然要收费才能使用,由于本来没多少钱后续就没退,然后我听了之后来了兴致,我想着能不能有什么漏洞能白嫖,于是有了本文,虽然最后解决也不难,不过我觉得最重要是满满的全是思路。

记一次帮丈母娘破解APP,满满的全是思路

事情是这样的,家里人从网上买了一个定位器,买之前也没问客服,结果到手之后一看竟然要收费才能使用,由于本来没多少钱后续就没退,然后我听了之后来了兴致,我想着能不能有什么漏洞能白嫖,于是有了本文,虽然最后解决也不难,不过我觉得最重要是满满的全是思路。

主要是下面两个东西

  • 定位器,里面有一张它出厂带的卡
  • 查询位置所用的app一个

定位器这东西,我是没有lot经验所以就先不用看了,直接看安卓app

0x01 初步研究app

拿出我的测试机,进入到app中会让你登录,登录的账号就是定位器的初始id,密码是123,进入后由于没有花钱,会出现一个弹窗,仔细看下图发现在弹窗后边我的设备显示在线,那是不是就意味着我只要让这个倒霉弹窗不再出现,我就可以点到后边我的设备,然后进行相关操作,于是想着关弹窗这种小活这不伸手就来,没想到这么简单,想想还有点小激动

1.png

于是有了第一个思路,想着先用开发助手定位弹窗组件,然后用算法助手pro直接拦截掉,但是当我定位组件的时候发现这个组件没ID,如图这个id名字什么的竟然是空的,神奇。

2.png

突然灵光一现,会不会是它不是个弹窗,由于后边的背景色是灰色,想着难不成直接弄了个Activity放到了前面

正想着,于是就有了第二个想法,直接用MT定位Activity,然后直接拦截这个Activity,太聪明了,没想到这么简单,想想还有点小激动

但是当我定位的时候发现,只有一个MainActivity这说明我的想法并不成立,难不成它还是个组件,只不过没名字

3.png

算了,正想着着实有点头痛,干脆直接看它代码吧,于是第三个思路,通过待激活关键字定位到相关代码,于是我下意识的拿出了我的脱壳机,不过没想到这家伙竟然没壳

4.png

正想着原来是我高看它了,那不分分钟手到擒来

直接拖到jadx搜索待激活,结果竟然没有

5.png

搜索unicode编码也没有

6.png

这让我百思不得其解,再尝试搜索了其余的几个关键字没有结果后,没有办法的我没了办法,于是想着那干脆从流量层面看看吧

0x02 流量侧心酸突破历程

直接设置系统代理到我的Burp,然后打开发现

7.png

666,竟然还有代理检测,于是直接上Postern,走vpn代理到茶杯狐Charles的socks端口,结果还是不行

8.png

没办法了,只好拿出了神器frida

  1. adb shell
  2. su
  3. cd /data/local/tmp/
  4. #测试机,启动frida服务
  5. ./frida-server-16.0.11-android-arm64

在本地新建了一个过代理检测的通杀脚本(来源:阿呆攻防)

  1. function wifi1_proxy_bypass(){
  2. Java.perform(()=>{
  3. var systemCls = Java.use('java.lang.System');
  4. systemCls.getProperty.overload('java.lang.String').implementation = function (val) {
  5. var ret = this.getProperty(val);
  6. if (val == "http.proxyHost") {
  7. return ""
  8. }
  9. if (val == "http.proxyPort") {
  10. return "-1" // 这里改""/"0"/"-1",我这里留的-1是之前金融项目好几家都是-1
  11. }
  12. return ret
  13. }
  14. })
  15. }
  16. function wifi2_proxy_bypass(){
  17. Java.perform(function () {
  18. var ConnectivityManager = Java.use('android.net.ConnectivityManager');
  19. ConnectivityManager.getLinkProperties.implementation = function (network) {
  20. var linkProperties = this.getLinkProperties(network);
  21. if (linkProperties) {
  22. var ProxyInfo = Java.use('android.net.ProxyInfo');
  23. var proxyInfo = ProxyInfo.$new(null, null, 0);
  24. linkProperties.setHttpProxy(proxyInfo);
  25. }
  26. return linkProperties;
  27. };
  28. });
  29. }
  30. function vpn1_bypass(){
  31. Java.perform(()=>{
  32. var ConnectivityManager = Java.use('android.net.ConnectivityManager');
  33. ConnectivityManager.getNetworkInfo.overload('int').implementation = function (networkType) {
  34. var result = this.getNetworkInfo(networkType);
  35. if (networkType === ConnectivityManager.TYPE_VPN.value) {
  36. return null;
  37. }
  38. return result;
  39. };
  40. })
  41. }
  42. function vpn2_bypass() {
  43. Java.perform(() => {
  44. // 获取 NetworkCapabilities 类
  45. var NetworkCapabilities = Java.use('android.net.NetworkCapabilities');
  46. // Hook hasTransport 方法
  47. NetworkCapabilities.hasTransport.overload('int').implementation = function (transportType) {
  48. // 如果检测到 TRANSPORT_VPN,返回 false
  49. if (transportType === NetworkCapabilities.TRANSPORT_VPN.value) {
  50. console.log("[*] VPN 检测被绕过");
  51. return false;
  52. }
  53. // 否则调用原始方法
  54. return this.hasTransport(transportType);
  55. };
  56. console.log("[*] NetworkCapabilities.hasTransport 已 Hook");
  57. });
  58. }
  59. function bypass_proxy_main(){
  60. wifi1_proxy_bypass()
  61. vpn1_bypass()
  62. }
  63. setImmediate(bypass_proxy_main)

然后直接frida进行hook

  1. .\frida.exe -U [APP进程] -l .\hook.js

发现正常请求走的通了,不过这个时候我的茶杯狐还没有抓取https流量,也就是说https的流量会经过茶杯狐,不过没有抓取ssl会导致它并不会利用茶杯狐的ssl证书做中转,我也就看不到对应的http报文的明文,看到的都是密文,不过好在软件使用的服务器的域名是知道了

9.png

域名到手

10.png

既然如此直接装好证书并信任,抓ssl,不点开没事,一抓https又完犊子了,这个时候我尝试了别的https请求是能抓到的,就这个app抓不到

11.png

666,竟然有校验,没关系,盲猜连壳都没有的app,撑死一个单向证书校验,把LSBJustTrustMe一开,心想这不就成了,没想到这么简单

12.png

发现还是不行

难不成双向证书校验?由于我知道域名了,直接访问对方域名,发现提示400,不过报错信息跟正常的双向证书不一样,我想着一定是对方伪装了一下,兵法有云,实则虚之虚则实之,小小诡计岂能瞒得过我,双向证书校验,绝对双向证书校验

13.png

然后我尝试搜了一下apk解包后有没有常见后缀p12cerjks等等的文件,发现并没有,按理说不能,这时想到难不成是伪装成了某个png文件,然后使用的时候在代码中又还原了出来?于是在代码中一顿找最后也没有。

思路转变一下,既然找不到证书,那么它加载本地证书的时候肯定是要读取本地证书文件的

既然如此废话不多说直接上r0capture,具体使用方法不多赘述,直接看它项目首页介绍https://github.com/r0ysue/r0capture

恭喜你猜对了又是0收获,既然如此我又用objection尝试HOOK了java.io.File.$init想着你总要读文件的吧

  1. 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不是个语言吗?

14.png

于是直接开启上网冲浪模式,一顿冲浪下来,ok有解了

先说一下flutter,Flutter 是一个由 Google 开发的 开源 UI 框架,用于构建跨平台应用,也就是说,用一套代码可以同时生成 iOS、Android、Web 和桌面(Windows、macOS、Linux) 应用。

Flutter使用Dart编写,因此它不会使用系统CA存储,Dart使用编译到应用程序中的CA列表,Dart在Android上不支持代理,所以这就是为什么一开始使用系统代理没有生效的原因

当我们的应用存在libflutter.so的时候,其实就可以判断大概率为flutter的

还有一种方法是通过flutter的日志,如果有输出不仅可以判断出app是flutter写的,还看到对应的日志

  1. adb shell
  2. su
  3. logcat |grep flutter

可以看到这个应用的所有请求和响应都在日志中

15.png

下面这个函数是flutter的证书校验

  1. static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
  2. SSL_HANDSHAKE *hs,
  3. uint8_t *out_alert) {
  4. *out_alert = SSL_AD_INTERNAL_ERROR;
  5. STACK_OF(X509) *const cert_chain = session->x509_chain;
  6. if (cert_chain == nullptr || sk_X509_num(cert_chain) == 0) {
  7. return false;
  8. }
  9. SSL *const ssl = hs->ssl;
  10. SSL_CTX *ssl_ctx = ssl->ctx.get();
  11. X509_STORE *verify_store = ssl_ctx->cert_store;
  12. if (hs->config->cert->verify_store != nullptr) {
  13. verify_store = hs->config->cert->verify_store;
  14. }
  15. X509 *leaf = sk_X509_value(cert_chain, 0);
  16. const char *name;
  17. size_t name_len;
  18. SSL_get0_ech_name_override(ssl, &name, &name_len);
  19. UniquePtr<X509_STORE_CTX> ctx(X509_STORE_CTX_new());
  20. if (!ctx ||
  21. !X509_STORE_CTX_init(ctx.get(), verify_store, leaf, cert_chain) ||
  22. !X509_STORE_CTX_set_ex_data(ctx.get(),
  23. SSL_get_ex_data_X509_STORE_CTX_idx(), ssl) ||
  24. // We need to inherit the verify parameters. These can be determined by
  25. // the context: if its a server it will verify SSL client certificates or
  26. // vice versa.
  27. !X509_STORE_CTX_set_default(ctx.get(),
  28. ssl->server ? "ssl_client" : "ssl_server") ||
  29. // Anything non-default in "param" should overwrite anything in the ctx.
  30. !X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(ctx.get()),
  31. hs->config->param) ||
  32. // ClientHelloOuter connections use a different name.
  33. (name_len != 0 &amp;&amp;
  34. !X509_VERIFY_PARAM_set1_host(X509_STORE_CTX_get0_param(ctx.get()), name,
  35. name_len))) {
  36. OPENSSL_PUT_ERROR(SSL, ERR_R_X509_LIB);
  37. return false;
  38. }
  39. if (hs->config->verify_callback) {
  40. X509_STORE_CTX_set_verify_cb(ctx.get(), hs->config->verify_callback);
  41. }
  42. int verify_ret;
  43. if (ssl_ctx->app_verify_callback != nullptr) {
  44. verify_ret =
  45. ssl_ctx->app_verify_callback(ctx.get(), ssl_ctx->app_verify_arg);
  46. } else {
  47. verify_ret = X509_verify_cert(ctx.get());
  48. }
  49. session->verify_result = X509_STORE_CTX_get_error(ctx.get());
  50. // If |SSL_VERIFY_NONE|, the error is non-fatal, but we keep the result.
  51. if (verify_ret <= 0 &amp;&amp; hs->config->verify_mode != SSL_VERIFY_NONE) {
  52. *out_alert = SSL_alert_from_verify_result(session->verify_result);
  53. return false;
  54. }
  55. ERR_clear_error();
  56. return true;
  57. }

所以我们只需要hook这个函数,然后让其返回值为真即可,操作如下

  1. 首先找到lib目录下对应系统架构文件夹下的libflutter.so,拖入ida
  2. 然后看下方左下角等待ida加载完毕,如果没加载完毕就搜索很大概率搜不到
  3. Shift+F12打开字符串窗口,然后Ctrl+F搜索关键字ssl_server

16.png

  1. 双击跳转过去后,按Ctrl+X查找交叉引用,如下图所示,6C4B4C就是函数对应地址

17.png

  1. 编写hook函数
  1. function ssl_attack(){
  2. Java.perform(() => {
  3. var base = Module.findBaseAddress("libflutter.so");
  4. console.log("base: " + base);
  5. var ssl_crypto_x509_session_verify_cert_chain = base.add(0x6c4b4c);
  6. Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, {
  7. onEnter: function(args) {
  8. },
  9. onLeave: function(retval) {
  10. console.log("校验函数返回值: " + retval);
  11. retval.replace(0x1);
  12.    }
  13.   });
  14. });
  15. }

至此我们可以成功解决了流量侧的对抗问题,整体代码如下

  1. function wifi1_proxy_bypass(){
  2. Java.perform(()=>{
  3. var systemCls = Java.use('java.lang.System');
  4. systemCls.getProperty.overload('java.lang.String').implementation = function (val) {
  5. var ret = this.getProperty(val);
  6. if (val == "http.proxyHost") {
  7. return ""
  8. }
  9. if (val == "http.proxyPort") {
  10. return "-1" // 这里改""/"0"/"-1",我这里留的-1是之前金融项目好几家都是-1
  11. }
  12. return ret
  13. }
  14. })
  15. }
  16. function wifi2_proxy_bypass(){
  17. Java.perform(function () {
  18. var ConnectivityManager = Java.use('android.net.ConnectivityManager');
  19. ConnectivityManager.getLinkProperties.implementation = function (network) {
  20. var linkProperties = this.getLinkProperties(network);
  21. if (linkProperties) {
  22. var ProxyInfo = Java.use('android.net.ProxyInfo');
  23. var proxyInfo = ProxyInfo.$new(null, null, 0);
  24. linkProperties.setHttpProxy(proxyInfo);
  25. }
  26. return linkProperties;
  27. };
  28. });
  29. }
  30. function vpn1_bypass(){
  31. Java.perform(()=>{
  32. var ConnectivityManager = Java.use('android.net.ConnectivityManager');
  33. ConnectivityManager.getNetworkInfo.overload('int').implementation = function (networkType) {
  34. var result = this.getNetworkInfo(networkType);
  35. if (networkType === ConnectivityManager.TYPE_VPN.value) {
  36. return null;
  37. }
  38. return result;
  39. };
  40. })
  41. }
  42. function vpn2_bypass() {
  43. Java.perform(() => {
  44. // 获取 NetworkCapabilities 类
  45. var NetworkCapabilities = Java.use('android.net.NetworkCapabilities');
  46. // Hook hasTransport 方法
  47. NetworkCapabilities.hasTransport.overload('int').implementation = function (transportType) {
  48. // 如果检测到 TRANSPORT_VPN,返回 false
  49. if (transportType === NetworkCapabilities.TRANSPORT_VPN.value) {
  50. console.log("[*] VPN 检测被绕过");
  51. return false;
  52. }
  53. // 否则调用原始方法
  54. return this.hasTransport(transportType);
  55. };
  56. console.log("[*] NetworkCapabilities.hasTransport 已 Hook");
  57. });
  58. }
  59. function ssl_attack(){
  60. Java.perform(() => {
  61. var base = Module.findBaseAddress("libflutter.so");
  62. console.log("base: " + base);
  63. var ssl_crypto_x509_session_verify_cert_chain = base.add(0x6c4b4c);
  64. Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, {
  65. onEnter: function(args) {
  66. },
  67. onLeave: function(retval) {
  68. console.log("校验函数返回值: " + retval);
  69. retval.replace(0x1);
  70.    }
  71.   });
  72. });
  73. }
  74. function bypass_proxy_main(){
  75. wifi1_proxy_bypass()
  76. vpn1_bypass()
  77. ssl_attack()
  78. }
  79. setImmediate(bypass_proxy_main)

再次利用frida进行hook,可以发现大功告成了

18.png

进一步利用茶杯狐代理到Burp

19.png

也是成功获取到了流量

20.png

0x04 流量侧成功绕过收费限制

既然抓到流量了,那么接下来分析一下,登录之后可以看到它有一个请求,响应是下方这样,从响应中的参数不难看出,expired是否过期、showRechargeTip是否展示那个待激活的提示,还有过期时间等等参数

21.png

最后经过测试改成下面的响应能成功绕过提示,并且功能点均可正常使用

  1. {"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"}}}

功能出来了

22.png

点击查看定位直接就根据imei查询对应设备的坐标信息,不仅没校验是否有会员权限,而且没准还有水平越权,但是这个坐标位置是0.0

23.png

我猜这个东西的工作原理大概就是每隔一段时间定位器发送给服务器一个坐标,然后app通过这个接口查询,不过现在很明显是定位器有限制,没有上传坐标所以是0.0

然后就是后话了,我尝试了将定位器里面的卡换成正常有费有流量的卡,然后还是不行,那么就点到为止了,这一路下来其实想过很多次拉倒了,放弃吧,但是实在是不甘心,然后当时搞到了凌晨4点可算是弄明白了

还是那句话哈,有问题的老哥欢迎关注小惜渗透公众号后台回复,欢迎师傅们关注交流哈(本文提到的相关工具已经打包,有需要师傅们后台回复“APP测试工具”自取),另外这个文章等审核完后过些天也会同步,所以有想转发到公众号的师傅们等我投完再转发哈

24.png

参考:

https://mp.weixin.qq.com/s/zN3F_UIqL6rph6-AI0ydZw

https://www.freebuf.com/articles/mobile/360282.html

  • 发表于 2025-05-29 09:00:05
  • 阅读 ( 1784 )
  • 分类:渗透测试

4 条评论

小惜渗透
小惜渗透

10 篇文章

站长统计