首页
论坛
课程
招聘
[逆向分析] [混淆加固] [HOOK注入] [原创]使用LIEF打造类似王者X耀的静态代码注入
2021-3-31 16:45 6578

[逆向分析] [混淆加固] [HOOK注入] [原创]使用LIEF打造类似王者X耀的静态代码注入

2021-3-31 16:45
6578

使用lief工具写一个类似于腾讯so保护这样一个东西。本篇记录lief工具的使用和TX加固的原理,不会对ELF文件格式进行记录。

一、工具介绍

首先,lief是一个文件解析工具,可以解析ELF、PE、DEX等,用途比较广泛。

 

github 地址:https://github.com/lief-project/LIEF

 

我使用的是python版,在使用过程中经常出现毛病,所以并不建议使用python版。最好是不怕麻烦自己写一个,就可以避免很多不稳定导致的fix代码。

二、TX SO加密介绍

首先,王者荣耀采用的是il2cpp,比较关键的so有3个,libil2cpp.so、libGameCore.so、libtprt.so。其中libtprt.so为保护so。

 

王者荣耀加载被保护so(il2cpp或GameCore)时,会优先加载链接库。所以,整个流程unity启动后,会优先加载libtprt.so。

 

图1

 

libtprt.so对自身的tptext节进行了加密,在加载过程中执行init_array进行解密,跟踪mprotect,可以看到解密代码:

 

 

 

使用lief快速解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tprt = lief.parse(name)
tptext_section = tprt.get_section(".tptext").content
 
print(len(tptext_section))
offset = tprt.get_section(".tptext").offset
out = b""
for i in range(len(tptext_section)):
    out += (tptext_section[i] ^ 0xb8).to_bytes(1,byteorder='little')
 
 
print(len(out))
tpp = tprt_bin[:offset]+out+tprt_bin[offset+len(tptext_section):]
with open("tpp.so", 'wb') as fp:
    fp.write(tpp)

 

tptext节里的函数主要用于初始化各类保护,会被jni_onload调用。现在tprt已经被成功加载,现在就需要解密libil2cpp.so

 

由于libGameCore.so小一点,分析起来方便,所以后面就不分析il2cpp了。先观察ELF结构,可以发现它的LOAD段格外多。

 

个人分析他比原生多了3个LOAD段,新增的第一个LOAD段(04),里面是.dynamic 和 .init_array。很好理解,因为强行添加了一个依赖库和init_array,所以需要生成新的.dynamic,至于init_array个人猜测是添加字符串混淆之类的东西。

 

 

新增的第二个LOAD段(05),是新的.rel.plt,用ida分析,可以明显看出有增加项,且增加项在新增的第三个LOAD段(06):

 

 

跳转到0x228cfcc,可以看到这是一个got表,并且它的plt表在新增的第二个LOAD段(05)。

 

 

 

 

对添加的LOAD段有一定了解了,再看一下修改后的.dynamic有什么变化:

 

 

需要关注的,我已经添加了红色方框。大部分只是修改偏移,指向新的节(JMPREL、INIT_ARRAY),值得注意的是DT_INIT,这个是init段,比init_array更早执行。更适合用来解密。

 

 

通过取off_1cc的地址 - off_1cc的值(地址为base+0x1cc,值是自己设置的,为1cc)所以算出来是当前so的基址,然后调用sub_2289b58并且传入基址。

 

sub_2289b58就是解密函数,网上对它的分析很多了,总的就是调用g_tprt_pfn_array(“.text”,base,3)对当前的text段进行解密。(“.text”这个字符串,在新增的第三个LOAD(06)中,意味着是写死的)

 

 

所以总结一下:

 

MTP对SO新增了3个LOAD段,第一个LOAD新增.dynamic、.init_array,第二个LOAD段是映射新的.rel.plt、.plt、.text节,第三个LOAD段用来映射.got、.data节。

 

MTP在program header 之后添加了一段汇编,在init时执行,获取当前SO的基址,传入解密函数,并调用libtprt.so的导出函数g_tprt_pfn_array进行text节解密。

三、代码实现

由于一次写完大概率会很乱思路不够清晰,所以分几步写,方便理解,也好记录。

步骤一:添加INIT段,并调用任意函数

首先,生成一个SO,很简单,有一个PrintLog函数没有调用,所以LOG只有一条:

 

 

 

执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""
  添加init段,并且调用ADD_FUNC_NAME并传入一个参数(当前So的基址)
"""
INIT_PROC_SIZE = 0x2c
INIT_PROC_CONTENT = [0xC0,0x46,0xFF,0xB5,0x83,0xB0,0x6D,0x46,0x00,0xA3,0x14,0x3B,0x19,0x1C,0x1F,0x68,0xC9,0x1B,0x29,0x60,0x5E,0x68,0x03,0xD0,0x76,0x18,0x6E,0x60,0x28,0x68,0xB0,0x47,0x03,0xB0,0xFF,0xBD]# 腾讯代码里扣出来的嘿嘿
 
def add_init_proc():
 
    binary = lief.parse(TARGET_BIN)
    # 检测.dynamic节的空位是否足够,如果小于3个就要拓展dynamic节内容
    free_dynamic_entry = 0
    for entry in binary.dynamic_entries:
        if entry.tag == lief.ELF.DYNAMIC_TAGS.NULL:
            free_dynamic_entry += 1
    print("free dynamic entry num:", free_dynamic_entry)
    assert free_dynamic_entry > 3
 
    #获取位于segment header 后的 offset,用于添加init_proc
    init_proc_offset = binary.header.program_header_offset + \
                       binary.header.program_header_size * (binary.header.numberof_segments + 2 )
    print("init_proc offset:", hex(init_proc_offset))
 
 
    # 添加init入口
    if not binary.has(ELF.DYNAMIC_TAGS.INIT):
        # 先用0占位,直接写入偏移,lief工具会有点问题
        # 如果不出问题 binary.add(ELF.DynamicEntry(ELF.DYNAMIC_TAGS.INIT,init_proc_offset + 8))
        binary.add(ELF.DynamicEntry(ELF.DYNAMIC_TAGS.INIT,0))
 
    else:
        init_entry = binary.get(ELF.DYNAMIC_TAGS.INIT)
        print("[x] binary has init_proc:", init_entry)
        exit(1)
 
    binary.write("libnative-lib.so")
 
    # 手动修复 DynamicEntry 中的 value
    outbin = lief.parse("libnative-lib.so")
    out_dynamic = outbin.get_section(".dynamic")
    ADD_FUNC = outbin.get_symbol(ADD_FUNC_NAME).value
 
    num = 0
    for entry in outbin.dynamic_entries:
        if entry.tag == ELF.DYNAMIC_TAGS.INIT:
            break
        num+=1
 
    init_entry_offset =  out_dynamic.offset + (num * out_dynamic.entry_size)
    print(hex(init_entry_offset))
 
    patch_file("libnative-lib.so",init_entry_offset+4,struct.pack("<I", init_proc_offset + 8 + 1))
 
 
    global INIT_PROC_CONTENT
    #前四位 为 init——proc 的 偏移 ,后四位 为 要调用的 函数地址
    print("init_proc_offset :",hex(init_proc_offset))
    print("ADD_FUNC :", hex(ADD_FUNC))
 
    INIT_PROC_CONTENTS = list(struct.pack("<I", init_proc_offset)) + list(struct.pack("<I", ADD_FUNC)) + INIT_PROC_CONTENT
    patch_file("libnative-lib.so",init_proc_offset,INIT_PROC_CONTENTS)

 

 

可以看到函数PrintLog优先于JNI函数执行,并且传入的参数为基址。

步骤二:向so中添加可执行代码

关于重定位

 

需要记住的几个总结:

 

rel.plt ->got -> plt -> extern表(导入函数)

 

rel.plt ->got -> text(导出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//ELF.h 查看rel.plt的格式,r_offset 指向got表的地址,r_info高8位为类型,后24位为
 
// Relocation entry, without explicit addend.
 struct Elf32_Rel {
   Elf32_Addr r_offset; // Location (file byte offset, or program virtual addr)
   Elf32_Word r_info;   // Symbol table index and type of relocation to apply
 
   // These accessors and mutators correspond to the ELF32_R_SYM, ELF32_R_TYPE,
   // and ELF32_R_INFO macros defined in the ELF specification:
   Elf32_Word getSymbol() const { return (r_info >> 8); }
   unsigned char getType() const { return (unsigned char)(r_info & 0x0ff); }
   void setSymbol(Elf32_Word s) { setSymbolAndType(s, getType()); }
   void setType(unsigned char t) { setSymbolAndType(getSymbol(), t); }
   void setSymbolAndType(Elf32_Word s, unsigned char t) {
     r_info = (s << 8) + t;
   }
 };
 
 
 // Symbol table entries for ELF32.
 struct Elf32_Sym {
   Elf32_Word st_name;     // Symbol name (index into string table)
   Elf32_Addr st_value;    // Value or address associated with the symbol
   Elf32_Word st_size;     // Size of the symbol
   unsigned char st_info;  // Symbol's type and binding attributes
   unsigned char st_other; // Must be zero; reserved
   Elf32_Half st_shndx;    // Which section (header table index) it's defined in
 
   // These accessors and mutators correspond to the ELF32_ST_BIND,
   // ELF32_ST_TYPE, and ELF32_ST_INFO macros defined in the ELF specification:
   unsigned char getBinding() const { return st_info >> 4; }
   unsigned char getType() const { return st_info & 0x0f; }
   void setBinding(unsigned char b) { setBindingAndType(b, getType()); }
   void setType(unsigned char t) { setBindingAndType(getBinding(), t); }
   void setBindingAndType(unsigned char b, unsigned char t) {
     st_info = (b << 4) + (t & 0x0f);
   }
 };

编译一个so,里面的示范代码:

 

 

十分简单,就是一个打印log,将编译好的so拖入ida,可以看到,虽然我只写了一个函数,实际上so运行时还需要许多其他的函数。

 

 

执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def add_symbol():
    binary = lief.parse(TARGET_BIN)
    add_bin = lief.parse("libadd.so")
    add_got = add_bin.get_section(".got")
    add_data = add_bin.get_section(".data")
    add_plt = add_bin.get_section(".plt")
    add_text = add_bin.get_section(".text")
 
 
 
    before_add_load_num = 0
    for i in binary.segments:
        if i.type == ELF.SEGMENT_TYPES.LOAD:
            before_add_load_num+=1
    print(before_add_load_num)
 
 
    """
    添加2个load段,用于将add中内容添加进target
    """
    # 第一个可读可执行,用来映射新的.plt节 + .text节
    add_RE_seg = lief.ELF.Segment()
    add_RE_seg.alignment = 0x1000
    add_RE_seg.type = ELF.SEGMENT_TYPES.LOAD
    add_RE_seg.add(ELF.SEGMENT_FLAGS.X)
    add_RE_seg.add(ELF.SEGMENT_FLAGS.R)
    add_RE_seg.content = add_plt.content + add_text.content
    print("add_RE_seg.content :", add_RE_seg.content)
    binary.add(add_RE_seg)
 
 
    #第二个load段 可读可写 添加 .got .data
    add_RW_seg = lief.ELF.Segment()
    add_RW_seg.alignment = 0x1000
    add_RW_seg.type = ELF.SEGMENT_TYPES.LOAD
    add_RW_seg.add(ELF.SEGMENT_FLAGS.W)
    add_RW_seg.add(ELF.SEGMENT_FLAGS.R)
    add_RW_seg.content = add_got.content + add_data.content
    binary.add(add_RW_seg)
    print(add_got.content)
 
 
 
    addbin_relplt = add_bin.pltgot_relocations
 
    """
        添加addbin中的relplt,并且向dynsym添加对应的symbol。
        此操作改动了dynstr、dynsym、rel.plt。
        本来针对原节拓展即可,但lief工具新增了3个load段进行加载新的内容,所以还修改了dynamic
 
    """
    add_sym_value = list()
    for add_entry in addbin_relplt:
        if binary.has_symbol(add_entry.symbol.name):
            sym = binary.get_symbol(add_entry.symbol.name)
        else:
            sym = binary.add_dynamic_symbol(add_entry.symbol)#后面修复
            add_sym_value.append(add_entry.symbol.value - add_text.virtual_address)
            print(hex(add_entry.symbol.value),hex(add_entry.symbol.value - add_text.virtual_address))
 
            # 此处lief工具又有问题,写入后value变了,坑爹货
            #工具不出问题,此处减掉add中text的虚拟地址,加上intermediate中的新增的text虚拟地址就行了
 
            add_reloc = ELF.Relocation()
            add_reloc.type = add_entry.type
            add_reloc.symbol = sym
            add_reloc.address = add_entry.address - add_got.virtual_address
            add_reloc.purpose = ELF.RELOCATION_PURPOSES.PLTGOT
            add_reloc = binary.add_pltgot_relocation(add_reloc)
            # print("add_reloc - ", add_reloc)
 
 
    binary.write("intermediate.so")
    #辣鸡工具,会导致偏移出问题,所以不得不进行手工修复
    inter = lief.parse("intermediate.so")
    #获取前面添加的两个load段的虚拟地址
    add_RE_seg_virtual_address = 0
    add_RW_seg_virtual_address = 0
 
    after_add_load_num = 0
    for i in binary.segments:
        if i.type == ELF.SEGMENT_TYPES.LOAD:
            after_add_load_num += 1
            if after_add_load_num > before_add_load_num and after_add_load_num <= before_add_load_num +2:
                if i.has(ELF.SEGMENT_FLAGS.X):
                    add_RE_seg_virtual_address = i.virtual_address
                if i.has(ELF.SEGMENT_FLAGS.W):
                    add_RW_seg_virtual_address = i.virtual_address
 
    print(hex(add_RE_seg_virtual_address))
    print(hex(add_RW_seg_virtual_address))
 
 
    #修复dynsym中新增的symbol,将Elf32_Sym->st_value 指向text段即可,0不修改是指向import func
    new_dynsym_content = inter.get_section(".dynsym").content
    add_dynsym_start = len(new_dynsym_content) - len(add_sym_value)*16
    print("add_dynsym_start:",add_dynsym_start)
 
    modify_dynsym_content = []
    inx = 0
    for entry_content in [new_dynsym_content[i:i + 16] for i in range(add_dynsym_start, len(new_dynsym_content), 16)]:
        entry = DynSymEntry.parse_from_content(entry_content)
        if(entry.sym_value != 0):
            print(hex(entry.sym_value))
            entry.sym_value =add_sym_value[inx] +add_RE_seg_virtual_address +len(add_plt.content)
            print(hex(entry.sym_value))
        inx += 1
        modify_dynsym_content += entry.content
    patch_file("intermediate.so", inter.get_section(".dynsym").offset + add_dynsym_start, modify_dynsym_content)
 
    #修复.rel.plt中新增的rel项,指向新增的第二个load段中加载的add.so中的got表
    modify_rel_content = []
    relplt = binary.get_section(".rel.plt")
    add_rel_start = binary.get_section(".rel.plt").size - len(add_sym_value)*8
    print("add_rel_start :", hex(add_rel_start))
 
    add_entry_ndx = 0
    for rel_content in [relplt.content[i:i + 8] for i in range(add_rel_start, len(relplt.content), 8)]:
        rel = RelEntry.parse_from_content(rel_content)
        if(rel.offset != 0):
            print("offset :", hex(rel.offset))
            rel.offset = rel.offset + add_RW_seg_virtual_address
            print("offset :", hex(rel.offset))
        modify_rel_content += rel.content
        add_entry_ndx += 1
    patch_file("intermediate.so", inter.get_section(".rel.plt").offset + add_rel_start, modify_rel_content)

将intermediate.so改名为libnative-lib.so,在apk里能成功执行。不报错就是胜利!

 

ida打开intermediate.so可以看到相关的函数,虽然把plt解析成了函数,不过不影响。

 

 

再对比libadd.so,可以看到,函数内容是一致的:

 

 

 

成功运行不报错~

结合步骤一和步骤二,让添加的decrypt函数执行起来

有两种方式,TX是让plt-》got表之间的偏移不变,就不需要修改plt表。

 

如果修改了plt、got之间的偏移,就需要通过修改字节码来实现正常运行。

 

首先可以看到plt表有三条指令,获取当前地址,添加偏移,跳转。

 

 

第一个红框,由于是0不好理解,我换成0x01. 那么就是取当前地址+0x0100000

 

第二个红框,0x62,向r12添加 0x62000

 

第三个红框,0xf00c,通过a8028 - a801c = c 。所以低12位为添加的偏移-》0x00c、0x1fc

 

由于其他代码与之前的相同,就没必要重复粘贴了。将新增的plt表修复相关代码粘贴出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 修复plt表的相关代码
def get_offset(inaddr):
    high = inaddr // 0x0100000
    mid = (inaddr & 0xfffff)//0x1000
    low = (inaddr & 0xfff)
    return high,mid,low
 
#需要fix plt 表,调用外部函数时需要 通过plt表进行跳转
    add_plt_size = add_plt.size
    print("add_plt_size:",add_plt_size)
    PLT_TABLE_HEAD_LEN = 0x14
    need_fix_plt_content = add_RE_seg.content[PLT_TABLE_HEAD_LEN:add_plt_size]
    print(need_fix_plt_content)
 
    print(hex(add_RW_seg_virtual_address - add_RE_seg_virtual_address))
 
 
    inx = 0
    modify_plt_content = []
    for plt_entry in [add_RE_seg.content[i:i+12] for i in range(PLT_TABLE_HEAD_LEN,add_plt_size,12)]:
        #+8是因为ADR取地址是取PC的值
        got2plt_offset = got_address_list[inx] - (inx*12+add_RE_seg_virtual_address+8 +PLT_TABLE_HEAD_LEN )
        inx += 1
        print("got -> plt offset :", hex(got2plt_offset))
        print(plt_entry)
        h,m,l = get_offset(got2plt_offset)
        plt_entry[0] = h
        plt_entry[4] = m
        plt_entry[8] = l & 0xff
        plt_entry[9] = (plt_entry[9]&0xf0) + (l >> 8)
        print("fix entry",plt_entry,hex(plt_entry[9]))
        modify_plt_content += plt_entry
    patch_file("intermediate.so", add_RE_seg_offset + PLT_TABLE_HEAD_LEN, modify_plt_content)

修复plt表后,执行so,就可以看到我们注入的decrypt函数优先于JNI函数执行了。为什么需要修复plt表呢?是因为decrypt中使用了android_log这个系统函数(在正常解密函数中无法避免使用系统函数)

 

 

到此,只需要将libadd.so中decrypt函数替换成真正的解密代码就行了。甚至是简单异或都可以实现so加密。


[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班开始招生!!

收藏
点赞6
打赏
分享
最新回复 (8)
雪    币: 2550
活跃值: 活跃值 (972)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
mudebug 活跃值 2021-4-1 01:57
2
0
 楼主牛逼。
雪    币: 270
活跃值: 活跃值 (446)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
gtict 活跃值 2021-4-1 12:58
3
0
 楼主牛逼。
雪    币: 1181
活跃值: 活跃值 (1762)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
唱过阡陌 活跃值 1 2021-4-2 19:44
4
0
楼主牛皮
雪    币: 575
活跃值: 活跃值 (142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Atnil 活跃值 2021-4-5 02:47
5
0
这个注入有意思
雪    币: 153
活跃值: 活跃值 (197)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
人在塔在 活跃值 2021-4-5 19:55
6
0
顶一个
雪    币: 531
活跃值: 活跃值 (1903)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
珍惜Any 活跃值 2021-4-6 15:01
7
0
用xposed直接Hook code里面的函数就行了
雪    币: 86
活跃值: 活跃值 (87)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
挥一挥衣袖 活跃值 2021-4-9 10:15
8
0
静态注入,so操作,mark
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
万里星河 活跃值 2021-4-11 23:18
9
0
牛逼 虽然看不懂
游客
登录 | 注册 方可回帖
返回