OWASP Android Uncrackable1~3练习

2019-11-28 约 490 字 预计阅读 3 分钟

声明:本文 【OWASP Android Uncrackable1~3练习】 由作者 muirelle 于 2019-11-28 09:30:23 首发 先知社区 曾经 浏览数 402 次

感谢 muirelle 的辛苦付出!

学习安卓逆向时偶然发现了OWASP的crackme练习,相关资料也挺多的,正好用来学习下xposed和frida。链接:https://github.com/OWASP/owasp-mstg/tree/master/Crackmes

我使用的环境和工具:

  • x86_64 android10 Pixel_2_API_29_2
  • frida-server 12.7.12
  • frida 12.7.20
  • apktool 2.4.0
  • 夜神模拟器(android5,x86)

Uncrackable1-3

Uncrackable1

一个包含root检测的程序,需要绕过并得到其中的flag

xposed

安装好程序打开之后发现检测到root,点击OK后就结束了程序,无法进行后面的操作

首先静态分析一下文件,在MainActivity中有检测root和debuggable的代码块

通过检查后,程序设置了一个按钮监听器,调用a.a()并传递edit_text中的字符串作为参数来判断输入是否符合条件。

继续跟进,找到了用于判断输入的函数逻辑,可以看到加密方式为AES,并且给出了密钥和密文,而真正的解密函数在另一个包内(sg.vantagepoint.a)。sg.vantagepoint.a.a的a方法的返回值就是解密后的值(注意是byte []类型),我们只需要hook这个包内的a方法并得到返回值就行。

但是首先要绕过MainActivity的root检测,简单粗暴的绕过方式就是直接将这块代码删除,然后重新回编apk。

编写xposed模块:

package com.example.hookuncrack;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class HookMain implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
        if (loadPackageParam.packageName.equals("owasp.mstg.uncrackable1")) {
            try {
                XposedBridge.log("UncrackHOOKED!!!");
                XposedBridge.log("Loaded app: "+loadPackageParam.packageName);
                //Class clazz = loadPackageParam.classLoader.loadClass("sg.vantagepoint.a.a");
                XposedHelpers.findAndHookMethod("sg.vantagepoint.a.a", loadPackageParam.classLoader, "a", byte [].class, byte [].class, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

                    }

                    protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
                        String flag = new String((byte[]) param.getResult());
                        XposedBridge.log("FLAG IS:" + flag);
                    }
                });
            } catch (Throwable e) {
                XposedBridge.log("hook failed");
                XposedBridge.log(e);
            }

        }
    }
}

安装并重启后运行app,随便输入之后就可以在xposed日志中看到被hook的flag

frida

  • 新建js文件,exploit.js:

    Java.perform(function () {
    send("Starting hook");
    /*
      hook java.lang.System.exit, 使该函数只用来输出下面的字符串
      避免了应用的检测机制导致应用退出, 使用该方法绕过Java层的root/debug检测
    */
    var sysexit = Java.use("java.lang.System");
    sysexit.exit.overload("int").implementation = function(var_0) {
      send("java.lang.System.exit(I)V  // We avoid exiting the application  :)");
    };
    
      var a = Java.use("sg.vantagepoint.a.a");
      a.a.overload('[B', '[B').implementation = function(arg1,arg2){
          var ret = this.a.overload("[B","[B").call(this,arg1,arg2);
          var flag="";
          for (var i=0;i<ret.length;i++){
              flag+=String.fromCharCode(ret[i]);
          }
          send("flag: "+flag)
          return ret;
      }
    });
    
  • 打开模拟器中对应的app

  • 进入adb shell开启frida-server
  • 在外面新开shell,使用frida -U owasp.mstg.uncrackable1 -l exploit.js
  • 在app内随便输入并点击按钮触发hook,得到flag

Uncrackable2

使用夜神模拟器(android5,x86)

frida & 静态分析

在1的基础上将flag放到了so文件中,使得xposed的方法无法hook到函数的返回值。所以先使用frida试试

输入检测函数位于CodeCheck类中的a方法,看样子调用的bar函数应该是lib中的函数

把libfoo.so放进ida,注意到Java_sg_vantagepoint_uncrackable2_CodeCheck_bar,应该就是上面找到的函数了


由于ida在分析android so文件时缺少对JNIEnv结构的定义,所以反编译后会看到函数调用会变成a1+736这种难以阅读的形式。为了有更高的可读性,需要手动导入JNIEnv的结构定义,方法如下:

  • File->Load file->Parse c header file
  • 选择jni.h,如果安装了Android studio则一般位于Android-studio\jre\include\下,但是需要修改后才能导入,所以直接贴一个已经改好的下载链接
  • 选中要修改的函数指针,按y键,提示选择类型,直接手动输入JNIEnv*就行(看到各种教程说选择JNIEnv*但是一直没找到,后面发现可以直接手动输入。。。)

修改之后的可读性大大增加了

很容易看出来是将输入的内容和s2的内容对比,把s2转化成ascii就得到了flag:Thanks for all the fish

和上一题一样,用frida hook掉exit函数,绕过检测,再输入flag就行了

hook

虽然lib中的字符串很容易就被找出来了,但是如果生成的字符串的逻辑非常复杂就没办法一眼看出来了,所以要考虑更通用解法。这里尝试hook libfoo.so中的bar函数,直接得到strcmp的参数值,因为第二个参数就是flag。

不知道什么原因r2frida始终连不上夜神,所以换了个Android studio自带的模拟器(x86_64 android10 Pixel_2_API_29_2),重新下载frida-server的时候注意其版本号不能大于主机上frida的版本号。

先尝试一下用frida附加到进程

却被提示有两个同名进程,很奇怪。想起刚才用jadx查看java伪代码时native除了bar()还有一个init(),可能是调用了fork之类的函数?尝试杀掉子进程(pid较大的那一个)再试试

直接提示没有找到进程,所以两个进程都被杀了?那再试试直接用pid附加到父进程进行调试

依然失败,只能进so看看了。

查看函数导出表可知,确实存在init函数,进去看看init到底做了什么

调用了sub_8D0(),所以继续跟进

首先fork出一个子进程,然后调用ptrace将子进程附加到父进程。随后进入while循环,不断判断子进程是否还存在,如果子进程被杀死则调用exit结束掉主进程。这也就解释了为什么之前会看到两个同名的进程,并且杀掉子进程后父进程也会一起被杀掉。查资料后知道了由于程序使用了ptrace将子进程提前附加到父进程(相当于子进程调试父进程),所以我们再用frida附加到父进程调试的话就会报错,因为一个父进程只允许附加一个调试进程。这也是最简单的反调试机制。

frida提供了参数-f FILE,可以在程序运行之前就将脚本注入Zygote,从而绕过了程序自带的反调试检测

编写frida脚本:

setImmediate(function() {

    //hook exit函数,防止点击OK后进程被结束
    Java.perform(function() {
        console.log("[*] Hooking calls to System.exit");
        const exitClass = Java.use("java.lang.System");
        exitClass.exit.implementation = function() {
            console.log("[*] System.exit called");
        }

        //得到libfoo中所有关于strncmp的调用
        var strncmp = undefined;
        var imports = Module.enumerateImportsSync("libfoo.so");

        for( var i = 0; i < imports.length; i++) {
        if(imports[i].name == "strncmp") {
                strncmp = imports[i].address;
                break;
            }

        }

        //过滤出符合要求的strcmp
        Interceptor.attach(strncmp, {
            onEnter: function (args) {
               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
                }
             },
        });
        console.log("[*] Intercepting strncmp");
    });
});

使用命令frida -U -f owasp.mstg.uncrackable2 -l exploit.js --no-pause注入代码,没有报错并且成功hook strncmp得到flag:Thanks for all the fish

patch

使用-f有时会产生各种莫名的报错,所以尝试直接patch libfoo.so

用ida载入libfoo.so,用keypatch将init nop掉,然后放回原来的文件夹,apktool b重新打包。

安装之前要重新签名,否则会安装失败。

还是使用刚才的frida脚本和命令(不用f参数了)

可以更稳定地得到结果

Uncrackable3

放在夜神上莫名闪退,使用x86_64 android10 Pixel_2_API_29_2

反编译代码分析

首先观察MainActivity,与上一题的流程有所不同。

onCreate()首先调用verifyLibs(),并且给init()传入了字符串参数pizzapizzapizzapizzapizz,然后是和之前差不多的debugger检测和root检测。

关注一下verifyLibs函数,通过jadx的反编译代码可以知道,该函数主要完成了对各个版本的so库的crc校验,还有对classes.dex的crc校验。校验方式是重新计算一遍当前文件系统的crc校验码并将其与从apk文件本身获取的crc校验码比较,不同则调用system.exit(0)。这种检验方式只有在直接改动apk内文件时才会检测到差异,如果我们更改了so或者dex并重新打包,apk本身的crc也会重新计算一次,所以不会触发system.exit(0)

so静态分析

进入libfoo.so的init函数,接收参数后和上一题一样调用sub_3910使用ptrace进行反调试,之后再用strncpy将接收的字符串复制到dest(0x7040),猜测应该是之后提供给验证函数bar()作为加密密钥使用。最后++dword_705C

所以应该也可以像上一道题一样将反调试部分nop掉,即将sub_3910() nop掉。先试一下patch之后能不能用frida附加调试

出乎意料的的报错了:Trace/BPT trap

通过backtrace的报错信息找到了导致程序异常退出的是goodbye(),看来是还有一层检测。


找到了goodbye函数后并没有看到其交叉引用,手动找了一圈之后发现了sub_38A0。他启动了一个新线程并执行start_routine()

start_routine()首先打开/proc/self/maps,搜索任何包含'frida'和'xposed'的信息。因为maps会包含这个程序所有的内存映射区域,包括使用frida和xposed等调试器注入框架,所以当start_routine检测到它们的时候就会调用goodbye,并设置signal为6中止进程。二话不说,nop掉。

回到sub_38A0,这里最后也有一个++dword_705c。

然后进入bar()看看

可以看到需要满足dword_705c==2才能进入后面验证flag的流程,所以前面的init和sub_38A0在最后添加++dword_705c是为了确保反调试代码正确运行。

然后是验证flag的代码,判断加密方式是异或,大概是这样:

if(用户输入 == [pizza...] ^ [another key]) return 1;

现在已经知道了pizza,如果能找到另外一个key就能算出最后的flag。可以看到另一个key即是v7,并且函数sub_12c0对v7的值完成了初始化。


sub_12c0的逻辑看似很复杂,但其实只有最后几行代码才完成了对v7的赋值,并且都是固定的数据,可以直接得到。但是以防真的遇到了非常复杂的加密函数,所以这个地方还是用hook得到v7的值比较稳。问题是sub_12c0没有出现在函数导出表中,无法通过符号完成对该函数的hook。

这里学到一个frida新姿势,通过lib基址+函数偏移的方式动态获取函数实际地址,从而完成hook

Java.perform(function () {
  send("Starting hook");
  var arch = Process.arch;
  send("arch: "+arch);
  var sysexit = Java.use("java.lang.System");
  sysexit.exit.overload("int").implementation = function(var_0) {
    send("java.lang.System.exit(I)V  // We avoid exiting the application  :)");
  };

  function do_native_hooks_libfoo(){
      var libfoo_base = Module.findBaseAddress("libfoo.so");
      if(!libfoo_base){
          send("p_foo is null!Returning now");
          return 0;
      }
      else{
          send("libfoo_base: "+libfoo_base);
      }
      var complex_function = libfoo_base.add(0x12c0);

      Interceptor.attach( complex_function, {
          onEnter: function (args) {
              this.pointer = args[0];
          },

          onLeave: function (retval) {
              var buf = Memory.readByteArray(this.pointer,64);
              send("KEY: ");
              console.log(hexdump(buf, {offset: 0, length:64, header: true, ansi: true}));
              var xorkey_location = libfoo_base.add(0x7040);
              var xorkey = Memory.readByteArray(xorkey_location, 64);
              console.log(hexdump(xorkey, {offset: 0, length:64, header: true, ansi: true}));
          }
      });
  }
  do_native_hooks_libfoo();

});

pizza的偏移也能找到,所以利用frida同时hook v7值的同时顺便也获取了pizza的值

执行脚本,得到v7的值

总结

参考资料,三道题做下来也花了挺长时间,题目本身不难,多数时间花在了熟悉xposed和frida的配置和编写模块上面,所以主要还是不够熟悉。

参考:

https://assets-us-01.kc-usercontent.com/0d76cd9b-cf9d-007c-62ee-e50e20111691/8ea37cc4-b30f-447a-85f5-426b28cb1a3d/Mobile%20Security%20Newsletter%20-%20Newsletter%2064.pdf

https://www.52pojie.cn/thread-1048837-1-1.html

关键词:[‘安全技术’, ‘移动安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now