Android-UnCrackable-L4-(r2pay)

题目

Android-UnCrackable-Level4 v0.9 r2pay-v0.9.apk

Android-UnCrackable-Level4-v1.0 r2pay-v1.0.apk

Android-UnCrackable-L4-(r2pay)icon-default.png?t=N7T8https://mas.owasp.org/crackmes/Android/#android-uncrackable-l4

题目大概是要求我们让app在正常运行.

如果输入正确的pin,那么返回的r2coin是绿色的,而不是红色

但是题目的pin是一个hash函数进行校验.

所以pin好像不可以解密, 于是我们就让app正常运行,同时可以修改流程返回一个绿色的r2coin

另外,题目说了, Android-UnCrackable-L1~L4 可以白盒分析

黑盒分析了很久,没结果,能力有限

于是在理解源代码的基础上再次进行破解分析

同时官方也给了源代码. 

源代码icon-default.png?t=N7T8https://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.icon-default.png?t=N7T8https://medium.com/@ndsetobol/owasp-level-4-android-reversing-anti-debugging-root-checks-r2pay-1-0-239e224ec649利用owasp学习安卓安全- (Level 1 - Level 4)Challenges Write Upicon-default.png?t=N7T8https://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, 貌似也没什么奇怪的字符串参数

答案

见解析

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值