题目
Android-UnCrackable-Level4 v0.9 r2pay-v0.9.apk
Android-UnCrackable-Level4-v1.0 r2pay-v1.0.apk
Android-UnCrackable-L4-(r2pay)https://mas.owasp.org/crackmes/Android/#android-uncrackable-l4
题目大概是要求我们让app在正常运行.
如果输入正确的pin,那么返回的r2coin是绿色的,而不是红色
但是题目的pin是一个hash函数进行校验.
所以pin好像不可以解密, 于是我们就让app正常运行,同时可以修改流程返回一个绿色的r2coin
另外,题目说了, Android-UnCrackable-L1~L4 可以白盒分析
黑盒分析了很久,没结果,能力有限
于是在理解源代码的基础上再次进行破解分析
同时官方也给了源代码.
源代码https://github.com/OWASP/mas-crackmes
Android-UnCrackable-L4-1.0-(r2pay-v1.0) 没有源码但是v1.0貌似并没有做过多的该多, 所有可以参照v0.9的源码去分析v1.0
解析
方法一: 修改文件
v0.9
1), apk进行了很多混淆,有java层的,也有so层的
so层的ollvm是真恶心, 巨多无比, 感觉用deflat.py应该解决不了,也没去尝试
此刻我们遇到了第一道难关:混淆
java层的混淆如何解决? 我通常的做法是理解并重命名
so层的混淆,我真的是难崩.🤣
2), java层, 题目有2处地方调用了检测, 涉及的检测的方法和手段很多很多,
第一处 re.pwnme.MainActivity.onCreate
//
@Override // a.a.k.d, a.h.a.d, a.e.d.c, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
//...
b rb = new b(getApplicationContext());
if (rb.j() || (rb.a() && rb.e())) {
int i = 1337 / 0;
this.f1833a = (byte) (this.f1833a | 15);
}
//...
g();
}
第二处
re.pwnme.MainActivity.MainActivity$a
if (rb.j() || (rb.a() && rb.e())) {
MainActivity mainActivity = MainActivity.this;
mainActivity.f1833a = (byte) (mainActivity.f1833a | 15);
NullPointerException np = new NullPointerException();
np.notify();
}
此刻我们我们遇到了第二道难关 : 该死的 b.a.a.b;
分析一下这个检测, 发现它比较的独立, 不是那么分散.
解决办法
a), Xposed/Firda hook rb.j() || (rb.a() && rb.e())
b), 删除对应的检测, 因为该检测高内聚,低耦合
于是我很懒的采用了方法b直接 nop掉这些检测, 删除smali
3), 于是java层的防护就解除了, 我们该去解决so层的防护
so层的防护来自libnative-lib.so 它被混淆得很厉害, 反正我很难受
阅读ida的函数以及反汇编,没一点头绪.连so层怎么检测我们的都不知道
可以很悲惨的理解为, 我们连敌人长什么样子都不知道, 就被敌人把我们给kill掉了
通过阅读源码,发现 so层的检测有点多
于是我们遇到了第三道难关: so层众多检测
通过分析他的检测, 我们发现一下特点
a). 检测好像好像很多,不管多还是不多, 都没有做出类似于exit的调用
b). 当so检测到不正常的行为时,没有对原来的流程进行修改,甚至关键数据也没有修改,只是调用int crash(int rand) 去触发异常,然后apk进程崩溃退出.
c). native 层的c代码,很多函数都用的 static inline , 这就导致我们无法hook crash函数,
__attribute__((always_inline))
static inline int crash(int randomval) {
volatile int *p = gpCrash;
p += randomval * 2;
p += *p + randomval;
p = 0;
p += *p;
return *p;
}
那该怎么解决?
如何去Hook int crash(int randVar)?
遇到问题,我们就该思考了,而不是一味的无头苍蝇乱撞了.
因为这是我经常犯下的错,遇到困难,总是拿着一个突如其来的想法去尝试, 感觉可以成功. 却没去停下来思考, 然后再决定如何行动. 当然如果要做到这样, 对于我来说,也很难很难
我们遇到了什么问题?
a), 很多检测手段
b), 因为混淆的原因,这些检测手段我们找不到
c), 因为ollvm, 很多函数无法被IDA F5
d), 字符串也无法提供信息给我们
e). 导入表导出表,好像也没什么值得注意的信息
这些我们值得去思考的
太懒了, 没有做过的的思考.
我们为什么要hook crash()函数?
因为很多检测手段都会调用crash()
为什么我们无法hook crash()?
因为它变成了inline function
为什么变成了inline function就hook不到了?
因为它已经不再是函数了,而是一个函数里面的一段二进制代码
作为一段代码. 分散布局在了很多地方
那该怎么办? 可以考虑放弃int crash(int )
我还是很想对crash()开刀, 因为它太关键了, 因为它,导致了so层的检测手段生效. 如果没有它,so层的检测手段就完全失效.
于是我该去找到每个地方的crash,然后给他nop掉. 这是我想到的办法.
怎么找到crash函数? 应该找到他的特征! 通过特征去定位关键位置, 甚至需要我们写ida脚本
有哪些特征?
a), 他的参数有特征, 都是2字节,
crash(0x207A);
crash(0x34ED);
crash(0x7A20);
crash(0x9218);
...
通过这个特征,我们可以找到可疑的地方
通过下面这个IDA pyton脚本,我们可以找到几处
import idaapi
import idautils
import idc
def search_for_immediate_value(value):
results = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
disasm_line = idc.generate_disasm_line(head, 0)
if f"#0x{value:X}" in disasm_line:
results.append((head, disasm_line))
return results
def main():
immediate_value = 0x8190
results = search_for_immediate_value(immediate_value)
for ea, disasm in results:
print(f"0x{ea:X}: {disasm}")
if __name__ == "__main__":
main()
找到一处后, 通过分析发现,它并没有被ollvm特别的改动.
同时分析汇编,发现它和原函数非常的吻合
我们尝试通过分析汇编,发现它具体的一些特征
特征1:(不是很有说服力) 他的第一句是 mov w8开头
特征2: 他会调用全局变量,#dword_163008@PAGE
特征3: 末尾结束会有这么一句汇编 LDR W8, [X9]
通过特征2,我们可以尝试找找, 运行以下 ida python脚本
import idaapi
import idautils
import idc
def search_for_string_in_code(string):
results = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
disasm_line = idc.generate_disasm_line(head, 0)
if string == disasm_line:
results.append((head, disasm_line))
return results
def main():
search_string = "ADRP X9, #dword_163008@PAGE"
results = search_for_string_in_code(search_string)
cnt=0
for ea, disasm in results:
cnt=cnt+1
print(f"[{cnt:02}]:Address 0x{ea:X}: {disasm}")
if __name__ == "__main__":
main()
我们就可以找到21处
通过1个特征,显然有误差,于是结合3个特征,再去寻找
运行以下 ida python脚本
import idaapi
import idautils
import idc
def get_code_addresses():
addresses = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
addresses.append(head)
return addresses
def get_disassembly(ea):
return idc.generate_disasm_line(ea, 0)
def get_crash_area():
cnt=0
idx_movw8=0
idx_LDRW8X9=0
arr_address = get_code_addresses()
for i in range(2, len(arr_address)-40): # 从第3个指令开始,确保可以向上检查两条指令
st_asm1 = get_disassembly(arr_address[i])
if "ADRP X9, #dword_163008@PAGE" == st_asm1:
idx_movw8=i-2
st_asm2 = get_disassembly(arr_address[idx_movw8])
if "MOV W8" in st_asm2:
#cnt=cnt+1
#print(f"[{cnt}]Address 0x{arr_address[idx_movw8]:X}: {st_asm2}")
idx_LDRW8X9=idx_movw8+24
for j in range(10):
st_asm3 = get_disassembly(arr_address[idx_LDRW8X9+j])
if "LDR W8, [X9]"==st_asm3:
cnt=cnt+1
print(f"[{cnt:02}]==> {arr_address[idx_movw8]:X}~{arr_address[idx_LDRW8X9+j]:X}")
break
def main():
get_crash_area()
if __name__ == "__main__":
main()
我们还是得到了21处,和之前得到的一样,同时我们还得到了crash inline的范围
得到范围后,我们就可以把他们给nop掉了
运行以下 ida python脚本可nop
import idaapi
import idautils
import idc
def nop_instruction(ea):
# 获取指令长度
length = idc.get_item_size(ea)
# ARM64 的 NOP 指令是 4 字节
nop_bytes = b'\x1F\x20\x03\xD5' * (length // 4)
# 将地址处的指令替换为 NOP
idaapi.patch_bytes(ea, nop_bytes)
#print(f"Address 0x{ea:X}: NOPed {length} bytes")
def get_code_addresses():
addresses = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
addresses.append(head)
return addresses
def get_disassembly(ea):
return idc.generate_disasm_line(ea, 0)
def get_crash_area():
cnt=0
idx_movw8=0
idx_LDRW8X9=0
arr_address = get_code_addresses()
for i in range(2, len(arr_address)-40): # 从第3个指令开始,确保可以向上检查两条指令
st_asm1 = get_disassembly(arr_address[i])
if "ADRP X9, #dword_163008@PAGE" == st_asm1:
idx_movw8=i-2
st_asm2 = get_disassembly(arr_address[idx_movw8])
if "MOV W8" in st_asm2:
#cnt=cnt+1
#print(f"[{cnt}]Address 0x{arr_address[idx_movw8]:X}: {st_asm2}")
idx_LDRW8X9=idx_movw8+24
for j in range(10):
st_asm3 = get_disassembly(arr_address[idx_LDRW8X9+j])
if "LDR W8, [X9]"==st_asm3:
cnt=cnt+1
print(f"[{cnt:02}]==> {arr_address[idx_movw8]:X}~{arr_address[idx_LDRW8X9+j]:X}")
idx_tmp=idx_movw8
while idx_tmp<=(idx_LDRW8X9+j):
nop_instruction(arr_address[idx_tmp])
idx_tmp=idx_tmp+1
break
def main():
get_crash_area()
if __name__ == "__main__":
main()
nop后,注意保存一下文件.
4), javc层的检测破除, so层的检测破除
但是我们输入 pin和account还是红色,因为我们不知道具体pin
于是我们遇到了最后一个问题: 无论怎么输入,返回都是红色
所有我们就强制把错误的pin也显示为绿色的,这样顺眼一点
涉及的类是color, 去re.pwnme.MainActivity.MainActivity$a搜索color
然后 修改为 if-ne --> if-eq
if-eq v6, v8, :cond_3
.line 96
invoke-virtual {v1, v4}, Landroid/widget/TextView;->setTextColor(I)V
最后打包apk,签名apk,安装apk
v1.0
因为v1.0和v0.9很像, 于是拿着v0.9的一些数据,尝试去分析v1.0
1), 把apk拖入jadx,
就java层的混淆来说, 比v0.9层级提高了, 但也不是很影响
然后同样是 java层的检测手段, 我们可以查看 C0282 类被引用了多少次, 发现和v0.9 一模一样
,于是我的做法就是nop掉,删除smali代码.
2), 发现java层的检测手段, 0.9和1.0 差别不是很大
然后我把v0.9和v1.0的libnative-lib.so进行了一个对比分析
我先从导入表函数进行了分析.利用到了工具 radare2
发现如下差异
//共同的
mprotect
pthread_create
isdigit
open
printf
__cxa_finalize
calloc
__progname
closedir
snprintf
sscanf
gmtime_r
rand
__stack_chk_fail
nanosleep
opendir
readdir
malloc
memcpy
free
__system_property_get
__cxa_atexit
isspace
//v0.9特有的
strcmp
memset
//v1.0特有的
signal
lstat
tan
getpid
fork
strstr
exit
发现v1.0 有比较危险的函数 exit, fork, getpid.
这让我联想到了ptracr双进程反调试, 在Level2~Level3有所应用
但v1.0好像差一个ptrace函数
总之我们需要稍微注意一下 fork, exit,signal.
当然除了导入表的函数, 开发者也可以自定义api去代替libc.so的api
3), 既然v1.0和v0.9类似
那么 int crash(int randValue) 应该也是有的
先用 c1.py进行一个检测, 检测依据来自v0.9
import idaapi
import idautils
import idc
def search_for_immediate_value(value):
results = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
disasm_line = idc.generate_disasm_line(head, 0)
if f"#0x{value:X}" in disasm_line:
results.append((head, disasm_line))
return results
def main():
immediate_value = 0x9218
results = search_for_immediate_value(immediate_value)
for ea, disasm in results:
print(f"0x{ea:X}: {disasm}")
if __name__ == "__main__":
main()
'''
0x9E7F4: MOV W11, #0x9218
0x9E940: MOV W11, #0x9218
0xEEC20: MOV W11, #0x9218
0xEED1C: MOV W11, #0x9218
这4个地方,貌似都是crash
'''
发现有4个地方,都有crash(0x9218)
去其中一个地方看看, 并翻译一下arm汇编
[A]18D28 MOV W11, #0x34A5
[A]18D2C STUR W11, [lp_var1] ;var1 = 0x34A5
[A]18D30 ADRP X10, #dword_1B4008@PAGE
[A]18D34 LDR W11, [X10,#dword_1B4008@PAGEOFF] //
[A]18D38 MOV W10, W11
[A]18D3C STUR X10, [lp_gVar]
[A]18D40 LDUR W11, [lp_var1]
[A]18D44 LSL W11, W11, #1 ; w11=var1*2
[A]18D48 LDUR X10, [lp_gVar]
[A]18D4C ADD X10, X10, W11,SXTW#2 ; X10 = gVar + var*8
[A]18D50 STUR X10, [lp_gVar] ; gVar= gVar + var*8
[A]18D54 LDUR X10, [lp_gVar] ; X10 = gVar + var*8
[A]18D58 LDR W11, [X10] ; W11 = *(gVar + var*8) Exception_1
[A]18D5C LDUR W12, [lp_var1] ; w12=var1
[A]18D60 MOV W13, WZR ; W13 = 0
[A]18D64 SUBS W11, W13, W11 ; W11 = 0 - *(gVar + var*8)
[A]18D68 MOV W14, #0xCF5D1FB6 ; W14 = 0xCF5D1FB6
[A]18D70 SUBS W11, W14, W11 ; W11 = 0xCF5D1FB6 + *(gVar + var*8)
[A]18D74 SUBS W12, W13, W12 ; W12 = 0 - var1
[A]18D78 SUBS W11, W11, W12 ; W11 = 0xCF5D1FB6 + *(gVar + var*8) +var1
[A]18D7C MOV W12, #0xDA65FB6F ; W12 = r2
[A]18D84 SUBS W11, W12, W11 ; W11 = r2 - (r1 + *(gVar + var*8) +var1)
[A]18D88 MOV W12, #0xB08DBB9 ; W12 = r3
[A]18D90 SUBS W11, W12, W11 ; W11 = r3 - r2 + (r1 + *(gVar + var*8) +var1)
[A]18D94 LDUR X8, [lp_gVar]
[A]18D98 ADD X8, X8, W11,SXTW#2 ; X8 = gVar + (r3 - r2 + (r1 + *(gVar + var*8) +var1))*4
[A]18D9C STUR X8, [lp_gVar] ; gvar= gVar + (r3 - r2 + (r1 + *(gVar + var*8) +var1))*4
[A]18DA0 MOV X8, XZR ; X8 = 0
[A]18DA4 STUR X8, [lp_gVar] ; gVar=0
[A]18DA8 LDUR X8, [lp_gVar]
[A]18DAC LDRSW X8, [X8] ; X8 =*0 Exception_2
[A]18DB0 LDUR X10, [lp_gVar] ; X10 = 0
[A]18DB4 ADD X8, X10, X8,LSL#2 ; X8 = 0 + *0 x4
[A]18DB8 STUR X8, [lp_gVar] ; gVar= 0 + *0 x4
[A]18DBC LDUR X8, [lp_gVar] ;
[A]18DC0 LDR W11, [X8] ; W11 = *(0 + *0 x4) Exception_3
[A]18DC4 STR W11, [X19,#0x1CD4] ; var3=w11
[A]18DC8 ADRP X8, #x.91_ptr@PAGE
[A]18DCC LDR X8, [X8,#x.91_ptr@PAGEOFF]
[A]18DD0 LDR W11, [X8]
[A]18DD4 ADRP X8, #y.92_ptr@PAGE
[A]18DD8 LDR X8, [X8,#y.92_ptr@PAGEOFF]
[A]18DDC LDR W12, [X8]
[A]18DE0 MOV W13, #0x3293E9F2
[A]18DE8 SUBS W13, W13, W11
[A]18DEC MOV W14, #0x3293E9F1
[A]18DF4 SUBS W13, W14, W13
[A]18DF8 MUL W11, W11, W13
[A]18DFC ANDS W11, W11, #1
[A]18E00 CSET W13, EQ
[A]18E04 SUBS W14, W12, #0xA
发现和v0.9的crash有一点区别, 那就是多触发了一个异常.
v0.9的crash可以触发2个, v1.0 可以触发3个
同时,我们分析多处 v1.0的crash函数特征,提取出来下面3个特征
a), 开头是 MOV W11
b), 会访问全局变量 ADRP X10, #dword_1B4008@PAGE
c), 末尾是
STUR X8, [lp_gVar]
LDUR X8, [lp_gVar]
LDR W11, [X8]
通过这3个特征, 我们可以再次寻找v1.0的crash函数
通过特征b 查找所有地方 c2.py
import idaapi
import idautils
import idc
def search_for_string_in_code(string):
results = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
disasm_line = idc.generate_disasm_line(head, 0)
if string == disasm_line:
results.append((head, disasm_line))
return results
def main():
search_string = "ADRP X10, #dword_1B4008@PAGE"
results = search_for_string_in_code(search_string)
cnt=0
for ea, disasm in results:
cnt=cnt+1
print(f"[{cnt:02}]:Address 0x{ea:X}: {disasm}")
if __name__ == "__main__":
main()
'''
[01]:Address 0x18D30: ADRP X10, #dword_1B4008@PAGE
[02]:Address 0x18EC8: ADRP X10, #dword_1B4008@PAGE
[03]:Address 0x192B8: ADRP X10, #dword_1B4008@PAGE
...
[84]:Address 0xEAD4C: ADRP X10, #dword_1B4008@PAGE
[85]:Address 0xEEC28: ADRP X10, #dword_1B4008@PAGE
[86]:Address 0xEED24: ADRP X10, #dword_1B4008@PAGE
一共86处
'''
发现了差不多86个地方, 初步确定为86
然后我们通过3个特征一起检测 ,c3.py
import idaapi
import idautils
import idc
def nop_instruction(ea):
# 获取指令长度
length = idc.get_item_size(ea)
# ARM64 的 NOP 指令是 4 字节
nop_bytes = b'\x1F\x20\x03\xD5' * (length // 4)
# 将地址处的指令替换为 NOP
idaapi.patch_bytes(ea, nop_bytes)
#print(f"Address 0x{ea:X}: NOPed {length} bytes")
def get_code_addresses():
addresses = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
addresses.append(head)
return addresses
def get_disassembly(ea):
return idc.generate_disasm_line(ea, 0)
def get_crash_area():
cnt=0
idx_movw8=0
idx_LDRW8X9=0
arr_address = get_code_addresses()
for i in range(2, len(arr_address)-40): # 从第3个指令开始,确保可以向上检查两条指令
st_asm1 = get_disassembly(arr_address[i])
if "ADRP X10, #dword_1B4008@PAGE" == st_asm1:
idx_movw8=i-2
st_asm2 = get_disassembly(arr_address[idx_movw8])
if "MOV W11" in st_asm2:
#cnt=cnt+1
#print(f"[{cnt}]Address 0x{arr_address[idx_movw8]:X}: {st_asm2}")
idx_LDRW8X9=idx_movw8+25
for j in range(20):
st_asm3 = get_disassembly(arr_address[idx_LDRW8X9+j])
st_asm4 = get_disassembly(arr_address[idx_LDRW8X9+j-1])
st_asm5 = get_disassembly(arr_address[idx_LDRW8X9+j-2])
if "LDR W11, [X8]"==st_asm3 and "LDUR" in st_asm4 and "STUR" in st_asm5:
#cnt=cnt+1
len_code=arr_address[idx_LDRW8X9+j]- arr_address[idx_movw8]
print(f"[{cnt:02}]==> {arr_address[idx_movw8]:X}~{arr_address[idx_LDRW8X9+j]:X} len {len_code}")
#idx_tmp=idx_movw8
#while idx_tmp<=(idx_LDRW8X9+j):
#nop_instruction(arr_address[idx_tmp])
#idx_tmp=idx_tmp+1
break
def main():
get_crash_area()
if __name__ == "__main__":
main()
好像检测到了86处,于是我们尝试把他nop掉,运行c4.py
import idaapi
import idautils
import idc
def nop_instruction(ea):
# 获取指令长度
length = idc.get_item_size(ea)
# ARM64 的 NOP 指令是 4 字节
nop_bytes = b'\x1F\x20\x03\xD5' * (length // 4)
# 将地址处的指令替换为 NOP
idaapi.patch_bytes(ea, nop_bytes)
#print(f"Address 0x{ea:X}: NOPed {length} bytes")
def get_code_addresses():
addresses = []
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg.type == idaapi.SEG_CODE:
for head in idautils.Heads(seg.start_ea, seg.end_ea):
if idc.is_code(idc.get_full_flags(head)):
addresses.append(head)
return addresses
def get_disassembly(ea):
return idc.generate_disasm_line(ea, 0)
def get_crash_area():
cnt=0
idx_movw8=0
idx_LDRW8X9=0
arr_address = get_code_addresses()
for i in range(2, len(arr_address)-40): # 从第3个指令开始,确保可以向上检查两条指令
st_asm1 = get_disassembly(arr_address[i])
if "ADRP X10, #dword_1B4008@PAGE" == st_asm1:
idx_movw8=i-2
st_asm2 = get_disassembly(arr_address[idx_movw8])
if "MOV W11" in st_asm2:
#cnt=cnt+1
#print(f"[{cnt}]Address 0x{arr_address[idx_movw8]:X}: {st_asm2}")
idx_LDRW8X9=idx_movw8+25
for j in range(20):
st_asm3 = get_disassembly(arr_address[idx_LDRW8X9+j])
st_asm4 = get_disassembly(arr_address[idx_LDRW8X9+j-1])
st_asm5 = get_disassembly(arr_address[idx_LDRW8X9+j-2])
if "LDR W11, [X8]"==st_asm3 and "LDUR" in st_asm4 and "STUR" in st_asm5:
cnt=cnt+1
len_code=arr_address[idx_LDRW8X9+j]- arr_address[idx_movw8]
print(f"[{cnt:02}]==> {arr_address[idx_movw8]:X}~{arr_address[idx_LDRW8X9+j]:X} len {len_code}")
idx_tmp=idx_movw8
while idx_tmp<=(idx_LDRW8X9+j):
nop_instruction(arr_address[idx_tmp])
idx_tmp=idx_tmp+1
break
def main():
get_crash_area()
if __name__ == "__main__":
main()
然后保存一下so文件,
4), 依照v0.9的步骤, 把那个绿色 红色的逻辑修改一下, 输入错误返回绿色好看点
6), 打包apk,签名apk,安装apk, 尝试在root的手机上运行一下
没啥大问题, 就这样吧..
另外v1.0的r2pay有点问题, 那就是我 输入ps -A | grep "re.pwnme"
找不到该进程, 只能在 top | grep "re.pwnme" 中找到, 真奇怪
方法2: frida
v0.9
java层的检测,就不说了. 我还是很想把它nop掉的😁😁
这里说说so层的检测.
这里权当是复现一下这2篇文章
Owasp level 4 Android Reversing Anti-Debugging/Root checks: r2pay 1.0.https://medium.com/@ndsetobol/owasp-level-4-android-reversing-anti-debugging-root-checks-r2pay-1-0-239e224ec649利用owasp学习安卓安全- (Level 1 - Level 4)Challenges Write Up
https://www.ebounce.cn/archives/84/
首先利用radare2查看so引入了哪些api
┌──(kali㉿Kali)-[~/code/file/apk]
└─$ r2 -A UnCrackable-Level4-v0.9.AAA/lib/arm64-v8a/libnative-lib.so
[0x000052b0]> ii
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――
5 0x00005130 GLOBAL FUNC mprotect
13 0x00005140 GLOBAL FUNC pthread_create
18 0x00005150 GLOBAL FUNC isdigit
27 0x00005160 GLOBAL FUNC open
31 0x00005170 GLOBAL FUNC printf
39 0x00005180 GLOBAL FUNC __cxa_finalize
58 0x00005190 GLOBAL FUNC calloc
75 ---------- GLOBAL OBJ __progname
78 0x000051a0 GLOBAL FUNC closedir
100 0x000051b0 GLOBAL FUNC snprintf
106 0x000051c0 GLOBAL FUNC sscanf
121 0x000051d0 GLOBAL FUNC gmtime_r
123 0x000051e0 GLOBAL FUNC rand
144 0x000051f0 GLOBAL FUNC __stack_chk_fail
166 0x00005200 GLOBAL FUNC nanosleep
172 0x00005210 GLOBAL FUNC opendir
193 0x00005220 GLOBAL FUNC strcmp
222 0x00005230 GLOBAL FUNC readdir
242 0x00005240 GLOBAL FUNC malloc
245 0x00005250 GLOBAL FUNC memcpy
250 0x00005260 GLOBAL FUNC memset
262 0x00005270 GLOBAL FUNC free
263 0x00005280 GLOBAL FUNC __system_property_get
268 0x00005290 GLOBAL FUNC __cxa_atexit
269 0x000052a0 GLOBAL FUNC isspace
这些api中,我们可以关注一些,并 hook他们,打印一下参数的值.
strcmp hook 情况: 我自己没发现什么特殊的数据, 反倒是strcmp一大堆调用
open hook 情况: 有对su文件的访问, 这个东西我们要拒绝
snprintf hook 情况: 有对自身子进程/proc/self/task/{PID}/status的访问, 这个后面说, 这个也要拒绝
opendir hook 情况: 一直在被调用, 应该是后台线程一直在跑, 有对自身进程的访问 /proc/self/task ,这个后面说, 也要拒绝
readdir hook 情况: 一直在被调用, 应该是后台线程一直在跑, 但没发现什么特别的数据
sscanf hook 情况: 没发现什么特别的数据
如果我们在运行下面这个frida脚本的情况下
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())
}
})
}
function hook_C_opendir(){
var snprintf_address = Module.findExportByName("libc.so","opendir")
Interceptor.attach(snprintf_address,{
onEnter: function(args){
this.arg1 = args[0]
console.log("[+] opendir ",this.arg1 .readCString())
}
})
}
Java.perform(function(){
//hook_C_open();
hook_C_opendir();
hook_C_snprintf();
})
看输出结果
λ frida -U -f re.pwnme -l asset\f2.js
____
/ _ | Frida 16.3.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to PBCM10 (id=1d518c72)
Spawned `re.pwnme`. Resuming main thread!
[PBCM10::re.pwnme ]-> [+] Snprintf parameter: 50000
[+] opendir /proc/self/task
[+] Snprintf parameter: /proc/self/task/7066/status
[+] Snprintf parameter: /proc/self/task/7076/status
[+] Snprintf parameter: /proc/self/task/7077/status
[+] Snprintf parameter: /proc/self/task/7078/status
[+] Snprintf parameter: /proc/self/task/7079/status
[+] Snprintf parameter: /proc/self/task/7080/status
[+] Snprintf parameter: /proc/self/task/7081/status
[+] Snprintf parameter: /proc/self/task/7082/status
[+] Snprintf parameter: /proc/self/task/7084/status
[+] Snprintf parameter: /proc/self/task/7085/status
[+] Snprintf parameter: /proc/self/task/7086/status
[+] Snprintf parameter: /proc/self/task/7087/status
[+] Snprintf parameter: /proc/self/task/7088/status
[+] Snprintf parameter: 0
[+] Snprintf parameter: 0
[+] Snprintf parameter: 0
[+] Snprintf parameter: 0
[+] opendir /odm/lib64/hw/
[+] opendir /vendor/lib64/hw/
Process crashed: Bad access due to invalid address
发现它访问了自身进程信息 /proc/self/task
读取了 /proc/self/task/{PID}/status
然后就结束了
多运行几次, 我们也会发现结果如出一辙.
我们可以去查看一下最后一个子进程是什么, 可能与什么检测有关
启动apk进程时,要pause, 因为我们要去看PID
λ frida -U -f re.pwnme -l asset\f1.js --pause
____
/ _ | Frida 16.3.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to PBCM10 (id=1d518c72)
Spawned `re.pwnme`. Use %resume to let the main thread start executing!
[PBCM10::re.pwnme ]->
然后
PBCM10:/ $ ps -A | grep "re.pwnme"
u0_a246 27678 1805 5744660 37140 0 0 S re.pwnme
所以, 当前进程是PID=27678
然后查看 /proc/27678/task
,
PBCM10:/ $ cd /proc/27678/task
PBCM10:/proc/27678/task $ cat ./*/status | awk "/(Name:)|(^Pid:)/"
Name: re.pwnme
Pid: 27678
Name: Jit thread pool
Pid: 27688
Name: Runtime worker
Pid: 27689
Name: Runtime worker
Pid: 27690
Name: Runtime worker
Pid: 27691
Name: Runtime worker
Pid: 27692
Name: Signal Catcher
Pid: 27693
Name: ADB-JDWP Connec
Pid: 27694
Name: HeapTaskDaemon
Pid: 27696
Name: ReferenceQueueD
Pid: 27697
Name: FinalizerDaemon
Pid: 27698
Name: FinalizerWatchd
Pid: 27699
Name: gmain
Pid: 27700
Name: gdbus
Pid: 27701
Name: Thread-3
Pid: 27702
Name: re.pwnme
Pid: 27704
注意观察我们的gmain进程, pid是 27700, 它好像是frida注入到目标进程的进程
然后,我们继续恢复apk进程的执行
[PBCM10::re.pwnme ]-> %resume
[PBCM10::re.pwnme ]-> [+] Open ==> /proc/self/cmdline
[+] Open ==> /data/app/re.pwnme-zTbqjxGxX-j6lOJvkpbGsg==/base.apk
[+] Snprintf(arg0,) ==>: 50000
[+] Open ==> /proc/28008/timerslack_ns
[+] Open ==> /data/vendor/gpu/esx_config_re.pwnme.txt
[+] Open ==> /data/vendor/gpu/esx_config.txt
[+] Open ==> /data/misc/gpu/esx_config_re.pwnme.txt
[+] Open ==> /data/misc/gpu/esx_config.txt
[+] Open ==> /data/app/re.pwnme-zTbqjxGxX-j6lOJvkpbGsg==/lib/arm64/libnative-lib.so
[+] Snprintf(arg0,) ==>: /proc/self/task/27678/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27688/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27689/status
[+] Open ==> /data/local/su
[+] Open ==> /data/local/bin/su
[+] Open ==> /data/local/xbin/su
[+] Open ==> /sbin/su
[+] Snprintf(arg0,) ==>: /proc/self/task/27690/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27691/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27692/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27693/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27694/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27696/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27697/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27698/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27699/status
[+] Snprintf(arg0,) ==>: /proc/self/task/27700/status
[+] Snprintf(arg0,) ==>: 50000
[+] Open ==> /proc/28015/timerslack_ns
[+] Open ==> /proc/cpuinfo
[+] Snprintf(arg0,) ==>: 0
[+] Snprintf(arg0,) ==>: 0
[+] Snprintf(arg0,) ==>: 0
[+] Snprintf(arg0,) ==>: 0
Process crashed: Bad access due to invalid address
***
Process name is com.google.process.gservices, not key_process
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'OPPO/PBCM10/PBCM10:10/QKQ1.191224.003/1615196842:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2024-06-28 16:58:17+0800
pid: 27678, tid: 28012, name: re.pwnme >>> com.google.process.gservices <<<
uid: 10246
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xfabb11cd
x0 0000007e057fb710 x1 000000008a3bda98 x2 0000000000000000 x3 0000000000000000
x4 00000000ffffffff x5 0000000000000000 x6 0000007e057fbd50 x7 0000000000ffd888
x8 00000000000067c8 x9 00000000fabb11cd x10 0000000085b34305 x11 0000000000000001
x12 000000000739dade x13 00000000fffffff6 x14 000000000739dade x15 0000000000000000
x16 0000000000000001 x17 0000007eef9f240c x18 0000007dfd440000 x19 0000007e057fb710
x20 0000007ef135f6fc x21 0000007e057fbd50 x22 0000007e057fc060 x23 0000007e057fbdd8
x24 0000007e057fbd50 x25 0000007e057fbd50 x26 0000007e057fc020 x27 0000007ef465d020
x28 0000007e05c0f4dc x29 0000007e057fbcf0
sp 0000007e057fb710 lr 0000007eef9f240c pc 0000007e05c406cc
backtrace:
#00 pc 000000000007b6cc /data/app/re.pwnme-zTbqjxGxX-j6lOJvkpbGsg==/lib/arm64/libnative-lib.so (BuildId: f87b3bd9fcae36e63939958f412d03a42e0ce406)
#01 pc 00000000000d9720 /apex/com.android.runtime/lib64/bionic/libc.so!libc.so (offset 0xd9000) (__pthread_start(void*)+36) (BuildId: 20c7dfeb41468016772e288818fb55e7)
#02 pc 0000000000075f2c /apex/com.android.runtime/lib64/bionic/libc.so!libc.so (offset 0x75000) (__start_thread+64) (BuildId: 20c7dfeb41468016772e288818fb55e7)
***
[PBCM10::re.pwnme ]->
Thank you for using Frida!
进程最后停止在了 PID=27700的gmain进程, 这样我们就可以猜测 so 对我们的frida进行了一个检测,
如何绕过? 不让其访问,或者错误的访问, /proc/self/task或者/proc/self/task/{PID}/status
这样就读取不到我们的frida寄生在目标进程的rida子进程
我采取的做法是 不让它访问 /proc/self/task frida代码如下
// open中有对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]
console.log("[+] Open parameter: "+ this.open_args.readCString())
}
})
}
// sprintf 中,有对/proc/self/task/[PID]/status的访问
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())
}
})
}
//strcmp 没发现什么特殊的数据
function hook_C_strcmp(){
var snprintf_address = Module.findExportByName("libc.so","strcmp")
Interceptor.attach(snprintf_address,{
onEnter: function(args){
this.snprintf_args = args[0]
this.snprintf_args2 = args[1]
},
onLeave: function(retval){
//console.log("[+] Snprintf parameter: " + this.snprintf_args.readCString())
if(this.snprintf_args2.readCString().search("gmain") != -1 || this.snprintf_args2.readCString().search("frida") != -1){
//this.snprintf_args.writeUtf8String("fake_status_path")
//console.log("[+] Snprintf changed parameter: " + this.snprintf_args.readCString())
//console.log("[+] status check ...");
console.log("[+] strcmp ",this.snprintf_args2.readCString())
}
}
})
}
// opendir有对/proc/self/task的访问
function hook_C_opendir(){
var snprintf_address = Module.findExportByName("libc.so","opendir")
Interceptor.attach(snprintf_address,{
onEnter: function(args){
this.arg1 = args[0]
console.log("[+] opendir ",this.arg1 .readCString())
}
})
}
//readdir 没发现什么特别的数据, readdir("H"),readdir("I"),readdir(".")
function hook_C_readdir(){
var snprintf_address = Module.findExportByName("libc.so","readdir")
Interceptor.attach(snprintf_address,{
onEnter: function(args){
this.arg1 = args[0]
console.log("[+] readdir ",this.arg1 .readCString())
}
})
}
// action =======================================================================================
function doing_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("fake_status_path")
//console.log("[+] Snprintf changed parameter: " + this.snprintf_args.readCString())
//console.log("[+] status check ...");
}
}
})
}
function doing_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("fake_su_path")
}
}
})
}
function doing_hook_C_opendir(){
var snprintf_address = Module.findExportByName("libc.so","opendir")
Interceptor.attach(snprintf_address,{
onEnter: function(args){
this.arg1 = args[0]
if(this.arg1.readCString().search("/proc/self/task") != -1){
this.arg1.writeUtf8String("fake_task_path")
//console.log("[+] Snprintf changed parameter: " + this.snprintf_args.readCString())
}
}
})
}
Java.perform(function(){
//hook_C_open();
//hook_C_snprintf();
//hook_C_strcmp();
//hook_C_opendir();
//hook_C_readdir();
//==action
//doing_hook_C_snprintf();
doing_hook_C_open();
doing_hook_C_opendir();
})
ps: 代码中已经对su文件的访问做出了措施
hook了task和su的访问, 我们的apk就可以正常访问了
λ frida -U -f re.pwnme -l asset\f1.js
____
/ _ | Frida 16.3.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to PBCM10 (id=1d518c72)
Spawned `re.pwnme`. Resuming main thread!
[PBCM10::re.pwnme ]->
result
v1.0
同样可以使用v0.9的frida脚本
v1.0多出来的api, 貌似也没什么奇怪的字符串参数
答案
见解析