利用owasp学习安卓安全- (Level 1 - Level 4)Challenges Write Up
Ebounce
撰写于 2023年 12月 18 日

利用owasp学习安卓安全- Challenges Write Up

需求工具

题目List地址: MAS-Crackmes

  1. Jadx-反编译Jar包或者APK
  2. adb-直接安装命令行工具或者AndroidStudio都行
  3. Frida-Hook工具
  4. Magisk-刷Root用工具
  5. Magisk-Frida-利用magisk模块启动frida,防止每次都需要手动启动server

怎么刷root,怎么安装frida在PC等问题,网上已经有的资料已经很详细了,笔者这里就不再赘述。

UnCrackable L1

安装APK

题目地址为: UnCrackable-Level1.apk

可以使用adb下载,也可以直接apk在手机上安装,这里记录一下使用的命令

显示已连接设备:

adb devices -l

进入shell模式,如果加了命令就是直接执行命令

adb shell <command>

安装apk:

adb install UnCrackable-Level1.apk

列出已安装的包:

adb shell pm list packages

使用adb传输文件(以传frida-server为例):

adb push ./frida-server /sdcard/Downloads/

观察APP运行

这里我们直接打开APP,会因为检测到root弹出对话框,如果我们点击ok按钮会直接闪退,但是不点击页面会被挂起,这里透过对话框,我们可以看出是需要输入一串字符串进行验证,从而到达下一步。

阅读源码

源码非常简单,整体结构如下:

这里摘录一些比较关键的源码:

  1. sg.vantagepoint.a.a.a是个AES解密函数:
    很简单的函数,接受两个字节流,然后进行AES加密。
  2. MainActivity 这里是APP启动时的主要逻辑:
    第一个框图,表示代码运行到这里时会弹出一个对话框,点击ok按钮后调用system.exit(0)随后程序退出;第二个框图,表示这里进行了root检测;第三个框图是验证的代码,根据打印字符串,可以看出sg.vantagepoint.uncrackable1.a.a是验证的代码,我们需要传值让这个函数通过校验。
  3. 根据上面MainActivity的逻辑,我们去看sg.vantagepoint.uncrackable1.a.a 这里给出了一个非常关键的代码,使用AES解密函数进行解密,并且给出了两个参数的具体值,然后判断解密后的值,是否与输入后的值相等,同时下面还有一处自写的加密函数。

总结,这里逻辑就很清晰了,我们需要传入AES解密函数已知的两个参数,看看究竟需要传入什么字符串才能通过校验。

Frida 动态hook

顺便说一句Jadx有个很无敌的功能,选中对应类或者函数之后,右键可以直接复制成为frida片段:

图上的例子复制为:

let a = Java.use("sg.vantagepoint.a.a");

如果你复制的为类中的函数,一般默认是你会重写这个方法:

let a = Java.use("sg.vantagepoint.a.a");
a["a"].implementation = function (bArr, bArr2) {
    console.log(`a.a is called: bArr=${bArr}, bArr2=${bArr2}`);
    let result = this["a"](bArr, bArr2);
    console.log(`a.a result=${result}`);
    return result;
};

最后我的脚本编写如下:

保存为hook1.js:

Java.perform(function(){
    console.log("[*] Hooking MainActivity...")
    let AnonymousClass1 = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");

    console.log("[*] Loading Base64 Module..")
    var b64tools = Java.use("android.util.Base64")

    console.log("[*] Replacing onClick function....")
    AnonymousClass1["onClick"].implementation = function (){
        console.log("[*] Onclick replaced successfully.")
    } // 替换掉ok按钮绑定的onClick函数为打印内容,防止点击ok按钮后退出。

    console.log("[*] Hook A function...")
    let a = Java.use("sg.vantagepoint.uncrackable1.a");

    let key = a.b("8d127684cbc37c17616d806cf50473cc")
    console.log(`[*] Get parameter1: ${key}`)

    let base64decode_key = b64tools.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=",0)
    console.log(`[*] Get parameter2: ${base64decode_key}`)
    
    console.log("[*] ")
    let AESfunction = Java.use("sg.vantagepoint.a.a");
    let aes_result = AESfunction.a(key,base64decode_key);
    console.log(`[*] AES result: ${aes_result}`)

    let string_result = ""
    for(var i=0; i < aes_result.length; i++){
        string_result += (String.fromCharCode(aes_result[i]))
    }
    console.log(`[*] Flag key : ${string_result}`)

})

运行frida:

frida -U hook1.js Uncrackable1

运行效果为

这里需要的是进程名,不是包名,具体可以使用frida-ps -U进行查询:

由于ok按钮绑定的onClick事件已经被重写了,所以我们按下ok不会退出,而会打印特定字符串:

然后我们只需要在app中输入Flag key就行:

UnCrackable L2

题目地址:UnCrackable L2
安装APP这一步不再赘述了,和前文一样的。

这题比想象中的简单,只需要会一点二进制的知识就可以了。

观察APP运行

这里APP运行和Level 1的情况是类似的:

阅读源码

源码部分和Level 1大同小异,同样在MainActivity这里创建有root检测,检测之后会生成对话框,如果点ok会直接退出程序,我们通过frida重写绕过即可,主要是如果达到正确分支这里:
.

这里调用的this.m.a根据前文,this.m为:

那我们就去阅读a函数:

出现了native关键词,这个一般都是调用lib里面用C写的原生函数时需要注明的,因此我们需要去看lib.so

非常庆幸只有一个lib,接下我们只需要讲apk解压,或者.so再进行二进制分析即可,拖入IDA,因为自己的电脑是x64架构所以这里用了x64lib,首先查看函数表:

显然这就是我们要找的函数了:

由于菜的如我,读不太懂x64汇编,这里使用无敌的F5反汇编,得到IDA猜测的C源码:

这里代码还是很简单的,上面是预定义了一些需要使用的变量,其中有个strcpy函数,熟悉C的同学知道,这个字符串拷贝到某个变量里面,虽然有一段很长的内容,这里猜测就是从Java那边接收获得的值,最后利用strncmp函数比较接受来的值和v6是否相同,相同返回0,不相同返回1,最后取反,然后赋值result
此时这个思路就很清晰了,我们绕过root检测的退出机制,输入copyv6值即可通关。

Firda Hook

Hook脚本如下,直接使用jadx的复制frida片段功能,获得onclick函数,然后进行重写.

Java.perform(function(){
    console.log("[*] Bypassing the root Detect.")
    let AnonymousClass1 = Java.use("sg.vantagepoint.uncrackable2.MainActivity$1");
    AnonymousClass1["onClick"].implementation = function () {
        console.log("[*] System.exit(0) is replaced")
    };
    
})

最后frida运行即可(这里别忘了使用frida-ps查看进程名,或者直接使用pid也可以)

frida -U -l hook2.js "Uncrackable level 2"

frida -U -l hook2.js -p <pid>

最后输入指定字符Thanks for all the fish

UnCrackable L3

题目地址: UnCrackable L3

观察APP运行情况

这题和前面两题一样,都是会进行root检测并且闪退,但是如果我们直接使用之前绕过root的hook脚本,这里也会闪退了:

阅读源码

拖入JADX中进行分析,核心逻辑还是在MainActivity里面分为三块:

  1. verifylib()
    如果不知道具体这个函数是干什么的,我们可以通过阅读adb logcat来观察,这里从源码来看是进行了输出的 我们注意到,这里出现了tamper Detected,同时这里打印源码中的log信息,我们可以看出来这里应该是将libxxx.so移动到指定位置。
  2. xor_key,这个变量名提示很明显了,显然本题是使用了异或进行编码,先记下这个值:
  3. verify(),其实就是通关的逻辑代码,我们看看这里调用了什么函数

跟着源码的指引查看check.check_code函数:
又是一个native函数了,是时候使用ida

Native函数

这里使用x64libfoo.so,主要是x86汇编熟悉一点(虽然最后直接用了反汇编功能),先看函数表,找到我们的bar函数:

然后使用反汇编,看看源码逻辑:

其实这里IDA的反汇编有点非常难读但是我们可以提取一些信息出来

  1. 第一个框图我们发现,前面将v7进行了初始化操作,但是经过一个函数之后,v7参与了后面的异或运算,很显然sub_12c0v7进行了赋值操作
  2. 框图2和框图3需要仔细分析一下,我们挨个分析里面出现的值

    • v7这个变量我们根据sub_12c0函数进行分析,发现应该是存在内存中的常量 这个值我们只能通过动态读取内存来获得。
    • v5熟悉x86汇编的朋友,看到寄存器rcx,就会知道这是个下标,rcx通常是一个counter,常用在循环里面进行计数.
    • dest这个值不认识,但是IDA分析出这是个char[],阅读init函数时,发现dest在这里进行了赋值 ,这里又出现了一个v4,我们回到apk源码中:

      答案揭晓了,destxor_key,到此我们可以重写这一坨内容了,用伪代码表示可以这样理解,唯一一个比较奇怪的就是,这里IDA理解为v5每次递增3,每次循环比较3个字符了。

      xor_key = "a"*24
      input = "b"*24
      memory_string = "c"*24
      for(i = 0;i > 24;i++){
       if (i == 24){
           return 1
       }
       temp_char = memory_string[i]^xor_key[i]
       if(b[i] == temp_char){
           continue
       }
       return 0
      }
  3. 这里还有一个值得注意的函数 这里明显检查了fridaxposed,根据之前的adb日志可以知道,我们就是因为触发了这一步才被exit的,后面的goodbye函数就是exit(0)

分析到这里,要解出这个secret我们需要下面几步:

  • 绕过native层的frida或者xposed校验
  • 取得内存中的v7
  • 利用xor_keyv7进行异或,得到secret
  • 输入secret前绕过java层的root检测

绕过Native层的hook校验

这里我们通过观察源码,可以hook这里面的判断函数进行绕过,我这里hook的是C标准库的strstr函数,写法为:

Java.perform(function(){
    /*
    Hook C external Function
    */
    console.log("[*]Try to hook C lib")

    var P_strstr = Module.findExportByName("libc.so","strstr")//只有导出的函数才可以利用这个方法找到地址
    var strstr = new NativeFunction(P_strstr,'int',['pointer','pointer']) // 第一个参数为要修改的函数地址,第二个参数为返回值,第三个参数为传参类型

    console.log("[*] overwriting strstr function")

    Interceptor.replace(P_strstr, new NativeCallback(function(haystack, needle){
        var retval = 0;
        return retval
    },'int',['pointer','pointer']));
    // 这里我将strstr函数,重写为不管判断什么字符串都只会返回0,也就是不匹配
})

你也可以hookfopen函数,解法不唯一.

获得内存中v7的值

这里需要使用Interceptor.attach,写法为:

Java.perform(function(){
        /*
    Hook Native Function by address
    */
   setTimeout(()=>{
    var baseAddress = Module.findBaseAddress("libfoo.so")
    console.log("[*] BaseAddress is "+baseAddress)
    var funcAddress = baseAddress.add(0x10E0)
    Interceptor.attach(funcAddress,{
        onEnter: function(args){
            this.buf = args[0]
            console.log(hexdump(this.buf,{offset: 0,
                length: 0x20,
                header: true,
                ansi: true}))
        },
        onLeave: function(retval){
            var length = 24
            var buff = Memory.readByteArray(this.buf,length)
            var secret_key = new Uint8Array(buff)
            var str_key = ""
            for(var i = 0;i<secret_key.length;i++){
                str_key += secret_key[i].toString(16)
            }
            var xor_key = "pizzapizzapizzapizzapizz"
            var secret = []
            for(var i = 0; i < length;i++){
                secret[i] = String.fromCharCode(secret_key[i]^xor_key.charCodeAt(i))
            }
            console.log(secret.join(''))
        }
    })
   },1000)
   // 这里如果不延迟,会造成so文件还没有加载,就已经运行了找基址的代码,从而获得null导致代码出错
}

有一些小细节需要注意,

  1. 因为之前源码分析中给v7赋值的函数不是导出函数,所以无法使用findExportByName, 这个可以在IDA上check
  2. 由于findBaseAddress这个函数只是寻找.so的基址,如果我们需要访问到函数,需要加上函数的偏移量,才能让指针指向正确的函数,对于x64来说,这个函数的偏移量为 ,然后因为我用的是arm64-v8a的实机,所以偏移量为 通过具体环境根据IDA来判断具体偏移量。
  3. 读几个字节是由长度来判断的,因为这里secret长度为24,所以我们只需要读24个字节。
  4. onEnter表示hook到这个函数还未执行时进行的操作,onLeave则是函数退出时进行的操作,hook函数因为需要取得运行完之后的值,所以这里需要等secret生成函数执行完,才能进行读取。

最后完整脚本如下:

Java.perform(function(){
    /*
    Hook C external Function
    */
    console.log("[*]Try to hook C lib")

    var P_strstr = Module.findExportByName("libc.so","strstr")
    var strstr = new NativeFunction(P_strstr,'int',['pointer','pointer'])

    console.log("[*] overwriting strstr function")

    Interceptor.replace(P_strstr, new NativeCallback(function(haystack, needle){
        var retval = 0;
        return retval
    },'int',['pointer','pointer']));

    

    /*
    Hook Native Function by address
    */
   setTimeout(()=>{
    var baseAddress = Module.findBaseAddress("libfoo.so")
    console.log("[*] BaseAddress is "+baseAddress)
    var funcAddress = baseAddress.add(0x10E0)
    Interceptor.attach(funcAddress,{
        onEnter: function(args){
            this.buf = args[0]
            console.log(hexdump(this.buf,{offset: 0,
                length: 0x20,
                header: true,
                ansi: true}))
        },
        onLeave: function(retval){
            var length = 24
            var buff = Memory.readByteArray(this.buf,length)
            var secret_key = new Uint8Array(buff)
            var str_key = ""
            for(var i = 0;i<secret_key.length;i++){
                str_key += secret_key[i].toString(16)
            }
            var xor_key = "pizzapizzapizzapizzapizz"
            var secret = []
            for(var i = 0; i < length;i++){
                secret[i] = String.fromCharCode(secret_key[i]^xor_key.charCodeAt(i))
            }
            console.log(secret.join(''))
        }
    })
   },1000)
   // 这里如果不延迟,会造成so文件还没有加载,就已经运行了找基址的代码,从而获得null导致代码出错
    console.log("[*] Start Hooking Java")
    /*
    Hook Java
    */
    var systemFunc = Java.use('java.lang.System')
    systemFunc.exit.implementation = function(v0){
        console.log("[*] No exit, Thanks!")
    }
    console.log("[*]OK, No root detect now")
})

结果为
首先随便输入一个内容,或者内存中的key,这也是第一个output的来源

最后输入正确内容,通关!

UnCrackable L4

观察运行情况

直接闪退

分析源码

主要看一下MainActivity:

看起来似乎该APP进行了某种加密操作:

这个函数是个Native函数

public native byte[] gXftm3iswpkVgBNDUp(byte[] bArr, byte b2);
static {
    System.loadLibrary("native-lib");
}

去到native-lib去看,发现全部进行了混淆:

绕过Native层校验

由于lib进行了混淆,对于我这个二进制苦手,基本上就分析不下去了,也暂时没那个能力去混淆,所以只能利用Frida,进行曲线救国。

利用Frida Hook导出函数获得信息

这里换一下ghidra(原因是我喜欢)

先看导出函数的列表:

然后利用frida,Hook得到他们的入参或者返回值,比如我要hookopen函数

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            console.log("[+] Open parameter: "+ this.open_args.readCString())
        }
    })
}

Java.perform(function(){
    hook_C_open();
})

根据具体函数的功能,来判断到底是Hook 入参还是Hook 返回值,最后测试出两个返回内容有用的函数:

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            console.log("[+] Open parameter: "+ this.open_args.readCString())
        }
    })
}

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            console.log("[+] Snprintf parameter: "+ this.snprintf_args.readCString())
        }
    })
}

Java.perform(function(){
    hook_C_open();
    hook_C_snprintf();
})

运行结果为:

我们可以发现读到/proc/self/8908/status就退出了,很显然这里发生了什么,下面是一个反推的过程,正常思路是先发现在某个地方退出了,然后和PID联系起来,然后又运行一次Hook脚本进行验证。

首先,我们得知道这个8908是啥,由于这里是复现了,就直接上结果了

frida -U -l .\test.js -f re.pwnme --pause

查询到启动应用的PID(如果你跟我一样使用的是小米实机,那么没有%resume之前,都是无法查询到PID的,这时候应用还没有完全启动,需要将应用放到后台,然后进行%resume)

ps -A | grep "re.pwnme"

或者

ps -A

查询到了对应的应用的PID:

和上面的snprintf的第一条就对起来了,我们先查看PID 8889下的内容:

cd /proc/8889/task

然后读下面的status:

cat ./*/status | awk "/(Name:)|(^Pid:)/"

和退出的那一条对上了PID: 8908 Name: gmain,而gmainfridahook时产生的进程,因此这里主要是检测frida,从这一条我们就可以知道是如何检测frida的了,该Native库会读主要进程下面所有子进程的status,如果检测到frida产生的进程,则直接退出。

所以我们可以利用fridasnprintf入参的时候,对参数进行修改,保证我们gmain不会被检测到,或者检测到虚假的status都可以.
我利用一个安全线程的status信息,并重写snprintf获得参数

cat /proc/[PID]/status/ > /data/tmp/status

然后重写hook_c_snprintf方法

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            console.log("[+] Snprintf parameter: " + this.snprintf_args.readCString())
            if(this.snprintf_args.readCString().search("status") != -1){
                this.snprintf_args.writeUtf8String("/data/tmp/status")
                console.log("[+] Snprintf changed parameter: " + this.snprintf_args.readCString())
            }
        }
    })
}

运行得到如下结果:
这里其实已经达到Java层了,值得注意的是我们发现这里检查了是否手机拥有su来判断是否有root权限

同理我们也可以修改open的入参来规避掉这个可能因素,这里我将path指向了一个不存在su的地方

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            if(this.open_args.readCString().search("su") != -1){
                this.open_args.writeUtf8String("/data/tmp/su")
            }
        }
    })
}

回归到真正报错的地方,这里提示是MainActivity报错仔细审阅代码之后,发现onCreate方法有个除0错误:

显然我们进了这个IF语句,查看rb.j方法,发现这个方法应该是在Java层检查root或者其他问题:

这里我通过Hook rb.j直接返回false

function Avoid_divide_by_zero(){
    let b = Java.use("b.a.a.b");
    b["j"].implementation = function () {
        return false
    };
}

完整Hook脚本如下:

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            if(this.open_args.readCString().search("su") != -1){
                this.open_args.writeUtf8String("/data/tmp/su")
            }
        }
    })
}

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            if(this.snprintf_args.readCString().search("status") != -1){
                this.snprintf_args.writeUtf8String("/data/tmp/status")
            }
        }
    })
}

function Avoid_divide_by_zero(){
    let b = Java.use("b.a.a.b");
    b["j"].implementation = function () {
        return false
    };
}

Java.perform(function(){
    hook_C_snprintf();
    console.log("[+] Hook Open function - All path related 'status' replaced to /data/tmp/status - Done")
    hook_C_open();
    console.log("[+] Hook Open function - All path related 'su' replaced to /data/tmp/su - Done")
    Avoid_divide_by_zero();
    console.log("[+] Avoid onCreate Function to divide by zero - Done")
})

运行之后发现APP可以正常使用了:


Part2需要找到正确的key和pin码,这里贴一下WP链接,有兴趣可以研究一下, 我这里就不继续搞了。
r2-pay: whitebox (part 2)

利用owasp学习安卓安全- (Level 1 - Level 4)Challenges Write Up

温馨提示:

本文最后更新于2024年01月17日,已超过100天没有更新,若内容或图片失效,请留言反馈。

利用owasp学习安卓安全- Challenges Write Up

需求工具

题目List地址: MAS-Crackmes

  1. Jadx-反编译Jar包或者APK
  2. adb-直接安装命令行工具或者AndroidStudio都行
  3. Frida-Hook工具
  4. Magisk-刷Root用工具
  5. Magisk-Frida-利用magisk模块启动frida,防止每次都需要手动启动server

怎么刷root,怎么安装frida在PC等问题,网上已经有的资料已经很详细了,笔者这里就不再赘述。

UnCrackable L1

安装APK

题目地址为: UnCrackable-Level1.apk

可以使用adb下载,也可以直接apk在手机上安装,这里记录一下使用的命令

显示已连接设备:

adb devices -l

进入shell模式,如果加了命令就是直接执行命令

adb shell <command>

安装apk:

adb install UnCrackable-Level1.apk

列出已安装的包:

adb shell pm list packages

使用adb传输文件(以传frida-server为例):

adb push ./frida-server /sdcard/Downloads/

观察APP运行

这里我们直接打开APP,会因为检测到root弹出对话框,如果我们点击ok按钮会直接闪退,但是不点击页面会被挂起,这里透过对话框,我们可以看出是需要输入一串字符串进行验证,从而到达下一步。

阅读源码

源码非常简单,整体结构如下:

这里摘录一些比较关键的源码:

  1. sg.vantagepoint.a.a.a是个AES解密函数:
    很简单的函数,接受两个字节流,然后进行AES加密。
  2. MainActivity 这里是APP启动时的主要逻辑:
    第一个框图,表示代码运行到这里时会弹出一个对话框,点击ok按钮后调用system.exit(0)随后程序退出;第二个框图,表示这里进行了root检测;第三个框图是验证的代码,根据打印字符串,可以看出sg.vantagepoint.uncrackable1.a.a是验证的代码,我们需要传值让这个函数通过校验。
  3. 根据上面MainActivity的逻辑,我们去看sg.vantagepoint.uncrackable1.a.a 这里给出了一个非常关键的代码,使用AES解密函数进行解密,并且给出了两个参数的具体值,然后判断解密后的值,是否与输入后的值相等,同时下面还有一处自写的加密函数。

总结,这里逻辑就很清晰了,我们需要传入AES解密函数已知的两个参数,看看究竟需要传入什么字符串才能通过校验。

Frida 动态hook

顺便说一句Jadx有个很无敌的功能,选中对应类或者函数之后,右键可以直接复制成为frida片段:

图上的例子复制为:

let a = Java.use("sg.vantagepoint.a.a");

如果你复制的为类中的函数,一般默认是你会重写这个方法:

let a = Java.use("sg.vantagepoint.a.a");
a["a"].implementation = function (bArr, bArr2) {
    console.log(`a.a is called: bArr=${bArr}, bArr2=${bArr2}`);
    let result = this["a"](bArr, bArr2);
    console.log(`a.a result=${result}`);
    return result;
};

最后我的脚本编写如下:

保存为hook1.js:

Java.perform(function(){
    console.log("[*] Hooking MainActivity...")
    let AnonymousClass1 = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");

    console.log("[*] Loading Base64 Module..")
    var b64tools = Java.use("android.util.Base64")

    console.log("[*] Replacing onClick function....")
    AnonymousClass1["onClick"].implementation = function (){
        console.log("[*] Onclick replaced successfully.")
    } // 替换掉ok按钮绑定的onClick函数为打印内容,防止点击ok按钮后退出。

    console.log("[*] Hook A function...")
    let a = Java.use("sg.vantagepoint.uncrackable1.a");

    let key = a.b("8d127684cbc37c17616d806cf50473cc")
    console.log(`[*] Get parameter1: ${key}`)

    let base64decode_key = b64tools.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=",0)
    console.log(`[*] Get parameter2: ${base64decode_key}`)
    
    console.log("[*] ")
    let AESfunction = Java.use("sg.vantagepoint.a.a");
    let aes_result = AESfunction.a(key,base64decode_key);
    console.log(`[*] AES result: ${aes_result}`)

    let string_result = ""
    for(var i=0; i < aes_result.length; i++){
        string_result += (String.fromCharCode(aes_result[i]))
    }
    console.log(`[*] Flag key : ${string_result}`)

})

运行frida:

frida -U hook1.js Uncrackable1

运行效果为

这里需要的是进程名,不是包名,具体可以使用frida-ps -U进行查询:

由于ok按钮绑定的onClick事件已经被重写了,所以我们按下ok不会退出,而会打印特定字符串:

然后我们只需要在app中输入Flag key就行:

UnCrackable L2

题目地址:UnCrackable L2
安装APP这一步不再赘述了,和前文一样的。

这题比想象中的简单,只需要会一点二进制的知识就可以了。

观察APP运行

这里APP运行和Level 1的情况是类似的:

阅读源码

源码部分和Level 1大同小异,同样在MainActivity这里创建有root检测,检测之后会生成对话框,如果点ok会直接退出程序,我们通过frida重写绕过即可,主要是如果达到正确分支这里:
.

这里调用的this.m.a根据前文,this.m为:

那我们就去阅读a函数:

出现了native关键词,这个一般都是调用lib里面用C写的原生函数时需要注明的,因此我们需要去看lib.so

非常庆幸只有一个lib,接下我们只需要讲apk解压,或者.so再进行二进制分析即可,拖入IDA,因为自己的电脑是x64架构所以这里用了x64lib,首先查看函数表:

显然这就是我们要找的函数了:

由于菜的如我,读不太懂x64汇编,这里使用无敌的F5反汇编,得到IDA猜测的C源码:

这里代码还是很简单的,上面是预定义了一些需要使用的变量,其中有个strcpy函数,熟悉C的同学知道,这个字符串拷贝到某个变量里面,虽然有一段很长的内容,这里猜测就是从Java那边接收获得的值,最后利用strncmp函数比较接受来的值和v6是否相同,相同返回0,不相同返回1,最后取反,然后赋值result
此时这个思路就很清晰了,我们绕过root检测的退出机制,输入copyv6值即可通关。

Firda Hook

Hook脚本如下,直接使用jadx的复制frida片段功能,获得onclick函数,然后进行重写.

Java.perform(function(){
    console.log("[*] Bypassing the root Detect.")
    let AnonymousClass1 = Java.use("sg.vantagepoint.uncrackable2.MainActivity$1");
    AnonymousClass1["onClick"].implementation = function () {
        console.log("[*] System.exit(0) is replaced")
    };
    
})

最后frida运行即可(这里别忘了使用frida-ps查看进程名,或者直接使用pid也可以)

frida -U -l hook2.js "Uncrackable level 2"

frida -U -l hook2.js -p <pid>

最后输入指定字符Thanks for all the fish

UnCrackable L3

题目地址: UnCrackable L3

观察APP运行情况

这题和前面两题一样,都是会进行root检测并且闪退,但是如果我们直接使用之前绕过root的hook脚本,这里也会闪退了:

阅读源码

拖入JADX中进行分析,核心逻辑还是在MainActivity里面分为三块:

  1. verifylib()
    如果不知道具体这个函数是干什么的,我们可以通过阅读adb logcat来观察,这里从源码来看是进行了输出的 我们注意到,这里出现了tamper Detected,同时这里打印源码中的log信息,我们可以看出来这里应该是将libxxx.so移动到指定位置。
  2. xor_key,这个变量名提示很明显了,显然本题是使用了异或进行编码,先记下这个值:
  3. verify(),其实就是通关的逻辑代码,我们看看这里调用了什么函数

跟着源码的指引查看check.check_code函数:
又是一个native函数了,是时候使用ida

Native函数

这里使用x64libfoo.so,主要是x86汇编熟悉一点(虽然最后直接用了反汇编功能),先看函数表,找到我们的bar函数:

然后使用反汇编,看看源码逻辑:

其实这里IDA的反汇编有点非常难读但是我们可以提取一些信息出来

  1. 第一个框图我们发现,前面将v7进行了初始化操作,但是经过一个函数之后,v7参与了后面的异或运算,很显然sub_12c0v7进行了赋值操作
  2. 框图2和框图3需要仔细分析一下,我们挨个分析里面出现的值

    • v7这个变量我们根据sub_12c0函数进行分析,发现应该是存在内存中的常量 这个值我们只能通过动态读取内存来获得。
    • v5熟悉x86汇编的朋友,看到寄存器rcx,就会知道这是个下标,rcx通常是一个counter,常用在循环里面进行计数.
    • dest这个值不认识,但是IDA分析出这是个char[],阅读init函数时,发现dest在这里进行了赋值 ,这里又出现了一个v4,我们回到apk源码中:

      答案揭晓了,destxor_key,到此我们可以重写这一坨内容了,用伪代码表示可以这样理解,唯一一个比较奇怪的就是,这里IDA理解为v5每次递增3,每次循环比较3个字符了。

      xor_key = "a"*24
      input = "b"*24
      memory_string = "c"*24
      for(i = 0;i > 24;i++){
       if (i == 24){
           return 1
       }
       temp_char = memory_string[i]^xor_key[i]
       if(b[i] == temp_char){
           continue
       }
       return 0
      }
  3. 这里还有一个值得注意的函数 这里明显检查了fridaxposed,根据之前的adb日志可以知道,我们就是因为触发了这一步才被exit的,后面的goodbye函数就是exit(0)

分析到这里,要解出这个secret我们需要下面几步:

  • 绕过native层的frida或者xposed校验
  • 取得内存中的v7
  • 利用xor_keyv7进行异或,得到secret
  • 输入secret前绕过java层的root检测

绕过Native层的hook校验

这里我们通过观察源码,可以hook这里面的判断函数进行绕过,我这里hook的是C标准库的strstr函数,写法为:

Java.perform(function(){
    /*
    Hook C external Function
    */
    console.log("[*]Try to hook C lib")

    var P_strstr = Module.findExportByName("libc.so","strstr")//只有导出的函数才可以利用这个方法找到地址
    var strstr = new NativeFunction(P_strstr,'int',['pointer','pointer']) // 第一个参数为要修改的函数地址,第二个参数为返回值,第三个参数为传参类型

    console.log("[*] overwriting strstr function")

    Interceptor.replace(P_strstr, new NativeCallback(function(haystack, needle){
        var retval = 0;
        return retval
    },'int',['pointer','pointer']));
    // 这里我将strstr函数,重写为不管判断什么字符串都只会返回0,也就是不匹配
})

你也可以hookfopen函数,解法不唯一.

获得内存中v7的值

这里需要使用Interceptor.attach,写法为:

Java.perform(function(){
        /*
    Hook Native Function by address
    */
   setTimeout(()=>{
    var baseAddress = Module.findBaseAddress("libfoo.so")
    console.log("[*] BaseAddress is "+baseAddress)
    var funcAddress = baseAddress.add(0x10E0)
    Interceptor.attach(funcAddress,{
        onEnter: function(args){
            this.buf = args[0]
            console.log(hexdump(this.buf,{offset: 0,
                length: 0x20,
                header: true,
                ansi: true}))
        },
        onLeave: function(retval){
            var length = 24
            var buff = Memory.readByteArray(this.buf,length)
            var secret_key = new Uint8Array(buff)
            var str_key = ""
            for(var i = 0;i<secret_key.length;i++){
                str_key += secret_key[i].toString(16)
            }
            var xor_key = "pizzapizzapizzapizzapizz"
            var secret = []
            for(var i = 0; i < length;i++){
                secret[i] = String.fromCharCode(secret_key[i]^xor_key.charCodeAt(i))
            }
            console.log(secret.join(''))
        }
    })
   },1000)
   // 这里如果不延迟,会造成so文件还没有加载,就已经运行了找基址的代码,从而获得null导致代码出错
}

有一些小细节需要注意,

  1. 因为之前源码分析中给v7赋值的函数不是导出函数,所以无法使用findExportByName, 这个可以在IDA上check
  2. 由于findBaseAddress这个函数只是寻找.so的基址,如果我们需要访问到函数,需要加上函数的偏移量,才能让指针指向正确的函数,对于x64来说,这个函数的偏移量为 ,然后因为我用的是arm64-v8a的实机,所以偏移量为 通过具体环境根据IDA来判断具体偏移量。
  3. 读几个字节是由长度来判断的,因为这里secret长度为24,所以我们只需要读24个字节。
  4. onEnter表示hook到这个函数还未执行时进行的操作,onLeave则是函数退出时进行的操作,hook函数因为需要取得运行完之后的值,所以这里需要等secret生成函数执行完,才能进行读取。

最后完整脚本如下:

Java.perform(function(){
    /*
    Hook C external Function
    */
    console.log("[*]Try to hook C lib")

    var P_strstr = Module.findExportByName("libc.so","strstr")
    var strstr = new NativeFunction(P_strstr,'int',['pointer','pointer'])

    console.log("[*] overwriting strstr function")

    Interceptor.replace(P_strstr, new NativeCallback(function(haystack, needle){
        var retval = 0;
        return retval
    },'int',['pointer','pointer']));

    

    /*
    Hook Native Function by address
    */
   setTimeout(()=>{
    var baseAddress = Module.findBaseAddress("libfoo.so")
    console.log("[*] BaseAddress is "+baseAddress)
    var funcAddress = baseAddress.add(0x10E0)
    Interceptor.attach(funcAddress,{
        onEnter: function(args){
            this.buf = args[0]
            console.log(hexdump(this.buf,{offset: 0,
                length: 0x20,
                header: true,
                ansi: true}))
        },
        onLeave: function(retval){
            var length = 24
            var buff = Memory.readByteArray(this.buf,length)
            var secret_key = new Uint8Array(buff)
            var str_key = ""
            for(var i = 0;i<secret_key.length;i++){
                str_key += secret_key[i].toString(16)
            }
            var xor_key = "pizzapizzapizzapizzapizz"
            var secret = []
            for(var i = 0; i < length;i++){
                secret[i] = String.fromCharCode(secret_key[i]^xor_key.charCodeAt(i))
            }
            console.log(secret.join(''))
        }
    })
   },1000)
   // 这里如果不延迟,会造成so文件还没有加载,就已经运行了找基址的代码,从而获得null导致代码出错
    console.log("[*] Start Hooking Java")
    /*
    Hook Java
    */
    var systemFunc = Java.use('java.lang.System')
    systemFunc.exit.implementation = function(v0){
        console.log("[*] No exit, Thanks!")
    }
    console.log("[*]OK, No root detect now")
})

结果为
首先随便输入一个内容,或者内存中的key,这也是第一个output的来源

最后输入正确内容,通关!

UnCrackable L4

观察运行情况

直接闪退

分析源码

主要看一下MainActivity:

看起来似乎该APP进行了某种加密操作:

这个函数是个Native函数

public native byte[] gXftm3iswpkVgBNDUp(byte[] bArr, byte b2);
static {
    System.loadLibrary("native-lib");
}

去到native-lib去看,发现全部进行了混淆:

绕过Native层校验

由于lib进行了混淆,对于我这个二进制苦手,基本上就分析不下去了,也暂时没那个能力去混淆,所以只能利用Frida,进行曲线救国。

利用Frida Hook导出函数获得信息

这里换一下ghidra(原因是我喜欢)

先看导出函数的列表:

然后利用frida,Hook得到他们的入参或者返回值,比如我要hookopen函数

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            console.log("[+] Open parameter: "+ this.open_args.readCString())
        }
    })
}

Java.perform(function(){
    hook_C_open();
})

根据具体函数的功能,来判断到底是Hook 入参还是Hook 返回值,最后测试出两个返回内容有用的函数:

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            console.log("[+] Open parameter: "+ this.open_args.readCString())
        }
    })
}

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            console.log("[+] Snprintf parameter: "+ this.snprintf_args.readCString())
        }
    })
}

Java.perform(function(){
    hook_C_open();
    hook_C_snprintf();
})

运行结果为:

我们可以发现读到/proc/self/8908/status就退出了,很显然这里发生了什么,下面是一个反推的过程,正常思路是先发现在某个地方退出了,然后和PID联系起来,然后又运行一次Hook脚本进行验证。

首先,我们得知道这个8908是啥,由于这里是复现了,就直接上结果了

frida -U -l .\test.js -f re.pwnme --pause

查询到启动应用的PID(如果你跟我一样使用的是小米实机,那么没有%resume之前,都是无法查询到PID的,这时候应用还没有完全启动,需要将应用放到后台,然后进行%resume)

ps -A | grep "re.pwnme"

或者

ps -A

查询到了对应的应用的PID:

和上面的snprintf的第一条就对起来了,我们先查看PID 8889下的内容:

cd /proc/8889/task

然后读下面的status:

cat ./*/status | awk "/(Name:)|(^Pid:)/"

和退出的那一条对上了PID: 8908 Name: gmain,而gmainfridahook时产生的进程,因此这里主要是检测frida,从这一条我们就可以知道是如何检测frida的了,该Native库会读主要进程下面所有子进程的status,如果检测到frida产生的进程,则直接退出。

所以我们可以利用fridasnprintf入参的时候,对参数进行修改,保证我们gmain不会被检测到,或者检测到虚假的status都可以.
我利用一个安全线程的status信息,并重写snprintf获得参数

cat /proc/[PID]/status/ > /data/tmp/status

然后重写hook_c_snprintf方法

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            console.log("[+] Snprintf parameter: " + this.snprintf_args.readCString())
            if(this.snprintf_args.readCString().search("status") != -1){
                this.snprintf_args.writeUtf8String("/data/tmp/status")
                console.log("[+] Snprintf changed parameter: " + this.snprintf_args.readCString())
            }
        }
    })
}

运行得到如下结果:
这里其实已经达到Java层了,值得注意的是我们发现这里检查了是否手机拥有su来判断是否有root权限

同理我们也可以修改open的入参来规避掉这个可能因素,这里我将path指向了一个不存在su的地方

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            if(this.open_args.readCString().search("su") != -1){
                this.open_args.writeUtf8String("/data/tmp/su")
            }
        }
    })
}

回归到真正报错的地方,这里提示是MainActivity报错仔细审阅代码之后,发现onCreate方法有个除0错误:

显然我们进了这个IF语句,查看rb.j方法,发现这个方法应该是在Java层检查root或者其他问题:

这里我通过Hook rb.j直接返回false

function Avoid_divide_by_zero(){
    let b = Java.use("b.a.a.b");
    b["j"].implementation = function () {
        return false
    };
}

完整Hook脚本如下:

function hook_C_open(){
    var open_address = Module.findExportByName("libc.so","open")
    Interceptor.attach(open_address,{
        onEnter:function(args){
            this.open_args = args[0]
            if(this.open_args.readCString().search("su") != -1){
                this.open_args.writeUtf8String("/data/tmp/su")
            }
        }
    })
}

function hook_C_snprintf(){
    var snprintf_address = Module.findExportByName("libc.so","snprintf")
    Interceptor.attach(snprintf_address,{
        onEnter: function(args){
            this.snprintf_args = args[0]
        },
        onLeave: function(retval){
            if(this.snprintf_args.readCString().search("status") != -1){
                this.snprintf_args.writeUtf8String("/data/tmp/status")
            }
        }
    })
}

function Avoid_divide_by_zero(){
    let b = Java.use("b.a.a.b");
    b["j"].implementation = function () {
        return false
    };
}

Java.perform(function(){
    hook_C_snprintf();
    console.log("[+] Hook Open function - All path related 'status' replaced to /data/tmp/status - Done")
    hook_C_open();
    console.log("[+] Hook Open function - All path related 'su' replaced to /data/tmp/su - Done")
    Avoid_divide_by_zero();
    console.log("[+] Avoid onCreate Function to divide by zero - Done")
})

运行之后发现APP可以正常使用了:


Part2需要找到正确的key和pin码,这里贴一下WP链接,有兴趣可以研究一下, 我这里就不继续搞了。
r2-pay: whitebox (part 2)

版权属于:Ebounce 所有,采用《知识共享署名许可协议》进行许可,转载请注明文章来源。

本文链接: https://www.ebounce.cn/archives/84/

下一篇
没有了

评论区(1条评论)

我要评论


老学弟
LV1

前面把root检测原理搞清楚后,如果想要跳过root检测,可以使用magisk hide,或者shamiko模块,或者kernal su定制一下,感觉这些apk检测应该比国内银行那些app检测要松一些