使用LIEF打造类似王者X耀的静态代码注入
2021-04-11 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:178 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:八重嘤

使用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。
libtprt.so对自身的tptext节进行了加密,在加载过程中执行init_array进行解密,跟踪mprotect,可以看到解密代码:
使用lief快速解密:
tprt = lief.parse(name)tptext_section = tprt.get_section(".tptext").content
print(len(tptext_section))offset = tprt.get_section(".tptext").offsetout = 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只有一条:
执行以下代码:
"""  添加init段,并且调用ADD_FUNC_NAME并传入一个参数(当前So的基址)"""INIT_PROC_SIZE = 0x2cINIT_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中添加可执行代码

关于重定位:

https://bbs.pediy.com/thread-221821.htm

 
需要记住的几个总结:
(1) rel.plt ->got -> plt -> extern表(导入函数)

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

//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,里面的示范代码:
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表修复相关代码粘贴出来。
# 修复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加密。

- End -

看雪ID:八重嘤

https://bbs.pediy.com/user-home-833877.htm

  *本文由看雪论坛 八重嘤 原创,转载请注明来自看雪社区。

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458383567&idx=1&sn=c622e42adfe29665a92715bdb4a6dad2&chksm=b180c44586f74d53fe50ca26d3cebd48d50f4511f638760dd2738d82428564d5b17efef60ed7#rd
如有侵权请联系:admin#unsafe.sh