OWASP 实战分析 level 3
2023-12-5 18:11:22 Author: 看雪学苑(查看原文) 阅读量:6 收藏

这篇文章详细介绍了解决OWASP人员(Bernhard Mueller)发布的Androidcrackme程序(owasp uncrackable)的几种方法。

题目链接

链接:https://pan.baidu.com/s/1VJ7Y3psWoSi5NnlOpaohEw
提取码:1234

owasp uncrackable 安全机制

owasp uncrackable能够找到的安全机制:

1.Java反调试

2.Java完整性校验(CRC)

3.java Root检测

4.Native层的反调试

使用到的工具

Java层的反编译工具(Dalvik bytecode):

◆Jadx-gui.

◆JEB.

So层反编译程序:

◆IDA Pro

动态二进制检测框架:

◆Frida.

编译工具

◆vscode

题目难度变大。

3.1. JAVA层

apk丢到JADX-GUI查看MainActivity界面。

简化一下界面如下:

/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
private static final String TAG = "UnCrackable3";
static int tampered = 0;
private static final String xorkey = "pizzapizzapizzapizzapizz";
private native long baz();
private native void init(byte[] bArr);

/* JADX INFO: Access modifiers changed from: private */
public void showDialog(String str) {
}

private void verifyLibs() {
}

/* JADX INFO: Access modifiers changed from: protected */
/* JADX WARN: Type inference failed for: r0v2, types: [sg.vantagepoint.uncrackable3.MainActivity$2] */
@Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.SupportActivity, android.app.Activity
public void onCreate(Bundle bundle) {
verifyLibs();
init(xorkey.getBytes());
super.onCreate(bundle);
setContentView(owasp.mstg.uncrackable3.R.layout.activity_main);
}

public void verify(View view) {
String obj = ((EditText) findViewById(owasp.mstg.uncrackable3.R.id.edit_text)).getText().toString();
AlertDialog create = new AlertDialog.Builder(this).create();
if (this.check.check_code(obj)) {
create.setTitle("Success!");
create.setMessage("This is the correct secret.");
} else {
create.setTitle("Nope...");
create.setMessage("That's not it. Try again.");
}
}

static {
System.loadLibrary("foo");
}
}

得到的信息:

1.两个常量"pizzapizzapizzapizzapizz""UnCrackable3"
2.两个native层的调用bazinit(通过JNI调用会把字节数组"pizzapizzapizzapizzapizz",发送到native层)
3.CRC校验(注意,没有加密对app进行签名)

由于CRC校验和比较弱,我们也许能通过修改反编译得到的函数和native层中的函数,直接绕过所有安全的检测。

三个检测root函数来检测设备是否可能存在root权限。

checkRoot1()检查文件系统中是否存在含有SU内容的二进制文件来获取权限。

checkRoot2()检查 的BUILD标记。检测Android版本是开发者还是官方。

checkRoot3()检查是否存在有root权限的apk。

3.2. Native层

unzip xxx.apk -d fileso(文件夹名),进行解压。

丢到ida进行静态分析,结合jadx,会发现so层有个init函数对字符数组(由private static final String xorkey = "pizzapizzapizzapizzapizz"字符串得到字节数组)进行处理。

ida里面搜索Init。

反编译一下。

看到这里的思路是,能不能用动态ida调试查看一下,不过我们继续静态分析。

这里介绍一下JNI调用Java方法的传参逻辑:

1.第一个参数一般是JNIEnv指针,它是一个指向JNI环境的指针,用于调用JNI函数。
2.第二个参数一般是Java对象或者Java类的引用(一般写作jClass),用于指定要调用的Java方法所属的对象或类。
3.第二个参数一般是Java对象或者Java类的引用,用于指定要调用的Java方法所属的对象或类或者参数。

frida脚本演示可能更加直观一些。

function main() {
Java.perform(function(){
// 下面代码指定了要Hook的文件函数名和So文件名
Interceptor.attach(Module.findExportByName('libfridaso.so','Java_com_example_fridaso_FridaSoDefine_FridaSo'),{
onEnter: function(args){ //顾名思义,OnEnter就是我们进入改函数前的方法,args是传入的第一个参数,一般so层函数第一个参数都是JniENv,第二个参数是jClass,从第三个参数开始才是我们传入Java层的第三个参数
send('Hook Start');
send('args[2] ==>' + args[2]); //打印我们JAVA层传入的第一个参数
send('arhs[3]==>' +args[3]); //打印我们JAVA层传入的第二个参数
},
// reval就是返回的值
onLeave:function(reval){
send("return:" +reval);
// 切割
console.log(reval);
reval.replace(0);
console.log(reval);

}
})
})

根据上面的知识,我们在Java_sg_vantagepoint_uncrackable3_MainActivity_init(init),对传入参数的名字进行修改(选中参数,快捷键n)。

因为传入的a3是我们在jadx看到的字节数组(pizzapizzapizzapizzapizz)

所以result肯定和前面的字节数组(pizzapizzapizzapizzapizz)有关(具体情况目前不明朗)。


思路中断不妨查看一下启动时的so时候的ELF文件,该部分执行程序启动时候的函数指针(JNI)。

其中的.init_array里面sub_31B0的引起了注意,点击进入。

反编译

大概意思是sub_30D0创建了一个线程,线程调用了sub_30D0来里面是否有fridaxpose检测。

绕过这个函数可以通过hookpthread_create来阻止进程的创建来进行绕过也可以通过hooksub_30D0进行绕过。

在这里选择后者。

这里先贴一下相关知识点:

linker64 是 Android 64 位系统中的动态链接器,它负责加载和链接共享库,以及解析符号引用,使得程序能够正确执行。在 Android 系统中,linker64 是一个重要的组件,它在应用程序启动时被调用,负责加载应用程序所需的共享库,并建立共享库之间的依赖关系。

linker64 的主要功能包括:

◆加载共享库:根据程序指定的共享库路径,加载共享库到内存中。

◆符号解析:解析程序中对共享库的符号引用,找到符号在内存中的地址,以便正确执行程序。

◆重定位:对共享库中的重定位表进行处理,将符号引用重定位到正确的地址。

◆初始化:执行共享库中的 init_array 段,调用初始化函数进行必要的初始化操作。

◆启动程序:最后,linker64 调用程序的入口函数(一般是 main 函数),启动应用程序的执行。

在 Android 64 位系统中,init_array 一般会被 linker64 中的 call_array 函数调用。call_array 函数负责遍历共享库中的 init_array 段,并依次执行其中的函数指针所指向的初始化函数。它会在动态链接器加载共享库时被调用,确保在程序启动时执行这些初始化函数。

由于反调试函数(sub_30D0)启动线程是在init_array中,而so层的init_array都是被linker64中的call_array调用,所以我们直接去Hooklinker64 call_array,在这之前需要动态调试so获得linker64 call_array的地址。

3.2.1. 动态调试so获取linker64.so call_array的函数地址

3.2.1.1. 使用adb命令打开手机上的android_server64

1.搜索自己IDA下面的android_server64。

2.把android_server64使用adb push命令发送到自己的手机文件夹 /data/local/tmp下面。

3.启动android_server64。

3.2.1.2. 使用WINDOWS端的IDA连接手机端

1.打开ida,不要new新工程,直接go进去,选择Remote ARM Linux/Android debugger。

2.操作如下:

点击OK。

弹出一个列表框,search搜素app的包名。

3.2.1.3. 下面会有个坑

我们在右边的Module板块搜索我们在IDA之前找到的重要call或者so的关键字,会发现有时候根本搜索不到。

因为没有F9加载。

原因如下:

ida pro附加成功之后,会先调到其他so,比如libc.so这些,这个时候,需要先f9跳过,等没得跳之后,才需要执行jdb。

F9加载就行

搜索Module:linker64。

Module:linker64下搜索call_array。

记录一下call_array地址0000007D68B58764减去linker64的基地址(7D68B38000) =00020764(偏移地址)。

开始编写fridaHook脚本。

        

// 获取linker64模块的基地址
var linker64_module = Module.getBaseAddress("linker64");
//使用拦截器附加linker64模块的偏移地址
// 7D68B58764 - 7D68B38000 = 0x20764
Interceptor.attach(linker64_module.add(0x20764),{
// 进入函数,代码检查参数args[3]指向的字符串是否匹配libfoo.s
onEnter:function(args) {
if(args[3].readCString().match("libfoo.so")) {
// 获取libfoo.so的基地址
var libfoo_module = Module.findBaseAddress('libfoo.so');
console.log("获取libfoo.so的基地址==>"+libfoo_module)
Interceptor.replace(libfoo_module.add(0x30D0),new NativeCallback(function(){
return;
},'void',[]));
}
},onLeave:function(result){}
})

运行之后发现仍然存在检测,继续往下分析native层的init函数。

1.调用sub_323C(JNIenv, JClass);

2.保存字符串"pizzapizzapizzapizzapizz"

我们查看sub_323C的调用次数以便把握Hook时机(选中函数后按下x查看引用次数)。

只调用了一次,由于sub_323C只在init函数中,init函数在apk启动(oncreate)才被调用,所以我们选择在libfool.so加载的时候进行Hooklibfoo.soapkjava层调用System.loadLibrary("foo")进行加载,调用的底层逻辑是通过libandroid_runtime.soandroid_dlopen_ext来加载的so。

具体的调用逻辑如下:

1.应用程序通过JNI接口调用libandroid_runtime.so中的android_dlopen_ext函数。

2.android_dlopen_ext函数接收一个参数,即libfoo.so的路径。它会根据路径加载libfoo.so动态链接库。

3.在加载libfoo.so之前,android_dlopen_ext函数会检查是否已经加载过该库。如果已经加载过,则直接返回之前加载的库的句柄。

4.如果libfoo.so还没有被加载过,则android_dlopen_ext函数会通过系统调用打开libfoo.so文件,并获取到动态链接库的句柄。

5.android_dlopen_ext函数会将libfoo.so的句柄保存在一个全局的缓存中,以便在后续的调用中能够直接使用。

6.android_dlopen_ext函数返回libfoo.so的句柄给应用程序。

7.应用程序可以通过句柄来调用libfoo.so中的函数,实现底层逻辑。

需要注意的是,android_dlopen_ext函数是Android Runtime中的一个函数,用于加载动态链接库。它是通过JNI接口提供给应用程序使用的。在调用android_dlopen_ext函数之前,应用程序需要先加载libandroid_runtime.so,并通过JNI接口导入android_dlopen_ext函数。这样才能在Java层调用android_dlopen_ext函数,并实现加载libfoo.so的功能。

根据这些信息, 继续编写Frida脚本。

    

Java.perform(function(){
// 获取linker64模块的基地址
var linker64_module = Module.getBaseAddress("linker64");
//使用拦截器附加linker64模块的偏移地址
// 7D68B58764 - 7D68B38000 = 0x20764
Interceptor.attach(linker64_module.add(0x20764),{
// 进入函数,代码检查参数args[3]指向的字符串是否匹配libfoo.s
onEnter:function(args) {
if(args[3].readCString().match("libfoo.so")) {
// 获取libfoo.so的基地址
var libfoo_module = Module.findBaseAddress('libfoo.so');
console.log("获取libfoo.so的基地址==>"+libfoo_module)
Interceptor.replace(libfoo_module.add(0x30D0),new NativeCallback(function(){
return;
},'void',[]));
}
},onLeave:function(result){}
})

// hook android_dlopen_ext
var libfoo_loaded_flag = 0; //定义一个变量用于标记是否已加载libfoo.so
// 获取libandroid_runtime.so中的android_dlopen_ext函数的地址
var android_dlopen_ext_addr = Module.getExportByName("libandroid_runtime.so", "android_dlopen_ext");
// 拦截android_dlopen_ext函数
Interceptor.attach(android_dlopen_ext_addr, {
// 进入android_dlopen_ext函数时执行的操作
onEnter:function(args){
// 判断传入的参数中是否包含"libfoo.so"的字符串
if(-1 != args[0].readCString().indexOf("libfoo.so")){
// 如果包含,则将libfoo_loaded_flag标记为1,表示已加载libfoo.so
libfoo_loaded_flag = 1;
}
},
// 离开android_dlopen_ext函数时执行的操作
onLeave:function(result){
// 如果libfoo_loaded_flag为1,表示已加载libfoo.so
if(libfoo_loaded_flag == 1){
// 通过Interceptor.replace方法替换libfoo.so中偏移为0x323C的函数
var libfoo_module = Module.findBaseAddress("libfoo.so");
Interceptor.replace(libfoo_module.add(0x323C), new NativeCallback(function(){

console.log("获取反调试地址sub_323c==>"+libfoo_module)
return;
}, 'void', []));
libfoo_loaded_flag = 0;
}
}
})
// var RootDetection = Java.use("sg.vantagepoint.util.RootDetection");
// RootDetection["checkRoot1"].implementation = function () {
// console.log('checkRoot1 is called');
// var ret = this.checkRoot1();
// console.log('checkRoot1 ret value is ' + ret);
// return false;
// };

}
)}
// 立即开始执行函数
setImmediate(main);

3.2.1.4. 回到JAVA层

开始过java层的root检测。

直接选择checkRoot1右键获取Frida脚本。

添加到我们的之前的脚本上,返回false。

    

Java.perform(function(){
// 获取linker64模块的基地址
var linker64_module = Module.getBaseAddress("linker64");
//使用拦截器附加linker64模块的偏移地址
// 7D68B58764 - 7D68B38000 = 0x20764
Interceptor.attach(linker64_module.add(0x20764),{
// 进入函数,代码检查参数args[3]指向的字符串是否匹配libfoo.s
onEnter:function(args) {
if(args[3].readCString().match("libfoo.so")) {
// 获取libfoo.so的基地址
var libfoo_module = Module.findBaseAddress('libfoo.so');
console.log("获取libfoo.so的基地址==>"+libfoo_module)
Interceptor.replace(libfoo_module.add(0x30D0),new NativeCallback(function(){
return;
},'void',[]));
}
},onLeave:function(result){}
})

// hook android_dlopen_ext
var libfoo_loaded_flag = 0; //定义一个变量用于标记是否已加载libfoo.so
// 获取libandroid_runtime.so中的android_dlopen_ext函数的地址
var android_dlopen_ext_addr = Module.getExportByName("libandroid_runtime.so", "android_dlopen_ext");
// 拦截android_dlopen_ext函数
Interceptor.attach(android_dlopen_ext_addr, {
// 进入android_dlopen_ext函数时执行的操作
onEnter:function(args){
// 判断传入的参数中是否包含"libfoo.so"的字符串
if(-1 != args[0].readCString().indexOf("libfoo.so")){
// 如果包含,则将libfoo_loaded_flag标记为1,表示已加载libfoo.so
libfoo_loaded_flag = 1;
}
},
// 离开android_dlopen_ext函数时执行的操作
onLeave:function(result){
// 如果libfoo_loaded_flag为1,表示已加载libfoo.so
if(libfoo_loaded_flag == 1){
// 通过Interceptor.replace方法替换libfoo.so中偏移为0x323C的函数
var libfoo_module = Module.findBaseAddress("libfoo.so");
Interceptor.replace(libfoo_module.add(0x323C), new NativeCallback(function(){

console.log("获取反调试地址sub_323c==>"+libfoo_module)
return;
}, 'void', []));
libfoo_loaded_flag = 0;
}
}
})
var RootDetection = Java.use("sg.vantagepoint.util.RootDetection");
RootDetection["checkRoot1"].implementation = function () {
console.log('checkRoot1 is called');
var ret = this.checkRoot1();
console.log('checkRoot1 ret value is ' + ret);
return false;
};

}
)}
// 立即开始执行函数
setImmediate(main);

3.2.1.5. 再次来到Native看bar判断函数进行代码分析

简单概括一下,bar函数通过生成key1和字符串pizzapizzapizzapizzapizz进行异或来获取新的字符串,我们的思路就是hook生成key1函数(v6 = sub_10E0(v9);)得到key1,再和"pizzapizzapizzapizzapizz"进行异或。

完整脚本如下:

// hook linker64's call_array
function main() {
Java.perform(function(){
// 获取linker64模块的基地址
var linker64_module = Module.getBaseAddress("linker64");
//使用拦截器附加linker64模块的偏移地址
// 7D68B58764 - 7D68B38000 = 0x20764
Interceptor.attach(linker64_module.add(0x20764),{
// 进入函数,代码检查参数args[3]指向的字符串是否匹配libfoo.s
onEnter:function(args) {
if(args[3].readCString().match("libfoo.so")) {
// 获取libfoo.so的基地址
var libfoo_module = Module.findBaseAddress('libfoo.so');
console.log("获取libfoo.so的基地址==>"+libfoo_module)
Interceptor.replace(libfoo_module.add(0x30D0),new NativeCallback(function(){
return;
},'void',[]));
}
},onLeave:function(result){}
})

// hook android_dlopen_ext
var libfoo_loaded_flag = 0;
var android_dlopen_ext_addr = Module.getExportByName("libandroid_runtime.so", "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext_addr, {
onEnter:function(args){
if(-1 != args[0].readCString().indexOf("libfoo.so")){
libfoo_loaded_flag = 1;
}
},onLeave:function(result){
if(libfoo_loaded_flag == 1){
// hook libfoo.so + 0x323C , pass call ptrace
var libfoo_module = Module.findBaseAddress("libfoo.so");
Interceptor.replace(libfoo_module.add(0x323C), new NativeCallback(function(){
console.log("获取反调试地址sub_323c==>"+libfoo_module)
return;
}, 'void', []));
libfoo_loaded_flag = 0;
}
}
})
var RootDetection = Java.use("sg.vantagepoint.util.RootDetection");
RootDetection["checkRoot1"].implementation = function () {
console.log('checkRoot1 is called');
var ret = this.checkRoot1();
console.log('checkRoot1 ret value is ' + ret);
return false;
};

// hook异或
var str_xor_key1 = 0;
//获取hook so的基地址
var target_module= Module.findBaseAddress('libfoo.so');

//用拦截器 获取libfoo.so的函数sub10E0(v8)偏移地址
if(target_module){
console.log("start read libfoo.so")
Interceptor.attach(target_module.add(0x10E0),{
onEnter:function(args) {
// v8处理前
str_xor_key1 = args[0];
},
// v8处理后
onLeave:function(result) {
//readByteArray返回的是ArrayBuffer,转化成JS类型
console.log('\n',str_xor_key1.readByteArray(0x18));
var key1_bytes = new Uint8Array(str_xor_key1.readByteArray(0x18))
var key2 = "pizzapizzapizzapizzapizz";
var flag = '';
// 开始异或
for(var i=0;i<0x18;i++){
flag = flag+ String.fromCharCode(key2.charCodeAt(i) ^ key1_bytes[i]);
}
console.log("flag:",flag);
console.log("flag:",flag);

}
})
} else{
console.log("can't read libfoo.so")
}

}
)}

// 立即开始执行函数
setImmediate(main);

分析结束。

总结:

1.对于分析APK程序来说动静态结合必不可少。

2.Native层从ELF初始化字符串到so文件的root检测都不可忽略。

3.只要分析正确,Frida几乎可以帮我们绕过所有的安全检查。

4.和Dalvik代码不同,Native层的代码更难分析,也代表更有挑战性。

看雪ID:4Chan

https://bbs.kanxue.com/user-home-940967.htm

*本文为看雪论坛优秀文章,由 4Chan 原创,转载请注明来自看雪社区

# 往期推荐

1、2023 SDC 议题回顾 | 芯片安全和无线电安全底层渗透技术

2、SWPUCTF 2021 新生赛-老鼠走迷宫

3、OWASP 实战分析 level 1

4、【远控木马】银狐组织最新木马样本-分析

5、自研Unidbg trace工具实战ollvm反混淆

6、2023 SDC 议题回顾 | 深入 Android 可信应用漏洞挖掘

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458530209&idx=1&sn=b49ba510e20801dc01f948fe3b8fcd94&chksm=b18d012b86fa883d5a95f19186a426674e98a9ea778ab110173e8d8414417e92389234863fce&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh