OWASP Android Uncrackable1~3练习
2019-11-28 10:30:23 Author: xz.aliyun.com(查看原文) 阅读量:334 收藏

学习安卓逆向时偶然发现了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

一个包含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


文章来源: http://xz.aliyun.com/t/6819
如有侵权请联系:admin#unsafe.sh