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_as
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值