Unicorn实战(二):去掉armariris的字符串加密

armariris是上交Gossip的在2016年公开的,在ollvm上添加了额外一个Pass,从而实现字符串加密,由于其特征简单,破解也简单,于是使用unicorn破解掉它的字符串加密。

本文的涉及到2个文件,libcms.so请从4.0.0版本的抖音app里解压出来,decrypt_armariris.py 位于该gist下:https://gist.github.com/LeadroyaL/9b0bc6f6a908db1adfc48d85ee43451d

一、armariris字符串加密实现原理

我曾经移植过它,代码位于https://github.com/LeadroyaL/llvm-pass-tutorial/blob/dev/Armariris/StringObfuscation.cpp ,逻辑挺简单的,对于所有的常量字符串,先创建一份可读写的、类型相同、大小相同的全局变量,将原先的字符串xor随机数,存放到这块新的全局变量里。

再将对应的解密逻辑存放到一个函数里,将该函数放到.init_array中,从而在library被第一次加载时,从全局区读数据,再写入全局区,对字符串进行解密,不影响执行过程中对常量字符串的读取。

二、主要思路。很简单,因为解密逻辑在.init_array里,全都是exported=true、而且符号名是.datadiv开头,直接把它执行一遍。此时内存中的rwdata就是解密后的,覆盖掉原先的rwdata,再删掉解密函数,即patch完成。

三、详细思路

1、根据符号表,找到.datadiv开头的函数

1
2
3
4
5
6
7
# 遍历符号表,找到.datadiv_decode开头的函数
datadivs = []
dynsym = elf.get_section_by_name(".dynsym")
assert isinstance(dynsym, SymbolTableSection)
for symbol in dynsym.iter_symbols():
if symbol.name.startswith('.datadiv_decode'):
datadivs.append(symbol.entry.st_value)

2、加载so文件到unicorn中,依次执行各个字符串解密函数

1
2
3
4
5
6
# 调用datadiv
for datadiv in datadivs:
print("Function invoke", hex(datadiv))
emu.reg_write(UC_ARM_REG_LR, 0)
emu.emu_start(datadiv, 0)
print("Function return")

3、Patch掉rwdata的数据

1
2
3
4
5
6
# Patch data
data_section_header = elf.get_section_by_name('.data').header
new_data = emu.mem_read(data_section_header.sh_addr, data_section_header.sh_size)
print("Patch data")
fd.seek(data_section_header.sh_offset)
fd.write(new_data)

4、Patch掉字符串解密函数 这个地方是有细节的,解密函数的引用在init_array,会影响到init_array的size等,需要主动恢复,而且经过观测,init_array的内容会受到重定位的影响,需要把对应重定位项也移除掉。有以下三种方案防止在.init_array里把修好的字符串破坏掉 init_array里的重定位只涉及R_ARM_ABS32,在so被linker加载时,重定位会将对应位置写为base+abs32的内容,如果乱Patch会崩溃,因此有两个方案: 方案一:剔除掉重定位项,改为无用的值;剔除掉init_array里的解密函数,将init_array变紧凑; 方案二:在so里寻找bx-lr的gadget,即空函数,修改重定位项,将abs32指向它; 还有一个投机取巧的方案: 方案三:将所有的datadiv改为BX LR;

最终采用方案三的方案,因为这样做代价最小。。。

1
2
3
4
5
6
7
8
9
10
11
12
# Patch datadiv 直接返回
print("Patch datadiv")
ks_thumb = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
ks_arm = Ks(KS_ARCH_ARM, KS_MODE_ARM)
for datadiv in datadivs:
fd.seek(datadiv & 0xFFFFFFFE)
if datadiv & 0x1 == 0x1:
a = ks_thumb.asm('bx lr')[0]
else:
a = ks_arm.asm('bx lr')[0]
for _ in a:
fd.write(struct.pack("B", _))

四、验证成果

懒得手动使用armariris了,就使用了和上篇一样的样本,libcms.so借鉴了armariris的字符串加密,从原来的for循环加xor,改为了自己定义的运算,但方式是一模一样的。运行脚本后,将文件丢到手机里覆盖原先的文件,发现仍然可以正常工作,表明patch成功!

1
2
3
4
➜  unicorn_proj strings libcms.so  grep UserInfo
com/ss/android/common/applog/UserInfo
getUserInfo
getUserInfoSkipGet