使用lief工具写一个类似于腾讯so保护这样一个东西。本篇记录lief工具的使用和TX加固的原理,不会对ELF文件格式进行记录。
首先,lief是一个文件解析工具,可以解析ELF、PE、DEX等,用途比较广泛。
github 地址:https://github.com/lief-project/LIEF
我使用的是python版,在使用过程中经常出现毛病,所以并不建议使用python版。最好是不怕麻烦自己写一个,就可以避免很多不稳定导致的fix代码。
首先,王者荣耀采用的是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快速解密:
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节解密。
由于一次写完大概率会很乱思路不够清晰,所以分几步写,方便理解,也好记录。
首先,生成一个SO,很简单,有一个PrintLog函数没有调用,所以LOG只有一条:
执行以下代码:
可以看到函数PrintLog优先于JNI函数执行,并且传入的参数为基址。
关于重定位
需要记住的几个总结:
rel.plt ->got -> plt -> extern表(导入函数)
rel.plt ->got -> text(导出)
编译一个so,里面的示范代码:
十分简单,就是一个打印log,将编译好的so拖入ida,可以看到,虽然我只写了一个函数,实际上so运行时还需要许多其他的函数。
执行以下代码:
将intermediate.so改名为libnative-lib.so,在apk里能成功执行。不报错就是胜利!
ida打开intermediate.so可以看到相关的函数,虽然把plt解析成了函数,不过不影响。
再对比libadd.so,可以看到,函数内容是一致的:
成功运行不报错~
有两种方式,TX是让plt-》got表之间的偏移不变,就不需要修改plt表。
如果修改了plt、got之间的偏移,就需要通过修改字节码来实现正常运行。
首先可以看到plt表有三条指令,获取当前地址,添加偏移,跳转。
第一个红框,由于是0不好理解,我换成0x01. 那么就是取当前地址+0x0100000
第二个红框,0x62,向r12添加 0x62000
第三个红框,0xf00c,通过a8028 - a801c = c 。所以低12位为添加的偏移-》0x00c、0x1fc
由于其他代码与之前的相同,就没必要重复粘贴了。将新增的plt表修复相关代码粘贴出来。
修复plt表后,执行so,就可以看到我们注入的decrypt函数优先于JNI函数执行了。为什么需要修复plt表呢?是因为decrypt中使用了android_log这个系统函数(在正常解密函数中无法避免使用系统函数)
到此,只需要将libadd.so中decrypt函数替换成真正的解密代码就行了。甚至是简单异或都可以实现so加密。
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)
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)
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)
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
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))
if
not
binary.has(ELF.DYNAMIC_TAGS.INIT):
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"
)
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
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)
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)
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
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))
if
not
binary.has(ELF.DYNAMIC_TAGS.INIT):
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"
)
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
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)
/
/
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
);
}
};
/
/
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)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)