利用owasp学习安卓安全- Challenges Write Up
需求工具
题目List地址: MAS-Crackmes
- Jadx-反编译Jar包或者APK
- adb-直接安装命令行工具或者AndroidStudio都行
- Frida-Hook工具
- Magisk-刷Root用工具
- 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
按钮会直接闪退,但是不点击页面会被挂起,这里透过对话框,我们可以看出是需要输入一串字符串进行验证,从而到达下一步。
阅读源码
源码非常简单,整体结构如下:
这里摘录一些比较关键的源码:
sg.vantagepoint.a.a.a
是个AES解密函数:
很简单的函数,接受两个字节流,然后进行AES加密。
MainActivity
这里是APP启动时的主要逻辑:
第一个框图,表示代码运行到这里时会弹出一个对话框,点击
ok
按钮后调用system.exit(0)
随后程序退出;第二个框图,表示这里进行了root
检测;第三个框图是验证的代码,根据打印字符串,可以看出sg.vantagepoint.uncrackable1.a.a
是验证的代码,我们需要传值让这个函数通过校验。- 根据上面
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
架构所以这里用了x64
的lib
,首先查看函数表:
显然这就是我们要找的函数了:
由于菜的如我,读不太懂x64汇编,这里使用无敌的F5
反汇编,得到IDA
猜测的C源码:
这里代码还是很简单的,上面是预定义了一些需要使用的变量,其中有个strcpy
函数,熟悉C
的同学知道,这个字符串拷贝到某个变量里面,虽然有一段很长的内容,这里猜测就是从Java那边接收获得的值,最后利用strncmp
函数比较接受来的值和v6
是否相同,相同返回0
,不相同返回1
,最后取反,然后赋值result
。
此时这个思路就很清晰了,我们绕过root
检测的退出机制,输入copy
的v6
值即可通关。
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
里面分为三块:
verifylib()
如果不知道具体这个函数是干什么的,我们可以通过阅读
adb logcat
来观察,这里从源码来看是进行了输出的我们注意到,这里出现了
tamper Detected
,同时这里打印源码中的log信息,我们可以看出来这里应该是将libxxx.so
移动到指定位置。xor_key
,这个变量名提示很明显了,显然本题是使用了异或进行编码,先记下这个值:verify()
,其实就是通关的逻辑代码,我们看看这里调用了什么函数
跟着源码的指引查看check.check_code
函数:
又是一个
native
函数了,是时候使用ida
了
Native函数
这里使用x64
的libfoo.so
,主要是x86
汇编熟悉一点(虽然最后直接用了反汇编功能),先看函数表,找到我们的bar函数:
然后使用反汇编,看看源码逻辑:
其实这里IDA的反汇编有点非常难读但是我们可以提取一些信息出来
- 第一个框图我们发现,前面将
v7
进行了初始化操作,但是经过一个函数之后,v7
参与了后面的异或运算,很显然sub_12c0
对v7
进行了赋值操作 框图2和框图3需要仔细分析一下,我们挨个分析里面出现的值
v7
这个变量我们根据sub_12c0
函数进行分析,发现应该是存在内存中的常量这个值我们只能通过动态读取内存来获得。
v5
熟悉x86汇编的朋友,看到寄存器rcx
,就会知道这是个下标,rcx
通常是一个counter
,常用在循环里面进行计数.dest
这个值不认识,但是IDA分析出这是个char[]
,阅读init
函数时,发现dest
在这里进行了赋值,这里又出现了一个
v4
,我们回到apk
源码中:
答案揭晓了,dest
是xor_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 }
- 这里还有一个值得注意的函数
这里明显检查了
frida
和xposed
,根据之前的adb
日志可以知道,我们就是因为触发了这一步才被exit
的,后面的goodbye
函数就是exit(0)
分析到这里,要解出这个secret
我们需要下面几步:
- 绕过
native
层的frida
或者xposed
校验 - 取得内存中的
v7
- 利用
xor_key
和v7
进行异或,得到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导致代码出错
}
有一些小细节需要注意,
- 因为之前源码分析中给
v7
赋值的函数不是导出函数,所以无法使用findExportByName
, 这个可以在IDA上check - 由于
findBaseAddress
这个函数只是寻找.so
的基址,如果我们需要访问到函数,需要加上函数的偏移量,才能让指针指向正确的函数,对于x64来说,这个函数的偏移量为,然后因为我用的是
arm64-v8a
的实机,所以偏移量为通过具体环境根据IDA来判断具体偏移量。
- 读几个字节是由长度来判断的,因为这里
secret
长度为24,所以我们只需要读24个字节。 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
,而gmain
是frida
hook时产生的进程,因此这里主要是检测frida
,从这一条我们就可以知道是如何检测frida
的了,该Native
库会读主要进程下面所有子进程的status
,如果检测到frida
产生的进程,则直接退出。
所以我们可以利用frida
在snprintf
入参的时候,对参数进行修改,保证我们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)
《讲台深处》剧情片高清在线免费观看:https://www.jgz518.com/xingkong/124286.html
你的文章充满了智慧,让人敬佩。 http://www.55baobei.com/ZUqb3Ene4e.html
你的文章充满了智慧,让人敬佩。 http://www.55baobei.com/ZUqb3Ene4e.html
每次看到你的文章,我都觉得时间过得好快。 https://www.4006400989.com/qyvideo/1451.html
你的才华让人惊叹,请继续保持。 http://www.55baobei.com/wsEDr0ikwo.html
你的文章内容非常精彩,让人回味无穷。 http://www.55baobei.com/XMgNV3od8x.html
看的我热血沸腾啊https://www.ea55.com/
看的我热血沸腾啊https://www.237fa.com/
前面把root检测原理搞清楚后,如果想要跳过root检测,可以使用magisk hide,或者shamiko模块,或者kernal su定制一下,感觉这些apk检测应该比国内银行那些app检测要松一些