一
前言
二
Introduction
dlopen
加载动态库,然后通过 offset 获取函数地址,执行即可。gdbserver
或使用frida
等类似工具进行跟踪。但这种方法过于笨重。是否存在一种更轻量级的方法来模拟运行这个 ELF 文件呢?三
从去除字符串加密开始
libkpk.so
开始,这个库文件了包含了一个需要被逆向出来的加密算法。libkpk.so
是一个从安卓安装包里提取出来的库文件,根据反编译可以发现,这里面应该包含有三个函数,分别是fetch
,isKpk
和kpk
,java通过jni来调用这几个函数。在这些函数中,我们重点关系的是kpk
函数,这个函数是作为加密函数出现的。init_array
里面的.datadiv_decodexxx
函数修改,所以我们可以合理推断这些datadiv_decode
函数就是解密函数,用在lib被加载的时候来解密被加密的字符串。datadiv_decode
正是被Armariris进行字符串混淆后会出现的解密函数。Armariris混淆
详细步骤
aarch64
下编译的,而我们的电脑是amd64的,没办法直接运行。这个时候,我们可以使用unicorn
来模拟运行程序。unicorn
基于qemu
,但是更加轻量级,提供了一个多个架构下模拟cpu运行的接口,非常适合在这个地方使用。Step 1: 加载elf到内存中
PT_LOAD
segment标示的内存区域从文件中读取并写入相对应的内存地址。p_type
=PT_LOAD
即代表该段为可装载段,表示即这个段将被装载或映射到内存中,其中p_offset
代表该段在文件中的位置,p_filesz
代表该段的长度。p_vaddr
为数据映射到虚拟内存中的地址,p_flags
代表这段区域的读写执行权限。当然因为我们是在cpu模拟机下执行,我们根本不关心他的权限,所以全部设置为rwx
。typedef struct {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
https://docs.oracle.com/cd/E19683-01/816-7777/chapter6-83432/index.html
get_mapping_address
会计算出这块内存需要mmap哪一块内存地址,并对齐page size,也就是0x1000。然后mmap
并写入数据就行了。for seg in lib.iter_segments_by_type('PT_LOAD'):
st_addr, size = get_mapping_address(seg)
# don't care, rwx everywhere
emulator.mem_map(lib.address + st_addr, size, UC_PROT_ALL)
emulator.mem_write(lib.address + seg.header.p_vaddr, seg.data())
log.info("loaded segment 0x%x-0x%x to memory 0x%x-0x%x", seg.header.p_vaddr,seg.header.p_vaddr+seg.header.p_memsz, lib.address + st_addr, lib.address + st_addr+size)
Step 2: 找到所有.datadiv_decode开头的函数并执行
.datadiv_decode
的函数,然后执行即可。LR
,在进入函数前,先设置LR
,那么函数结束的时候就会跳回LR
,我们在这里把LR
设置为0,那么就知道当程序运行到0的时候,函数就结束了。datadivs = []
for name in lib.symbols:
if name.startswith(".datadiv_decode"):
datadivs.append(name)
for datadiv in datadivs:
log.info("[%s] Function %s invoke", hex(lib.symbols[datadiv]), datadiv)
emulator.reg_write(arm64_const.UC_ARM64_REG_LR, 0) # 把return pointer (LR) 设置为0
emulator.emu_start(begin=lib.symbols[datadiv], until=0)
log.info("[%s] Function return",hex(lib.symbols[datadiv]),)
Step 3: 用解密后的数据patch掉原来加密的数据
.data
段里,直接把整个.data
段覆盖掉就行了。log.info("Patch .data section")
new_data = emulator.mem_read(lib.address + data_section_header.sh_addr, data_section_header.sh_size)
libfile.seek(data_section_header.sh_offset)
libfile.write(new_data)
Step 4: Patch掉所有的解密函数
log.info("Patch .datadiv_decode functions")
for datadiv in datadivs:
libfile.seek(lib.symbols[datadiv] & 0xFFFFFFFE)
ret = b''
try:
ret = asm(shellcraft.ret())
except:
# fallback to manual
ret = asm("ret")
libfile.write(ret)
效果
JNINativeMethod
结构也比较好容易可以分辨出来。四
ELF文件是怎么Load的
Segment简单介绍
readelf -l libkpk.decrypt.so
我们可以读取ELF文件并获取到一些基本的信息,首先我们可以知道这个库文件是一个动态的共享库文件。PT_LOAD
:PT_LOAD
即可装载段,代表这类段会被加载到内存中。PT_DYNAMIC
:PT_INTERP。
PT_INTERP
-static
参数的可执行文件,一般来说是没有PT_DYNAMIC
和PT_INTERP
segment的,因为没有必要。静态链接与动态链接
静态链接
source: 创建静态库的过程14
动态链接
source: 动态库链接过程14
ELF LOOOADING…..
PT_LOAD
端都加载到内存中,设置并初始化好stack。entry
地址。PT_LOAD
端后还需要额外处理PT_DYNAMIC
段修复重定向。PT_LOAD
端都加载到内存中,设置并初始化好stack。PT_LOAD
并完成本程序内所有符号的重定向。.rel
或.rela
),解析所有符号依赖,找到每个符号的实际地址,对符号引用进行重定位,修改内存中的代码或数据,使其指向正确的符号地址(包含即时绑定和懒绑定)。entry
,开始执行程序。entry
就好了。重定位
PT_DYNAMIC
段来拿到所有需要的信息。在64位下,PT_DYNAMIC
端中的数据结构可以由如下数据结构表示。d_tag
相当于一个类型标识符,d_un
由d_tag
控制,内部的值根据d_tag
的不同代表不同的值。typedef struct {
Elf64_Xword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;
d_tag。
DT_STRTAB
/DT_STRSZ
: 字符串表的地址和长度。运行时链接程序所需的符号名称、依赖项名称和其他字符串位于该表中。
DT_SYMTAB
: 符号表的地址。st_name
和st_value
。st_name
表示该符号在字符串表的索引,可以通过这个值和字符串表拿到该符号的字符串名称,如果st_name
的值为0,则代表该符号没有相对于的字符串名称。st_info
代表该符号的值。根据上下文,该值可以是绝对值或地址(和之后的重定向的类型相关)。typedef struct {
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;
DT_RELA
/DT_REL
: 重定位表的地址。一个二进制文件里可以有多个重定位节。为可执行文件或共享目标文件创建重定位表时,链接编辑器会连接这些节以形成一个表。在64位下,ELF有两种重定位表的结构 REL and RELA,分别对于DT_REL和DT_RELA。typedef struct {
Elf64_Addr r_offset; // Address
Elf64_Xword r_info; // 32-bit relocation type; 32-bit symbol index
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset; // Address
Elf64_Xword r_info; // 32-bit relocation type; 32-bit symbol index
Elf64_Sxword r_addend; // Addend
} Elf64_Rela;
r_offset
代表需要修复的虚拟地址。r_info
存放了r_info_sym
和r_info_type
的值,其中r_info_sym
表示该重定向指向了符号表中的第N项,r_info_type
代表了重定向的类型,对于不同的架构,重定向类型也有不同,具体可以参考官方的ABI。r_info_sym = r_info >> 8
r_info_type = r_info && 0xff
r_info_sym
和符号表找到每一个重定位项对应的符号,最后根据符号表中的st_value
,重定位表中的r_info_type
,r_addend
以及当前程序的base address计算出重定向之后的地址并写入内存中即可。AArch64下的重定位
r_info_type
是如何计算重定向值的,具体的计算方法我们可以参考arm官方的ABI6。在这里呢,我们来看一下比较常用的几个重定向类别。R_AARCH64_ABS64
: 符号地址+r_addend
即 程序基值 +st_value
+r_addened
R_AARCH64_GLOB_DAT
: 也是符号地址 +r_addend
也是 程序基值 +st_value
+r_addened
R_AARCH64_RELATIVE
: 程序基值加上 +r_addend
R_AARCH64_JUMP_SLOT
: 这个比较特殊,代表了跳转表。这个类别一般和调用外部库时有关。当st_value
的值为0的时候,说明这个符号是外部导入的,需要通过连接器从加载的库中找到并加载。当然,这个重定向也可以在运行时再链接,即在需要这个符号的时候进行懒加载,关于这段可以在搜索并参考**__dl_runtime_resolve()**的过程。st_value
的值不为0,则和R_AARCH64_GLOB_DAT
一样,都是符号地址+r_addend。
def get_symbol_table(elf: 'ELF') -> SymbolTableSection:
for section in elf.iter_sections():
if section.header.sh_type == "SHT_DYNSYM":
return section
def get_relocations(elf: 'ELF') -> Dict[int, Relocation]:
rel_sections: List[RelocationSection] = []
for section in elf.iter_sections():
if section.header.sh_type in ["SHT_REL", "SHT_RELA"]:
rel_sections.append(section)
if section.header.sh_type == "SHT_DYNSYM":
dynsym = section
relocs = dict()
# https://static1.squarespace.com/static/59c4375b8a02c798d1cce06f/t/59d55a7bf5e2319471bb94a4/1507154557709/ELF+for+ARM64.pdf
for rel_section in rel_sections:
for reloc in rel_section.iter_relocations():
r_offset = reloc.entry.r_offset # 表示 .text[r_offset]处需要进行修复
if r_offset in relocs:
raise Exception("wtf")
relocs[r_offset] = reloc
# print(reloc.entry.r_info_sym)
# r_info = reloc.entry.r_info # 用来存放 r_info_sym 和 r_info_type
# r_info_sym = reloc.entry.r_info_sym # 表示该重定向为符号表中的第 N 项
# r_info_type = reloc.entry.r_info_type # 表示该重定向的类型,对应枚举值 ENUM_RELOC_TYPE_ARM
# r_addend = reloc.entry.r_addend
# print(f"{rel_section.name} fixing {dynsym.get_symbol(r_info_sym).name} at {hex(r_offset)} to {hex(dynsym.get_symbol(r_info_sym).entry.st_value)} with type {hex(r_info_type)} added {hex(r_addend)}", )
# # print(dynsym.get_symbol(r_info_sym).name,dynsym.get_symbol(r_info_sym).entry)
return relocs
# fix relocation
relocs = get_relocations(lib)
symtab = get_symbol_table(lib)
for addr, reloc in relocs.items():
# 0x4962a0
# https://static1.squarespace.com/static/59c4375b8a02c798d1cce06f/t/59d55a7bf5e2319471bb94a4/1507154557709/ELF+for+ARM64.pdf
if reloc.entry.r_info_type == ENUM_RELOC_TYPE_AARCH64['R_AARCH64_JUMP_SLOT']:
name = symtab.get_symbol(relocs[addr].entry.r_info_sym).name
# need to import from external library
if symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value == 0:
print(name, hex(addr), symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value,
relocs[addr].entry.r_addend)
if reloc.entry.r_info_type in [
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_ABS64'],
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_GLOB_DAT'],
ENUM_RELOC_TYPE_AARCH64['R_AARCH64_JUMP_SLOT']]:
ql.mem.write(lib.address + addr,
(lib.address + symtab.get_symbol(relocs[addr].entry.r_info_sym).entry.st_value + relocs[
addr].entry.r_addend).to_bytes(8,
"little"))
elif reloc.entry.r_info_type in [ENUM_RELOC_TYPE_AARCH64['R_AARCH64_RELATIVE']]:
ql.mem.write(lib.address + addr,
(lib.address + relocs[addr].entry.r_addend).to_bytes(8, "little"))
else:
print(f"not handled r_info_type {reloc.entry.r_info_type}")
# exit(0)
五
用Qiling来模拟运行
导入函数的实现
libkpk.so
需要使用strlen
这个函数,那我们就可以hookstrlen
的地址并实现strlen的功能,这样就不需要寻找对于的依赖库了,而且还可以追踪函数调用的参数和结果。def hook_strlen(ql: Qiling):
# The string address is in X0 for AArch64
string_address = ql.arch.regs.read(arm64_const.UC_ARM64_REG_X0)
length = 0
while True:
byte = ql.mem.read(string_address + length, 1)
if byte[0] == 0:
break
length += 1
stlogger.log(f"strlen called with {ql.mem.read(string_address, length)}")
# Write the result back to X0
ql.arch.regs.write(arm64_const.UC_ARM64_REG_X0, length)
stlogger.callstack.pop(-1)
ql.arch.regs.write(arm64_const.UC_ARM64_REG_PC, ql.arch.regs.read(arm64_const.UC_ARM64_REG_LR))
ql.hook_address(hook_strlen, base_address + 0x0013b970)
函数调用追踪
class StackTracerLogger:
def __init__(self,
addr_to_fname,
base_address=0,
print_func_with_symbol_only=False,
print_exit=True,
printer=print
):
self.addr_to_fname = addr_to_fname
self.base_address = base_address
self.printer = printer
self.print_func_with_symbol_only = print_func_with_symbol_only
self.print_exit = print_exit
self.callstack = []
def call(self, func_addr: int, call_from: int):
self.callstack.append(func_addr)
fname = ""
if func_addr in self.addr_to_fname:
fname = self.addr_to_fname[func_addr]
if fname == "" and self.print_func_with_symbol_only:
return
elif fname == "":
fname = f"func_{hex(func_addr - base_address)}"
self.printer(
f"[{len(self.callstack)}]{' ' * len(self.callstack)}calls {fname} from {hex(call_from - self.base_address)}")
def exit(self, exit_addr):
if self.print_exit:
self.printer(f"[{len(self.callstack)}]{' ' * len(self.callstack)}exit at {hex(exit_addr - base_address)}")
if self.callstack:
self.callstack.pop(-1)
def log(self, msg):
self.printer(f"[{len(self.callstack)}]{' ' * (len(self.callstack) + 1)}{msg}")
def select_qiling_backtrace(self, arch_type: QL_ARCH):
if arch_type == QL_ARCH.ARM64:
return self.ql_aarch64_backtrace
def ql_aarch64_backtrace(self, ql: Qiling, address, size):
# Read the code at the current address
code = ql.mem.read(address, size)
# Decode the instruction (simple detection based on opcode; consider using Capstone for complex cases)
if size == 4:
opcode = int.from_bytes(code, 'little')
# Detect BL or BLX (0x94000000 for BL, check mask for lower bits)
if (int.from_bytes(code, 'little') & 0xFC000000) == 0x94000000:
# Calculate target address (offset is 26 bits, shift left and sign extend)
offset = int.from_bytes(code, 'little') & 0x03FFFFFF
if offset & 0x02000000: # Sign bit of 26-bit offset
offset -= 0x04000000 # 2's complement negative offset
target = address + (offset << 2) # left shift to account for instruction size
self.call(target, address)
# blr
elif (opcode & 0xFFFFFC1F) == 0xD63F0000:
reg_num = (opcode >> 5) & 0x1F
reg_val = ql.arch.regs.read(reg_num)
self.call(reg_val, address)
elif opcode == 0xd65f03c0: # RET
self.exit(address)
addr_to_fname = dict((v, k) for k, v in lib.symbols.items())
stlogger = StackTracerLogger(
addr_to_fname, lib.address, print_func_with_symbol_only=True, print_exit=False,
printer=log.info
)
ql.hook_code(stlogger.select_qiling_backtrace(ql.arch.type))
效果
call stack 追踪,发现使用了aec ecb作为加密方法
成功完成加密,并读取到了加密后的网址
六
结语
unicorn
和qiling
进行跨平台模拟。Reference
看雪ID:Aynakeya
https://bbs.kanxue.com/user-home-967169.htm
# 往期推荐
2、恶意木马历险记
球分享
球点赞
球在看
点击阅读原文查看更多
原文始发于微信公众号(看雪学苑):How2模拟执行一个不同架构下的elf文件