Android逆向之动态调试全流程

Smali层动态调试

  • 本篇内容所涉及到的资源
    链接:https://pan.baidu.com/s/14ZF-7pop4NbrDPydtRQOeg
    提取码:8fs8

论述Smali调试的原理及必要性

  • Smali层调试的目的主要是为了调试app中的java代码

  • 在PC机上java代码一般封装为jar包运行,而jar包内的.class文件均为已编码的Java字节码,可以由java虚拟机解释运行。因此PC机上IDEA直接引用jar包即可进行java源码级调试

  • Android平台上主要使用Java语言来开发程序,但Android上的程序运行机制和标准的Java程序并不一样 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDoG31Zf-1637506695064)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211120161809336.png)]

    左侧为PC上Java运行流程,右图为Android上Java运行流程,不难发现Android多封装了一级字节码,这就导致我们直接解包的Apk其实需要两次反编译才能得到原始的Java代码。到这里很多水平比较高的小伙伴已经发现了问题,两次编码并不足以说明Smali调试的必要性,完全可以编写一个工具自动完成两次反编码的工作,实现Java层调试,甚至可以自己写一个编译器直接引入.dex文件作为代码库,通过重写.dex中的函数的方式快速重写百度网盘、新浪微博、拼多多这些APP的源码。这么方便的功能为什么没有人去做呢,其实问题就出在这个所谓的“Java源代码”的质量,我们选择两个强大的源码反编译工具(jd-gui和jadx)的结果做一下比对。

    jd-gui反编译结果:

    public void onClick(View paramView)
      {
        switch (paramView.getId())
        {
        }
        do
        {
          do
          {
            do
            {
              for (;;)
              {
                return;
                this.i.removeCallbacksAndMessages(null);
                this.i.sendEmptyMessageDelayed(1, 0L);
                BlogApplication.q.a("", "", "BS_LAUNCH_SPLASH_SKIP_CLICK", (String[][])null);
              }
            } while ((!i.a(this)) || (this.f == null));
            paramView = this.f.launchimgslink;
          } while ((paramView == null) || (paramView.length <= 0) || (this.g >= paramView.length));
          localIntent = paramView[this.g];
        } while (localIntent == null);
        if ("article".equals(localIntent.type))
        {
          this.i.removeCallbacksAndMessages(null);
          ac.b("SplashActivity", "先跳Main,然后在Main中做判断跳文章页");
          paramView = a.f(this);
          localIntent = a.b(this, localIntent.data, "", "");
          localIntent.putExtra("jump", PushConfig.JUMP_ARTICLE);
          localBundle = localIntent.getExtras();
          if (localBundle != null)
          {
            paramView.putExtras(localBundle);
            startActivity(paramView);
            finish();
          }
        }
        while ((!"webview".equals(localIntent.type)) || (TextUtils.isEmpty(localIntent.data))) {
          for (;;)
          {
            BlogApplication.q.a("", "", "BS_LAUNCH_SPLASH_CLICK", new String[][] { { "URL", this.h } });
            break;
            paramView.putExtras(localIntent);
          }
        }
        this.i.removeCallbacksAndMessages(null);
        ac.b("SplashActivity", "先跳Main,然后在Main中做判断跳文章页");
        paramView = a.f(this);
        Intent localIntent = a.i(this, localIntent.data);
        localIntent.putExtra("jump", PushConfig.JUMP_WEB_VIEW);
        Bundle localBundle = localIntent.getExtras();
        if (localBundle != null) {
          paramView.putExtras(localBundle);
        }
        for (;;)
        {
          startActivity(paramView);
          finish();
          break;
          paramView.putExtras(localIntent);
        }
      }
    

    jadx源码:

       @Override // android.view.View.OnClickListener
        public void onClick(View view) {
            SplashAdData[] splashAdDataArr;
            SplashAdData splashAdData;
            switch (view.getId()) {
                case R.id.splash_iv:
                    if (i.a(this) && this.f != null && (splashAdDataArr = this.f.launchimgslink) != null && splashAdDataArr.length > 0 && this.g < splashAdDataArr.length && (splashAdData = splashAdDataArr[this.g]) != null) {
                        if ("article".equals(splashAdData.type)) {
                            this.i.removeCallbacksAndMessages(null);
                            ac.b("SplashActivity", "先跳Main,然后在Main中做判断跳文章页");
                            Intent f = a.f(this);
                            Intent b2 = a.b(this, splashAdData.data, "", "");
                            b2.putExtra(a.C0046a.P, PushConfig.JUMP_ARTICLE);
                            Bundle extras = b2.getExtras();
                            if (extras != null) {
                                f.putExtras(extras);
                            } else {
                                f.putExtras(b2);
                            }
                            startActivity(f);
                            finish();
                        } else if ("webview".equals(splashAdData.type) && !TextUtils.isEmpty(splashAdData.data)) {
                            this.i.removeCallbacksAndMessages(null);
                            ac.b("SplashActivity", "先跳Main,然后在Main中做判断跳广告");
                            Intent f2 = a.f(this);
                            Intent i = a.i(this, splashAdData.data);
                            i.putExtra(a.C0046a.P, PushConfig.JUMP_WEB_VIEW);
                            Bundle extras2 = i.getExtras();
                            if (extras2 != null) {
                                f2.putExtras(extras2);
                            } else {
                                f2.putExtras(i);
                            }
                            startActivity(f2);
                            finish();
                        }
                        BlogApplication.q.a("", "", com.sina.sinablog.b.b.a.aG, new String[][]{new String[]{"URL", this.h}});
                        return;
                    }
                    return;
                case R.id.splash_ad_jump_btn:
                    this.i.removeCallbacksAndMessages(null);
                    this.i.sendEmptyMessageDelayed(1, 0);
                    BlogApplication.q.a("", "", com.sina.sinablog.b.b.a.aH, (String[][]) null);
                    return;
                default:
                    return;
            }
        }
    }
    

    乍一看你一定会觉得这是两个不同的函数,然而通过一些关键字符串和方法名你就能辨识出:这个反编译结果其实来自同一段smali代码,那为什么jd-gui反编译时入参名叫View paramView而jadx却叫View view呢,答案是因为这是编译器猜测的结果。原始的smali语句其实长这样:

    .method public onClick(Landroid/view/View;)V
        .locals 9
    
        .prologue
        const/4 v8, 0x0
    
        const/4 v7, 0x1
    
        const/4 v0, 0x0
    
        .line 169
        invoke-virtual {p1}, Landroid/view/View;->getId()I
    

    括号内通过Landroid/view/View声明了参数的类型却并没有声明参数的名称,这个名称其实被省略了,也就是Java在转Smali时,代码的信息被压缩了,根本没有变量名那反编译器怎么办呢,自然是瞎写一个上去,只要人能读懂就行。那么两种反编译结果的区别也就迎刃而解了,jd-gui将分支处理为do while形式,jadx将分支处理为switch case的形式,看起来两者似乎都能跑通,实际上复制到编译器里你就发现没一个能直接运行的,因为类似的错误太多了,说不准哪里就运行不了。

  • 回过头来再说说调试器最主要的功能,也就是两个:一个是监视变量信息,一个是下断点。反编译代码的错误顺序,以及瞎猜的变量名就注定了源码级调试根本就无法完成这两个功能,因此Smali的分析是少不了的,直接重写百度网盘APP的Java源码也是不现实的,除非你花一个月的时间把这些全是漏洞的垃圾代码重构一下。

  • 熟悉Smali插桩的小伙伴可能会说,Android Killer一键改Smali不也挺方便的吗,我只能说动态调试Smali是更爽的姿势,插桩最大的劣势还是打包耗费的时间成本过高,当你逆向一些比较复杂的程序时,这一劣势就会非常致命,会让你测到绝望

准备工作

手机root

  • 首先你要准备一台root过的手机,因为调试过程中经常涉及到很高的系统权限的调用,没root过的机子真是各种憋屈,不要用虚拟机!!!用过的都知道,最新发布的应用很多都开始禁止在虚拟机上运行,原因就是虚拟机对破解软件的人太友好了,厂商也受不了自己的软件轻易被破解。

  • 老机型推荐使用360一键root工具,傻瓜式操作,瞬间完成,查看是否root成功可以通过运行如下命令:

    adb shell
    su root
    

    如果成功切换为root用户,而不是默认的乱七八糟的用户名,就说明root成功了。

  • 新机型一般root的门槛比较高,可以百度对应手机型号的root方案,一般需要将系统重装为官方的“开发版”,这种方式刷完的机子连后面获取权限都省了,因为开发版默认就有调试权限。

调试权限开启

  • 出于安全考虑,Android系统并不允许应用被随意调试,官方文档称需要满足二者之一的条件。

    1. App的AndroidManifest.xml中Application标签必选包含属性android:debuggable=“true”;
    2. 根目录下的/default.prop文件中ro.debuggable的值为1;
  • 我们先来看第一个条件有没有办法满足,首先,发行版的App都会将debuggable设置为 false,使第三方不能直接调试分析APP,这也是厂商出于安全的考虑,那我们就需要反编译Apk,修改后进行重打包,这也是绝大多数教程的做法,但我个人非常非常不建议这么操作,因为重打包容易遭受无妄之灾。
    你想研究App的通讯协议和加密字段,这已经足够让人焦头烂额,你可能会遇到繁杂的代码、诡异的反抓包,So层的加密……而如果你对App进行重打包,那就要面对App额外的保护措施,比如重打包失败,签名验证等。因为重打包这个操作主要是开发盗版App和破解版App做的事,这对厂商来说更加难以忍受,只是修改一个debuggable字段就要揽上这么多事,显然吃力不讨好。

  • 那第二个条件好满足吗?default.prop 文件非常好找,它就在Android的根目录下,我们可以通过ES文件浏览器找到它。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3uXH5yW-1637506695066)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211120182808321.png)]

    很不幸的发现,这台手机的debuggable标识为0,不可调试。一个朴素的想法是直接修改这个值不就可以了?但是这是不可以的,这个值只在系统启动时,也就是开机时才会读取和加载一次。那重启?抱歉,每次重启,这个值就会恢复默认。所以就造成了一个死循环。

    解决这个问题的办法有很多:

    1. 改写系统文件,修改ro.debuggable为1。提取系统镜像文件,解压、改写、打包,在“挖煤模式”下,通过fastboot启动引导工具将镜像写入硬盘。难度稍大,但一劳永逸,缺点是对新手不友好。可以参考文章:

    2. 注入init进程,修改内存中ro.debuggable的值,这个也是之前惯常的做法,且非常简单。 需要注意的是,因为是修改内存值,所以文件中的ro.debuggable值并不会变化,且每次重启设备都要重新注入。

    3. 之前提到的,使用开发版/测试版的手机系统,ro.debuggable值常常为1

    4. 使用模拟器,比如雷电模拟器、Genymotion等,许多模拟器天然支持动态调试,尽管defalut.prop中值并不为1,打开adb shell,用getprop ro.debuggable命令查看内存中的debuggable值却为1。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qnpNnORG-1637506695068)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/825468-20200812153428951-1065657205.png)]

    5. 如果手机可以安装Xposed框架,那么推荐Xposed Hook系统判定函数,成熟的工具已经有很多了,如:BDOpener,只需要下载Apk,在Xposed中激活后重启手机,就可以一劳永逸。但新版的手机一般不支持一键安装xposed框架,需要手动在Recovery模式下刷zip包才能才能安装xposed框架,我使用的测试机是三星的更是根本没有recovery模式,因此还要使用刷机工具单刷一个第三方的Recovery引导。装个框架就要刷两次机,属实难顶。

关键代码定位

  • 想要调试自然需要选择和合适的入口断点来阻塞程序,分析数据流,代码定位的方式非常多,包括:

    • 搜索特征字符串,如资源文件id号,弹窗窗口标题等

    • 直接搜索关键API,如:OnCreate()、OnDestroy()

    • 代理抓包,搜索网包特征URL路径

  • 这次采用的是第三种定位方法,跟着教程走可以发现URL特征值为get_article_info.php(就不详细介绍了这不是学习调试流程的重点),使用jdax搜索一下特征字符。jadx支持zip、apk、dex、jar等多种文件的反编译,这里采用的方法是:将APK文件重命名为zip包,解压zip包中的class.dex文件进行反编译

  • 反编译工具我只推荐jdax,而且推荐自己编译源码来使用,编译版的界面优雅美观(反编译工具真没几个美观的),搜索速度快而准,而且还支持正则表达式搜索,这点很重要,正则搜索很多情况下比字符串搜索有用的多。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dl6OVQOo-1637507139374)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121080319728.png)]

  • 根据抓包结果可以确定成员变量String m是我们想要的结果,右击变量查找用例,查看变量调用情况,查找结果却是空的。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bgld93CS-1637506695070)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121080814834.png)]

  • 如果使用旧版jadx是可以查找到用例代码的 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySGKP7r6-1637506695071)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121081316486.png)]

  • 乍一看好像新版本被旧版吊打了,然而经过测试发现新版的查找用例能够匹配到比原版更多的调用,只是取消了对静态全局变量的查找支持,可能是出于性能考虑,个人感觉还是非常合理的,因为静态全局变量的调用模式是固定的,直接字符串搜索就够了,像这里直接搜索c.b.m字符串即可找到调用代码。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6RxmrCWy-1637506695071)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121082030332.png)]

  • 进入方法后点左下角的Smali定位到Smali代码入口,jadx的任务就完成了 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-38sYtrgj-1637506695072)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121082548392.png)]

构建Smali调试项目

Android Killer解包APK

  • 首先要对APK进行解包,反编译出Smali源码文件,这里推荐使用Android killer进行反编译,因为这个工具还支持重新打包APK。无论如何,破解APP的最终形态一定是二次打包,因此使用Android killer一劳永逸。

  • 直接将APK拖进软件里等待一段时间,大概率软件会卡在“ 正在反编译APK源码请稍后…”。这个时候直接退出程序重新进一下即可,不过这样操作会造成程序的“查看源代码”功能用不了,因为源码目录下的class文件没有正常生成,可以手动修复一下。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wzBRlZe-1637506695072)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121085139988.png)]

  • 将APK重命名为zip文件,解压出classes.dex与classes2.dex。直接上逆向助手执行dex转jar命令在路径下生成两个对应的jar包,然后在找到根目录下的ProjectSrc文件夹下分别创建smali和smali_classes2两个子目录(注意名称一定要对,Android killer就是靠这两个目录索引源码的)。最后用winrar分别解压两个jar包中的class文件到子目录下即可。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2kqUXM0-1637506695073)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121084739361.png)]

  • 右击→查看源码,即可以显示Smali对应的Java代码。这个反编译效果没有jadx好,有的时候图个方便可以临时用一下。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6DhlAbsm-1637506695073)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121085954744.png)]

  • 在工程管理栏目中,选择根目录右击→打开方式→打开文件路径即可显示项目所在文件夹,这里的Project目录下就含有解包的所有Smali代码。这些代码就放置在Project/Smali目录下。至此Android Kill的使命也暂时完成了。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZ0YJdl0-1637506695074)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121090635702.png)]

Android Studio+Smali插件

  • 直接开Android Studio选择Project文件夹作为根目录导入项目文件,直接进来会提示框架错误,这是因为项目没有关联AndroidManifest.xml文件,点击右下角的Event Log,发现有一个可以点击的Configure链接,点一下即可自动关联配置文件,这一步不绑定会造成项目无法正常连接手机进行调试[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tEka7050-1637506695074)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121091900534.png)]

  • 网上下载好smalidea.zip插件包,打开File→Settings→Plugins选项,选择Install Plugin from Disk,然后选择下载好的zip包即可。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJ8oUEGk-1637506695075)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121093404801.png)]

  • 重启软件后,看到如下界面,证明安装成功 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICIHAzHW-1637506695076)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121093519522.png)]

  • 回到工程在顶部工具栏选择Android测试机,并点击调试按钮来附加进程 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EoYmK3HD-1637506695076)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121094535912.png)]

  • 如果你的工具栏内没有显示Android设备,说明项目没有关联AndroidManifest.xml文件,直接参考第一步绑定Configuration即可。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jGBRyHLP-1637506695077)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121093916318.png)]

  • Android Studio会根据AndroidManifest.xml文件中的Package字段自动定位程序包名,若想显示更多进程直接勾选Show all processes 即可,点击OK开始调试。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hQucnel-1637506695077)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121094749864.png)]

  • 当然空跑调试进程是没有任何意义的,只有设置了断点才能单步观测程序数据流。直接复制一下之前jadx定位到的关键代码的类名com.sina.sinablog.network.d,沿着包名路径找到d.smali代码文件,定位到a()函数入口下一个断点。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJAbZcAK-1637506695078)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121100635874.png)]

  • 如果你发现点击代码左侧无法成功下断点,那你的界面一定长这样,所有的smali文件都是红色的,这是由于新版Android Studio内置的Smali插件覆盖了smalidea插件的功能导致的 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6l5SLrLv-1637506695078)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121095707171.png)]

  • 解决办法是,打开File→Settings→Editor→File Types选项找到红色的Smali插件,删掉它接管的*.smali文件,点击黑色的Smali插件,添加*.smali文件的接管策略,点击“确定”发现Smali文件图标均变成黑色,并可以正常设置断点。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZRfNckFN-1637506695079)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121100439236.png)]

  • 重复之前的操作附加调试进程,点击推文标题触发断点 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhMMIyV4-1637506695079)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121101231650.png)]

  • 简单阅读Smali代码可知寄存器v0中存储了HashMap类型的数据,在调试器右侧的watcher中点击加号添加v0寄存器查看数据详情 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLPtc09t-1637506695080)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121101502154.png)]

  • 点击执行表达式按钮,可以在断点处随意运行自定义Java代码,功能超级强大,比Smali静态插桩效率高几百倍。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2IKeaba-1637506695080)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121102001149.png)]

阻塞式启动-调试程序入口

  • 有的时候关键代码不是通过触发方式运行的,而是沿着程序入口顺序执行,这种情况下,先开启程序再附加进程的方式就显得不那么有效,即使是单身二十年的手速也很难抢在程序初始化完成之前附加进程。

  • 因此程序阻塞式启动就显得很有必要了,所谓阻塞式启动就是提前让调试程序进入进程池,但是不进入用户层运行入口方法,而是等待调试器附加进程后再运行入口方法,这样就可以使用调试器分析程序入口点了,下面我们以“新浪博客APP”的OnCreate入口函数为例来讲解具体操作过程

  • 在Android Killer 工程信息中点击左上角的入口连接,即可跳转到程序入口类,函数列表内找到OnCreate函数体,根据包名可以确定入口函数路径在com/sina/sinablog/ui/SplashActivity [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTbPfdFY-1637506695081)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121114434668.png)]

  • 已知软件进程包名为com.sina.sinablog 因此使用运行如下adb命令让程序以阻塞方式启动,-D参数说明了以调试模式启动应用,com.sina.sinablog声明了启动包名,/.ui.SplashActivity声明了启动类入口

    adb shell am start -D -n com.sina.sinablog/.ui.SplashActivity
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LmhMVUm0-1637506695081)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121115046529.png)]

  • 运行后可以发现手机提示Waiting For Debugger(等待调试器)信息,此时切到Android Studio中,在OnCreate入口下断,附加调试进程,即可开始调试程序入口。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpi8qjdN-1637506695082)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121115430929.png)]

  • 通过这种方式不但可以调试Smali层函数入口,对于So层入口同样适用,因此之后会讲到,通过Android Studio + IDA就可以轻松实现联动调试了。

SO层动态调试

阻塞式启动的另一种姿势

  • 很多的反调试和加密工作都是在SO层入口函数内就进行的,控制SO层入口点的重要性远大于Smali层。因此,虽然我们之后的教程没有用到阻塞式启动,但以备不时之需,我们还是先来聊聊如何实现APP程序阻塞式启动。

  • 之前我们讲解过如何使用Android Studio配合ADB命令实现阻塞调试,通过Android Studio + IDA很容易就可以复现这一过程,实现Smali层、SO层并行调试。不过我的习惯是So层调试时不开Android Studio,免得电脑卡,因此这里介绍另一种方式触发Debugger。

  • 首先找到AndroidSDK低下的moniter工具来初始化8700上的调试服务,工具路径一般为/AndroidSdk/tools/lib/monitor-x86/moniter.exe,双击运行。LogCat窗口中可以显示各种APP的日志信息,一般Smali插装以后信息会在这里打印,这次我们用不到它。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DFTNphs8-1637506695082)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121120234567.png)]

  • 运行adb阻塞式启动命令,程序阻塞在入口处,此时在cmd中输入以下命令,运行8700端口上的调试服务,即可恢复程序的运行状态,执行成功后可以在moniter中看到绿色的调试图标:

    jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZyV8fpP-1637506695083)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121120826523.png)]

启动IDA调试服务

  • 在IDA安装目录下有一个/dbgsrv文件夹,使用ADB将里面的android_server程序上传到手机/data/local/tmp文件夹内

    adb push "android_server" /data/local/tmp/android_server
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGPUatYY-1637506695083)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121172555950.png)]

  • 使用adb shell 命令获取手机终端,再终端运行su root切换为root用户,这一步很重要,不切换root用户会导致权限不足,IDA无法正常调试,cd命令进到/data/local/tmp文件夹,使用chmod 777 android_server赋予文件超级管理员权限,最后就是执行./android_server命令启动服务程序。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBeBFo4p-1637506695084)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121123315476.png)]

  • 从返回值可以看到该服务监听的端口号为23946,注意这个端口号是手机的,想通过PC机与这个端口通信,需要进行TCP转发,adb工具也提供了这个功能,运行如下命令即可实现tcp转发

    adb forward tcp:23946 tcp:23946
    

附加调试进程

  • 下一步就是启动IDA附加调试进程了,网上有很多教程让你先加载一个so库到IDA中,其实这一步完全是多余的,还会起误导作用,让你以为IDA会将静态SO与内存中的动态SO关联起来,其实IDA的动态调试与静态分析完全是独立的,强行关联会造成不必要的数据错误,因此我们直接启动32位的IDA(手机系统一般是32位的),不导入任何SO库,选择Debugger→Attach→Remote ARM Linux/Android debugger [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5E86XDX-1637506695084)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121124322518.png)]

  • 配置好主机地址和端口号,点击OK [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkW9bk4J-1637506695085)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121124412464.png)]

  • 弹窗内CTRL+F搜索包名关键字,找到对应的附加进程,选中并点击OK [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hgkl7q9e-1637506695085)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121124717956.png)]

  • 进入界面后,手机上的APP程序会卡住,点击绿色的运行按钮,继续正常运行调试进程即可。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6O7To74-1637506695086)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121125042017.png)]

取消多线程中断信号挂起

  • 本来想把这一节放到最后说,不过转念一想,肯定有人看完《关键call搜索+断点调试》就直接去搞了,然后这一节的标题又感觉跟调试没啥关系干脆就不看了,结果就是调试的时候焦头烂额,也不知道为什么教程上的截图和自己做的效果不一样。所以呢综合考虑就把这一节放在中间了,按顺序看下来的人,即使觉得标题有点怪但还是会扫两眼,如果还是跳过这一节不看那就没救了,只能自己研究去了

  • 这一节的内容非常重要,如果你按上面说的点击了一下”继续执行“按钮,你就会发现,程序根本无法继续运行,IDA会疯狂地弹出类似这样的窗口,然后就是APP程序各种卡住,点击什么位置都没有反应,你退出这个窗口点击“继续执行”,很快它又来一个,不多久你的调试器就崩溃退出了。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8PJCYuCZ-1637506695086)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121135613328.png)]

  • 你可能会下意识地认为这是一种反调试机制,其实这是由于进程之间使用中断信号通信导致的,我们都知道计算机系统在硬件层面支持处理外设中断服务,其中有一种中断方式就是软件中断,常用于在底层系统编写定时器。程序进程也有类似的中断管理机制,进程的启动、挂起、终止操作都是由相应的中断信号控制的,父子进程之间可以通过发送中断信号的方式实现进程控制,针对特殊需求还可以启用自定义的中断编号,并在程序内定义好中断服务程序,接管中断信号,根据编号不同执行对应的处理策略,我们来看一下C++底层的具体实现原理:

    void signal_handler_IO(int signum) //status中断向量号
    {
        int realNum = signum - SIGRTMIN;//用户自定义中断号
        int pointer = 0x1 << realNum;//移位
        read_flag |= pointer; //设置对应标志位置1,其他位忽略
        return;
    }
    int pointer0 = 0x1 << this->id;                //flag标志位指针
    int sigNum0 = SIGRTMIN + this->id;              //分配用户自定义中断号
    struct sigaction new_action;                    //串口中断向量管理结构体
    new_action.sa_handler = signal_handler_IO;      //初始化中断服务向量
    sigaction(sigNum0, &new_action, NULL);          //将中断服务绑定到预设端口
    int fd0 = openDev(bindPort0); //打开串口设备
    fcntl(fd0, F_SETOWN, getpid());  //设置本进程可以响应fd对应的SIGNAL中断
    fcntl(fd0, F_SETFL, FASYNC);     //设置fd对应的IO设备寄存器阻塞式读取,实时同步到本进程下
    fcntl(fd0, __F_SETSIG, sigNum0); //设置设备发生中断时提交的中断服务编号
    //串口读取数据
    int Read_Data(int fd)
    {
        fcntl(fd, F_SETFL, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK); //临时设置非阻塞式读取
        int nread = 0;
        int tempBuffSize = MAX_PACKAGE_SIZE * 5; //临时可以缓存5个包
        int findBegin = -1, findEnd = -1;
        char *tempBuff = (char *)malloc(tempBuffSize);
        char *buff = (char *)malloc(MAX_PACKAGE_SIZE);
        AddrGenerator *dag = new AddrGenerator(tempBuff, tempBuffSize);
        memset(buff, 0, sizeof(buff)); //buff清零
    
        while (nread = read(fd, dag->getCurrentAddr(0), tempBuffSize) > 0)
        {
            for (int i = 0; i < tempBuffSize; i++)
            {
                if (dag->getCurrentByte(i) == (char)0x55 && dag->getCurrentByte(i - 1) == (char)0xaa)
                {
                    findBegin = i - 1; //找到了包头
                }
                if (dag->getCurrentByte(i) == '\r' && dag->getCurrentByte(i - 1) == '\n')
                {
                    findEnd = i; //找到了包尾
                }
                if (findBegin >= 0 && findEnd >= 0 && findBegin < findEnd) //已经找到了合格的网包
                {
                    int pklength = findEnd - findBegin + 1;
                    memcpy(buff, dag->getCurrentAddr(findBegin), pklength); //整理出包片段
                    MyPackage *pk = new MyPackage(buff);
                    this->recieveCallBack(pk);
                    findBegin = -1;
                    findEnd = -1; //重新寻找下一个合格网包
                }
            }
        }
        fcntl(fd, F_SETFL, FASYNC); //恢复阻塞式读取,方便接听中断
        return 0;
    }
    do
    {
        if (read_flag & pointer0) //查看绑定的目标串口是否收到了数据
    	{
            int length = Read_Data(fd0);
            read_flag &= (~pointer0); //读取完成后复位read_flag标志位,表示目前没有数据
    	}
    }while(1);
    
    
  • IDA调试器默认情况下会对这些中断信号进行挂起(其实大多数调试器都会这么做),也就是遇到中断信号时使用自己的进程接管中断信号然后弹出窗口让逆向工程师决定如何操作,而不是正常提交给用户层,这就造成如果底层SO库线程多次使用中断信号进行通信,而IDA又接管了中断信号,就会造成线程间通信不正常,该退出的线程不退出,线程池中挤压大量异常线程,进而造成主线程运行走飞,IDA异常退出的连锁反应。理解了原理就好办了,我们只需要取消IDA的挂起机制,让中断信号能够正常转发给用户层,就可以维持程序的正常运行了。

  • 在之前配置主机地址和端口号的界面选择Debug-options,弹出窗口中选择Edit exceptions打开中断管理列表 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tEK0up9M-1637506695086)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121163818828.png)]

  • 经测试可知“新浪博客”大量使用33号自定义中断,对应的16进制数为0x21,我们右击选择Insert插入一个自定义中断号 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6Bx7o1A-1637506695087)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121164117408.png)]

  • 弹窗菜单中设置中断标识Name为“新浪进程通信”,中断编号code写33,处理方式选择“Pass to application”直接转发用户层,Report方式选择“Log exception” 在控制台打印中断,然后点击确定即可 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n4E5WK5F-1637506695088)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121164508499.png)]

  • 经过测试发现“新浪博客”还大量使用了SIGCHLD中断,这个中断的编号为0x11在IDA里已经预设好了这个中断的处理方式为“挂起”,咱们需要将它也改写一下,双击SIGCHLD中断,将其设置按如下方式改写。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FySzpnGb-1637506695089)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121164802593.png)]

  • 改写后我们可以发现,我们之前设置的两个中断号,在Suspend字段下的值均为“No”,Passed to字段均为Application,证明发生中断后,调试器已经不会再对程序进行挂起了,而是会直接转发给“新浪博客APP”,至此中断信号的处理设置就完成了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-osI4O5h5-1637506695089)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121165219407.png)]

关键call搜索+断点调试

  • 根据教程我们知道,新浪博客APP在点击推文时,会从SO层调用一个叫native_call的签名函数过来,这个签名函数就声明在libcrossplt.so下面,我们在右侧模块列表中搜索到这个SO库,双击点开可以看到这个SO库封装的函数列表[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UFIOUhxv-1637506695090)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121125559131.png)]

  • 搞过Java+C联合开发的人应该会比较清楚,在C层所有可供Java调用的函数名都是以包名加下划线的方式命名的,然后在Java层声明一个native方法即可按包名和函数名称索引到对应的方法地址,了解了这些之后拿到这个函数列表我们就不那么手足无措了,直接CTRL+F搜索字符串Java_com即可搜索到com包下的所有native方法。Java层方法叫native_call那稍微懂点编程的人都能看出第一个函数就是我们想要的吧。
    在这里插入图片描述

  • 双击打开第一个函数,按F2在函数头部下一个断点,然后在手机上点击推文标题触发断点,然后就是按F8单步调试,改寄存器,按F9直接继续运行这些基本操作了,逆向过的人都知道接下来才是折磨的开始。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zBuJr1vr-1637506695091)(file://D:/Work/Notes/在本地的图片服务器/Android 逆向笔记/image-20211121131131597.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值