题目链接
链接: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. owasp level3
题目难度变大。
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");
}
}
得到的信息:
"pizzapizzapizzapizzapizz"
和"UnCrackable3"
native
层的调用baz
和init
(通过JNI调用会把字节数组”pizzapizzapizzapizzapizz
“,发送到native
层)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
方法的传参逻辑:
JNIEnv
指针,它是一个指向JNI
环境的指针,用于调用JNI函数。Java
对象或者Java
类的引用(一般写作jClass),用于指定要调用的Java方法所属的对象或类。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
来里面是否有frida
和xpose
检测。
绕过这个函数可以通过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
(偏移地址)。
开始编写frida
的Hook
脚本。
// 获取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
加载的时候进行Hook
,libfoo.so
是apk
在java
层调用System.loadLibrary("foo")
进行加载,调用的底层逻辑是通过libandroid_runtime.so
的android_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
# 往期推荐
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):OWASP 实战分析 level 3