一、前言
咳咳咳,下面是一段彩蛋,请读者使用 BASE64 解码查看。5oiR5Z+65pys5LiN5ZyoQ1NETuabtOaWsOS6hu+8jOeOsOWcqOS4k+W/g+e7tOaKpOaYn+eQg+OAguebruWJjeW3suWPkeW4g+WGheWuuei2hei/hyAzMDAg56+H77yM57K+5Y2O6LS06LaF6L+HIDEwMCDnr4fjgILlhoXlrrnljIXmi6wgVW5pZGJnIOaooeaLn+aJp+ihjOOAgUlEQSDohJrmnKzlvIDlj5HjgIHlr4bnoIHlrabjgIHnrpfms5XliIbmnpDlrp7miJjnrYnnrYnor53popjvvIzliqDmiJHlvq7kv6EgMTg3OTYyNTMwNTEg5LqG6Kej5pif55CD6K+m5oOF44CC5Y6f5Lu3IDI5Oe+8jENTRE4g55So5oi354m55Lu3IDgg5oqY44CC。
字符串加密是一种非常传统的代码保护技术,可以追溯到21世纪初。它在 PC 安全与对抗中得到了广泛的应用,然后迁移到 JAVA/JavaScript/Android 等语言和场景上。因为字符串加密保护的出现频率很高,所以几乎每一款反汇编器或反混淆器,在介绍自身所支持的扩展形式时,都会展示如何用它做字符串解密,就像下面这样。
毫不夸张的说,用脚本做字符串解密就是脚本开发场景里的 Hello World
,我们对 IDA 脚本开发的讨论也从此处开始。
我们会用至少五篇内容去讨论它,你可能会对此心存困惑,为什么要对它花这么大的篇幅讨论?请读者耐心往下看。任何保护以及其对抗的当前方案,都不是什么传世名著,出版后几百年都一字不变,相反,它身处于一场猫鼠游戏,对抗和保护的方案都在不断优化和发展,以更好的限制对方。即使是字符串加密这样简单的保护方案,它的实现与解密也经过了多轮迭代。将它的发展过程展开讲,自然就会占据大量篇幅,但这么做是有意义的。如果我们总是直接学习最新的议题和解决方案,那么在惊叹于它的复杂和精巧之余,也会心生畏惧,觉得自己无法把握。但如果追溯其发展过程,看到方案从朴素的正则匹配逐步迭代到 IR 处理的全过程,这种畏惧感就会退散,让人恐惧的老板小时候也穿过尿不湿,类似这样的感觉。
二、字符串加密简述
字符串信息是二进制文件里是十分重要的辅助信息,富有经验的攻击者可以通过观察字符串表获得大量信息,比如所采用的加密算法、检测与 Anti 的关键点等等。字符串加密使得攻击者在静态分析二进制文件时,只能观测到字符串被加密后的密文,无法理解其含义,进而阻挠攻击者对样本的分析和理解。
字符串加密干扰静态分析,但基本影响不了动态分析。不论字符串被怎么加密,被使用时一定处于完全的解密和明文状态,否则这种加密方案就影响了业务逻辑。因此一些分析人员认为,包括字符串加密在内的绝大多数代码保护方案都是纸老虎,只要动态调试跑起来。跟着程序逻辑走,总能查看到明文和正确逻辑。花指令、控制流混淆、字符串加密、虚假控制流等都属于此列。这种观点发展到极端,认为一切混淆技术都是纸老虎,动态调试从头冲到底即可。
我并不是这种观点的拥护者,出于两方面原因
- 这个方法论让样本分析这件事变成了一趟苦旅
- 忽略了时间成本,不做任何反混淆会绕很多弯路
因此我更倾向于使用反混淆、自动化相关的技术。在字符串加密这个话题上,如果样本不复杂,还原和解密字符串,使其不影响静态分析,是一件美事。除此之外,处理字符串加密所使用的技术、思路、API,也可用于处理其它形式的混淆和对抗。本篇浅浅的讨论几个样例,为后文更深入的分析做铺垫。
三、dump 回填
3.1 样本一
样本:pdd.7z
pdd_secure
存在字符串加密,字符串解密的时机是它被使用的具体函数内部。随便选择一个函数 F5 ,就可以观察这个现象,比如 sub_2D9B4 。
__int64 __fastcall sub_2D9B4(__int64 a1)
{
unsigned int v1; // w8
int v2; // w8
int v4; // w8
_BYTE v6[96]; // [xsp-60h] [xbp-D0h] BYREF
__int64 v7; // [xsp+0h] [xbp-70h]
unsigned __int64 v8; // [xsp+8h] [xbp-68h]
int v9; // [xsp+10h] [xbp-60h]
unsigned int v10; // [xsp+14h] [xbp-5Ch]
__int64 v11; // [xsp+18h] [xbp-58h]
v7 = a1;
v8 = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
v11 = *(_QWORD *)(v8 + 40);
v1 = __ldar(dword_699B0);
v9 = 527729684;
v10 = v1;
while ( 1 )
{
while ( 1 )
{
while ( v9 > -752143043 )
{
if ( v9 == -752143042 )
{
pthread_mutex_lock(&stru_699C0);
if ( __ldar(dword_699B0) )
v4 = -1065271286;
else
v4 = -1890910423;
v9 = v4;
}
else
{
if ( v10 )
v2 = -1825803887;
else
v2 = -752143042;
v9 = v2;
}
}
if ( v9 != -1890910423 )
break;
byte_6225C ^= 0x5Eu;
byte_6225D ^= 0x69u;
byte_6225E ^= 0x5Fu;
byte_6225F ^= 0xA9u;
byte_62260 ^= 0x21u;
byte_62261 ^= 0xA3u;
byte_62262 ^= 0x8Cu;
byte_62263 ^= 0x8Bu;
byte_62264 ^= 0xE4u;
byte_62265 ^= 5u;
byte_62266 ^= 0x78u;
byte_62267 ^= 0x23u;
__stlr(1u, dword_699B0);
v9 = -1065271286;
}
if ( v9 != -1065271286 )
break;
pthread_mutex_unlock(&stru_699C0);
v9 = -1825803887;
}
__system_property_get(&byte_6225C, v6);
return (*(__int64 (__fastcall **)(__int64, _BYTE *))(*(_QWORD *)v7 + 1336LL))(v7, v6);
}
单独看其中的字符串解密部分
byte_6225C ^= 0x5Eu;
byte_6225D ^= 0x69u;
byte_6225E ^= 0x5Fu;
byte_6225F ^= 0xA9u;
byte_62260 ^= 0x21u;
byte_62261 ^= 0xA3u;
byte_62262 ^= 0x8Cu;
byte_62263 ^= 0x8Bu;
byte_62264 ^= 0xE4u;
byte_62265 ^= 5u;
byte_62266 ^= 0x78u;
byte_62267 ^= 0x23u;
有人可能会困惑,为什么确定这是字符串解密,而不是某种算法逻辑?我们可以验证一下,看这段代码做了什么。
byte_6225C ^= 0x5Eu;
也就是 byte_6225C = byte_6225C ^ 0x5Eu;
。byte_address 在 IDA 中意值位于 address 位置的单个字节。因此表达式的语义就是将 0x6225C 地址处的字节和 0x5E 异或后,结果放回原处。我们可以用 IDAPython 脚本验证这一点,代码运行环境是 IpyIDA。
import idaapi
chr(idaapi.get_byte(0x6225c) ^ 0x5e)
Out[7]: 'r'
chr(idaapi.get_byte(0x6225d) ^ 0x69)
Out[8]: 'o'
chr(idaapi.get_byte(0x6225e) ^ 0x5f)
Out[9]: '.'
chr(idaapi.get_byte(0x6225f) ^ 0xa9)
Out[10]: 'b'
chr(idaapi.get_byte(0x62260) ^ 0x21)
Out[11]: 'u'
chr(idaapi.get_byte(0x62261) ^ 0xa3)
Out[12]: 'i'
chr(idaapi.get_byte(0x62262) ^ 0x8c)
Out[13]: 'l'
chr(idaapi.get_byte(0x62263) ^ 0x8b)
Out[14]: 'd'
chr(idaapi.get_byte(0x62264) ^ 0xe4)
Out[15]: '.'
chr(idaapi.get_byte(0x62265) ^ 0x05)
Out[16]: 'i'
chr(idaapi.get_byte(0x62266) ^ 0x78)
Out[17]: 'd'
chr(idaapi.get_byte(0x62267) ^ 0x23)
Out[18]: '\x00'
前面我们谈关于 IDC 和 IDAPython 的差异时说过,在执行简单的脚本任务时, IDC 的体验也很好,展示如下
我们发现,异或后这段内存的结果为 ro.build.id ,这证实了我们前面的猜想,其功能为字符串解密。
下面开始考虑如何对它做处理,使得字符串加密对静态分析不造成影响。
我们首先可以观察到,此处密文在解密后,明文放到了原先密文的地址上,这种解密方式叫 原地解密 或 覆盖解密。这意味着我们可以通过 patch 实现解密的功能。
from idaapi import patch_byte, get_byte
patch_byte(0x6225c, get_byte(0x6225c) ^ 0x5e)
Out[3]: True
patch_byte(0x6225d, get_byte(0x6225d) ^ 0x69)
Out[4]: True
patch_byte(0x6225e, get_byte(0x6225e) ^ 0x5f)
Out[5]: True
patch_byte(0x6225f, get_byte(0x6225f) ^ 0xa9)
Out[6]: True
patch_byte(0x62260, get_byte(0x62260) ^ 0x21)
Out[7]: True
patch_byte(0x62261, get_byte(0x62261) ^ 0xa3)
Out[8]: True
patch_byte(0x62262, get_byte(0x62262) ^ 0x8c)
Out[9]: True
patch_byte(0x62263, get_byte(0x62263) ^ 0x8b)
Out[10]: True
patch_byte(0x62264, get_byte(0x62264) ^ 0xe4)
Out[11]: True
patch_byte(0x62265, get_byte(0x62265) ^ 0x5)
Out[12]: True
patch_byte(0x62266, get_byte(0x62266) ^ 0x78)
Out[13]: True
patch_byte(0x62267, get_byte(0x62267) ^ 0x23)
Out[14]: True
patch 已经生效,转到 IDA 的 Hex 界面,它会将我们修改过的字节标为橘色,看起来十分直观。
再回到反汇编界面,我们需要帮助 IDA 识别这个字符串
鼠标悬停在首字节 byte_6225c 上,选择 Edit 菜单栏,Strings - String 确认转化为字符串,也可以直接使用快捷键 A 。
对 sub_2D9B4 重新F5。
有了两方面变化。1 是将新识别的字符串命名为 aR0BuildId,这是个挺好的自动命名,不用跳转到对应地址就能知晓内容。 2 是这个字符串的使用位置是作为 system_property_get
的参数 1,我们可以静态理解这个函数调用的意义,处理字符串解密确实有其价值。
关于第一点,即 IDA 对字符串的自动命名,下面做详细讨论。默认情况下,IDA 会以 a 为前缀,根据字符串的内容,基于驼峰命名法做命名,比如上面的aRoBuildId
。我们也可以介入与改变命名方式,使其更符合我们个人的习惯。首先打开 Options 菜单栏,General - Strings 设置面板,红框内部是可做的修改,修改会从下一次命名字符串开始生效。
Prefix 用于设置前缀,默认为 a 。比如修改为 str_ 时,效果如下
.data:0000000000060030 str_AndroidPerm DCB "android.permission.READ_PHONE_STATE",0
Preserver case 勾选框指是否使用驼峰命名法,否则会使用全小写命名,和上一条联合设置的效果如下
.data:0000000000060030 str_androidperm DCB "android.permission.READ_PHONE_STATE",0
Generate serial names 意指不必根据字符串内容做命名,而是按照命名生成的先后,从数字 Number 开始命名,Width 为字符宽度。当 Number 设置为 100,Width 设置为 8 时,效果如下
.data:0000000000060084 str_00000100 DCB "rom_status=%ld",0
.data:0000000000060084
.data:0000000000060093 ALIGN 4
.data:0000000000060094 str_00000101 DCB "hitSpng",0
.data:0000000000060094
.data:000000000006009C str_00000102 DCB "pdd_config",0
感兴趣的读者可以选择自己的字符串命名格式,下面回归正题。在 sub_2D9B4 单个函数上,我们已经实现了初步的字符串反混淆。说这是一个初步的反混淆,是因为并没有将代码完全还原回字符串加密前的状态,字符串加密部分的逻辑应该被 patch 掉,反汇编或反编译里都不应该出现,但目前似乎已经够用,所以不做这种讨论。下面讨论如何对全体函数做解密。
请注意,既然是覆盖解密。那么如果我们 dump 运行时的内存,如果目标函数已经执行过,那么它所对应的密文不就已经被明文覆盖了吗?比如从 0x609f0 开始的 16 字节,试试我们的 Frida 插件。
// 由ShowFridaCode生成的dump memory
function dump_0x609f0() {
var base_addr = Module.findBaseAddress("libpdd_secure.so");
var dump_addr = base_addr.add(0x609f0);
console.log(hexdump(dump_addr, {length: 0x10}));
}
Hook 结果如下,发现确实如此。
[MIX 2S::xxx]-> dump_0x609f0()
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
73b40329f0 70 64 64 5f 73 65 63 75 72 65 5f 6e 61 74 69 76 pdd_secure_nativ
[MIX 2S::xxx]->
只要目标函数已经被执行,那么其中的字符串就会被解密,进而可以 dump 到对应的明文字符串。因此我们需要运行一段时间 App,确保 SO 中的函数尽可能多的被执行,尽可能多的密文被解密为明文。或者反过来说,如果不这么做,比如刚启动 App 就 dump,甚至在 JNI_OnLoad 执行前就 dump,那么打印出来的依然是密文字符串。
那么问题的关键就在于——全体密文的具体位置在哪儿?逐一收集每个函数中所涉及到的解密地址范围是可行的,但比较麻烦。有没有比较简单粗糙,但可行的办法?
让我们整体观察密文的存储位置,首先看一下各段的信息,View 菜单栏 - Open subviews - Segments。
rodata 段,即只读数据段,它用于保存程序里面的常量数据。下图可见,其中的字符串都是可见明文。这很合理,常量加密的话,按照本样本的解密逻辑,没法覆盖写入。
data 段,即可读写数据段,用于存放各种已初始化的变量。下图可见,其中找不到什么明文字符串数据。
应该说,data 段中,字符串数据占据了较大一部分。因此,能不能省事一点,不去找所有密文所处的具体位置, dump 整个 data 段并回填呢?这么做可能会造成误伤,因为除了字符串以外的数据本不需要 dump,而且其中一些在运行时发生了改变, dump 放到静态文件中还会带来错误。但先不管这么多,我们目前最需要的是找个方法,它可以整体上解决问题,至于细节、优化、副作用可以放后面考虑。
IDA View - Open subviews - Segments 查看 data 段的范围 0x60000 - 0x642E0,修改 Frida 脚本
function dump_memory() {
var base_addr = Module.findBaseAddress("libpdd_secure.so");
var dump_addr = base_addr.add(0x60000);
console.log(hexdump(dump_addr, {length: 0x42E0}));
}
以 attach 模式附加到已启动的 App 上,运行尽可能多的代码逻辑,调用 dump_memory 函数,将 hexdump 获得的数据整体拷贝到 cyberchef 中查看。
可以看到非常多的明文字符串。接下来将数据 tohex ,设置去除间隔符拷贝下来。
使用IDA的 patch_bytes
API做回填
import idaapi
hex_string = "00000000000000000000000000000000284c616e64726f69642f636f6e74656e742f436f6e746578743b295a00000000616e64726f69642e7065726d697373696f6e2e524541445f50484f4e455f535441544500000000000000000000000000636f6d2f78756e6d656e672f70696e64756f64756f2f75742f7574696c2f5574696c7300726f6d5f7374617475733d256c64000068697453706e67007064645f636f6e6669670000000000000000000054454c4550484f4e595f53455256494345000000000000000000000000000000706c6174666f726d5f747970653d25640000000050484f4e455f545950455f43444d4100000000000000000000000000616e64726f69642e7065726d697373696f6e2e57524954455f45585445524e414c5f53544f5241474500000050484f4e455f545950455f47534d00007375625f43313033000000007375625f43313034000000006e64657669640000626f6f745f74696d653d256c640000007375625f43313032000000007375625f4331303100000000696d6569305f64657465637400000000696d6569315f646574656374000000006470693d25640000000000007265736f6c7574696f6e3d256478256400000000776966690000000073657269616c000073657269616c300073657269616c3100706464696400000070646469643d000025640000726f6f743d2564006275696c645f69640000000066696e6765727072696e740063686172616374657269737469637300626174746572795f737461747573000000000000000000006c696273706e672d302e302e312e736f000000006d656964305f646574656374000000006d656964315f64657465637400000000637075696e666f0000000000696e7374616c6c5f74696d653d256c6c640000000000000000000000000000006170705f7570646174655f74696d653d256c6c64000000006574685f636865636b3d2564000000000000000000000000736861726564707265666572656e63655f6964006d656d6f72795f69640000007364636172645f6964000000696d656900000000696d656930000000696d6569310000006465766963653000646576696365310064657669636532006d656964000000006d656964300000006d656964310000006d616300626174746572790075696400636f6f6b69650000696d736900000000696d736930000000696d736931000000616e64726f69645f69640000736e6f0073657175656e6365000000006c6f63616c5f73657175656e636500006469645f696e666f0000000000000000c21da1b2c66236e7cadcf82c04b3dd18a41fa9fe99e23388de4ab46636e4dd0296725d0a699e58544fddddcf251986230d03d7451a25eb5c6232c904cdc7bb6e4cb9f18126fb6e83f1a59b5da14917838e82938e71088c68356ea062a73d83ee44db698fa6cab356e0881d68b13aa8f87543f0d721cdd9b687a0175ee030479b7b226b6579223a222573222c2264617461223a222573227d0000000001000100c21da1b2c66236e7cadcf82c04b3dd18a41fa9fe99e23388de4ab46636e4dd0296725d0a699e58544fddddcf251986230d03d7451a25eb5c6232c904cdc7bb6e4cb9f18126fb6e83f1a59b5da14917838e82938e71088c68356ea062a73d83ee44db698fa6cab356e0881d68b13aa8f87543f0d721cdd9b687a0175ee030479b7b226b6579223a222573222c2264617461223a222573227d000000000100010081906394ccb400000000000000000000774ff75ae8567ffeb9563fd0482ad10ea2a4b98d1f1a66882f37c40a468f5ad4704341d4de146b3a2b267f98a35f7ea25d6e2bc1fa764495d43774629e2cd9e2bcf1630989c97d2f35b7668522d6817f57ace4a94c726a42c94653685b8e0ae88c4603d981fb2798e8879824f2f652a75ebcea51a799f45727e124296e2eabec14c0030025732c257300000000000000904332b0db5124fe2162386ed0786dd19ae7d5528a658e67bcb2e53204ce40701f13dfb0799484c63d31a9c05c712a394a42dc7c945743e8d3a0cfcefd700ebe34d41857b0fda0f0709bc353e2e621fd8a6986a03eb5b1ef5efa0205428af2992ab4daebe5a338fbc9201e673584644133c058ad154b7c6dcd25071d2ec1cb6d01000100000000000000000000000000142ff681ff62ba64f51865f48cda3105ee72284f989ab4fe4b5bffa65b119013bb3d285729f6d7bb1608f97dc4e00000db5b7b7e6f4f9ef4697d7a2b8d071b9fbf433ba30a505f77c2b323a4f24ad4a60de42fd6845e1c4c3aa5b4c3c21cea3d6cacb97dcb56b0c4ca9a0ce40868d27fd30b03a041b89d2e463ab9bb73dd88c6683f7ab23b5947c62a516481eb5c98eb16e44e788aac893de60dc8db0277c937b69e9e6da1181a472dec0643337fb3f90db5cc00000000000000000000000000a4279eb14045a7bfaff92d988b7a199b763cdbbc1b7f7c38d50000000000000010d7ceccd3d843d5891b42c4fa73f91529164097028584e4729411686063741509a4af7c9fc6d07ea47fe513e576457e26e96888116cef36ec62a945d88c38986a655d4542703047626db312fbecd19432e8d840ca1a070fb743363b1ef3d61eb8d7941ca16a5da2f17b4203b8bbd2a120cff85806a17e015d8cad45d869b93d62b31000592ddb70520efb54fe1fb76d12a9a330000000000000000000000000fba1e1766e9863d45495edeee58a94d9a3f93b4e168f6b02989745c1f5dda13806d5c54514ead67bb2cfbc539aecf02d19ac6d6102a49c54ca5083469395fd1c56810c7ae526bde72b7fd2cb47386efee66f67973a994bc9cfa64fef703065d0bbaeb25ac7599e6edc5482db3bcbfdc869d3e778e71d816b6d849f4d50c4197a6879fd000cc52b0047686432d69c20063128039e1274b02e876c3baa29ef50ffec56ec91387a87e532616600000000007064645f6165735f3138303132315f31000000002829490068617368436f6465000000000000000000000000000000006a6176612f6c616e672f537472696e670000000032616700f5d7a200000000004b840e74ad4e797f49b9346458551623a02927402247e7bf0d4830d0ddcf3420d91aff3865e7b1e31cc5b15b81c798bbcca355dce345e6cb1b7e9816534da97a3f7b673a536373514dd974ed1345a3970f279a514cc7d4c1dd120239a75fc7a908c1c50d269e48a98c87dceb4ea34a012bfdf6ed87eec3c17553d20c1bad67ce99fa4c97037f5910fe422df9db8c12045fe7311e63eed99416853c4ee13b00002e603c00000000000000000000000000502f367ea262299bb47ef5f1356bebd540dd65bcba8b2a0794639e732917000081a1dd43123e943a8715e1ead0e75e5e157c8631a09b4341c97a191df9f6cb04c1c0c127b28563ab5036a36303134276491c3a610850eee5c75dc0ec7b679e72a163526c2dbead467d2b1b517823ca8564a72c790b8d32cf778c04478d79557e717b58207a1a945cba3820881aca444f847f0f4a6158bfae7509562bedb25ed37064645f7365637572655f6e61746976650000000000000000000000000000004a4e495f4f6e4c6f616420797562656e675f7365637572652064617461203a2573202573000000004d617920313320323032320030313a35333a3334000000004a4e495f4f6e4c6f616420797562656e675f736563757265204a4e495f4f6e4c6f6164206572726f72000000000000004a4e495f4f6e4c6f6164207064645f7365637572652064617461203a2573202573000000000000000000000000000000636f6d2f78756e6d656e672f70696e64756f64756f2f7365637572652f4465766963654e617469766500000000000000e4d702b473000000f5d702b47300000006d802b47300000017d802b473000000f0d802b47300000031d902b47300000042d902b4730000006fd902b47300000080d902b47300000091d902b473000000a2d902b473000000b3d902b473000000c4d902b473000000d5d902b473000000e6d902b473000000f7d902b47300000018da02b4730000000000000000000000726f2e6275696c642e76657273696f6e2e73646b000000002f646174610000002f73797374656d2f62696e2f737500002f73797374656d2f7862696e2f7375002f73797374656d2f7362696e2f7375002f7362696e2f7375000000002f76656e646f722f62696e2f737500005041544800000000737500007768696368207375000000007200000000000000000000002f73797374656d2f6170702f5375706572757365722e61706b000000000000006a6176612f6c616e672f537472696e670000000067657442797465730000000028295b4200000000000000000000000067657453797374656d5365727669636500000000000000000000000000000000616e64726f69642f636f6e74656e742f436f6e74657874000000000000000000284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f4f626a6563743b000000000000000000004c6a6176612f6c616e672f537472696e673b00000000000000000000000000000e45dd3d8b5edfc2a130d69cbadeeb56ca342e000000000000000000000000005f006c6ed700765a05ee5a2d35a82559a5d011e52382bcf548fc89268b353b16c6d5cd13000000000000000000000000432fa57ecddbf8765b56f4762657666a584b748b94524b220000000000000000616e64726f69642f636f6e74656e742f436f6e74657874000000000000000000676574536861726564507265666572656e636573000000000000000000000000284c6a6176612f6c616e672f537472696e673b49294c616e64726f69642f636f6e74656e742f536861726564507265666572656e6365733b0000000000000000616e64726f69642f636f6e74656e742f436f6e746578740000000000000000007265676973746572526563656976657200000000000000000000000000000000284c616e64726f69642f636f6e74656e742f42726f61646361737452656365697665723b4c616e64726f69642f636f6e74656e742f496e74656e7446696c7465723b294c616e64726f69642f636f6e74656e742f496e74656e743b0000000000616e64726f69642f636f6e74656e742f436f6e74657874000000000000000000636865636b53656c665065726d697373696f6e00000000000000000000000000284c6a6176612f6c616e672f537472696e673b2949000000000000000000000066b33bfc85d9dce2e3a684a67e9a8598513760cfff562459a6da27e883d8d7cf716870600000000000000000000000009c38e6dbefc97b209aa99f86e76c8955e600000068ae7aebd8e0482a18a5bda5ea00000009ba283af6ee3e410000000054b3569cdaec8a912e254f63f2fa80e81e1d16c2eb000000000000000000000073801305f62d9a77b6eef6ebb27bd8e3ec66974b1bbce218f0a29a0e06a5aaf8df320f52c700000000000000000000005685ef2724fa018c99b56106f57b28d9abebe252a9067a1d7322a3f879e3d6d570a70000000000000000000000000000616e64726f69642f636f6e74656e742f436f6e746578740000000000000000006765745061636b6167654d616e6167657200000000000000000000000000000028294c616e64726f69642f636f6e74656e742f706d2f5061636b6167654d616e616765723b0000000000000000000000616e64726f69642f636f6e74656e742f436f6e7465787400000000000000000028294c6a6176612f6c616e672f537472696e673b000000006765745061636b6167654e616d6500000000000000000000ad23d5bca5a8e97a9c4db4f52e333d7c29772fc9297f579cc2f4f20000000000c8da7b64f808a6dd80595e5f6890be3473aa67f9668b0f9c7bd0c082d06da40009807ee898b0c97829ac346b33584e50d559af37d35f0000b6b73a5e15bacf66cad26ddbb6f7b7000000000000000000c1e0ef4b65629cc14a34234463bc1d2d0d52ca9caa405778aab2625100000000726f2e6275696c642e646973706c61792e696400000000000000000000000000726f2e6275696c642e76657273696f6e2e696e6372656d656e74616c00000000616e64726f69642e7065726d697373696f6e2e524541445f50484f4e455f535441544500000000000000000000000000726f2e70726f647563742e6272616e6400000000000000000000000000000000726f2e70726f647563742e646576696365000000726f2e6275696c642e74616773000000000000000000000000000000726f2e70726f647563742e6d6f64656c00000000000000000000000000000000726f2e70726f647563742e6d616e756661637475726572000000000000000000726f2e70726f647563742e626f61726400000000726f2e6275696c642e696400726f2e6275696c642e74797065000000726f2e6275696c642e76657273696f6e2e72656c656173650000000000000000726f2e6275696c642e76657273696f6e2e73646b000000000000000000000000726f2e6275696c642e646174652e7574630000000000000000000000000000002f73797374656d2f6275696c642e70726f700000000000000000000000000000616e64726f69642e7065726d697373696f6e2e524541445f50484f4e455f53544154450000000000000000000000000054454c4550484f4e595f5345525649434500000050484f4e455f545950455f47534d000050484f4e455f545950455f43444d410047505253000000004544474500000000554d54530000000043444d4100000000554e4b4e4f574e004556444f5f3000004556444f5f41000031785254540000004853445041000000485355504100000048535041000000004944454e000000004556444f5f4200004c5445004548525044000000485350415000000047534d0054445f5343444d410000000049574c414e0000004c54455f43410000636f6d2f78756e6d656e672f70696e64756f64756f2f7365637572652f4555006761640000000000000000000000000028294c6a6176612f6c616e672f537472696e673b000000004743432056455253494f4e002f70726f632f76657273696f6e000000720000004c494e55582056455253494f4e00000047434300534d5020505245454d5054000000000000000000616e64726f69642f6f732f44656275670000000000000000000000000000000069734465627567676572436f6e6e65637465640028295a002f70726f632f25642f73746174757300720000005472616365725069640000000000000000000000eb53392bf3a72cc50da09691b3af3254aeba0000000000000000000000000000e2de145c9796785559550fa5914c04a7bdbc6faeff77364e5c00000000000000dcbba23814433ec1266d6736800e96fae6e56730000000000000000000000000dea21b80aa9a01510ff50aa4e4b016a3bc65e86dc7a0aeeb00000000000000007a8d022194d2e1dd41a35813aba7ce4695639d03119205af856a412611b7f8550c1a0000000000000000000000000000246401dae8f531a3d39ca6ed9adf4b6154a3b3613dbd64f0acda484b81b9b5b25e053937a46a000059f919c9ecdb2f21edf57ba6e17fc20000e762467e00000073f0ed6230062fa700000000000000007e9a30781355d6f2401bb7db894393d7acde902e438e00000000000000000000c08215f3d0d28740059132f9edc2275a30b691e81fe69bb90188a03a5c8dc62efec44f0024af7558024b56c50f65b437015aca4460d06346ca85d6840af163313bd4290000000000000000000000000027e4647414138eac573db58476981b04c8a66ee798f843000000000000000000778f831408e617d61a6eed0c769cc4a4e93cfd2ab1000000282956000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000006a6176612f6c616e672f5468726f7761626c65003c696e69743e00000000000064652e726f62762e616e64726f69642e78706f73656400000000000000000000636f6d2e73617572696b2e73756273747261746500000000676574537461636b5472616365000000000000000000000028295b4c6a6176612f6c616e672f537461636b5472616365456c656d656e743b000000000000000000000000000000006a6176612f6c616e672f537461636b5472616365456c656d656e7400676574436c6173734e616d6500000000000000006a6176612f7574696c2f55554944000072616e646f6d5555494400000000000028294c6a6176612f7574696c2f555549443b0000746f537472696e670000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000006a6176612f6c616e672f537472696e67000000007265706c616365416c6c0000284c6a6176612f6c616e672f537472696e673b4c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f537472696e673b000000002d0000007365637572655f736861726564707265666572656e63655f69640000000000007365637572655f6c6f63616c5f73657175656e63655f696400000000eb8e78023e250000201d000047bd0000000000007ec516d1a83d7b7657b3bc164df1e59d175abc543d65c79ad61f7eed58a20a4829000000a079cbd93c0fc293313c14000f04cc00a8ef5ba461ca706d2dddabfbd5bbc24d72656164537472696e67000072656379636c6500000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f636f6e74656e742f436f6e7465787400000000000000000054454c4550484f4e595f534552564943450000000000000000000000000000004c6a6176612f6c616e672f537472696e673b0000000000000000000000000000636f6d2f78756e6d656e672f70696e64756f64756f2f736572766963655f686f6f6b2f53797374656d53657276696365486f6f6b65720000676574536572766963650000000000000000000000000000616e64726f69642f6f732f50617263656c0000006f627461696e00000000000028294c616e64726f69642f6f732f50617263656c3b00000000000000000000007772697465496e74657266616365546f6b656e00000000000000000000000000284c6a6176612f6c616e672f537472696e673b29560000007772697465537472696e6700000000000000000000000000284c6a6176612f6c616e672f537472696e673b294c616e64726f69642f6f732f4942696e6465723b0000000072656164457863657074696f6e00000028295600616e64726f69642f6f732f536572766963654d616e6167657200000000000000636f6d2f616e64726f69642f696e7465726e616c2f74656c6570686f6e792f4954656c6570686f6e79245374756200006765745061636b6167654e616d6500006173496e746572666163650000000000284c616e64726f69642f6f732f4942696e6465723b294c636f6d2f616e64726f69642f696e7465726e616c2f74656c6570686f6e792f4954656c6570686f6e793b0000000000000000000000000000006a6176612f6c616e672f4f626a65637400000000676574436c6173730000000028294c6a6176612f6c616e672f436c6173733b00676574446576696365496400284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f537472696e673b00000000000000000000676574496e7465726661636544657363726970746f7200006a6176612f6c616e672f436c617373000000000000000000676574456e636c6f73696e67436c6173730000000000000000000000000000005452414e53414354494f4e5f676574446576696365496400490000000000000028494c616e64726f69642f6f732f50617263656c3b4c616e64726f69642f6f732f50617263656c3b49295a0000000000616e64726f69642f6f732f4942696e64657200007472616e736163740000000057494e444f575f5345525649434500003c696e69743e00006765744d6574726963730000282956000000000000000000616e64726f69642f7574696c2f446973706c61794d6574726963730000000000616e64726f69642f636f6e74656e742f7265732f5265736f7572636573000000676574446973706c61794d65747269637300000000000000000000000000000028294c616e64726f69642f7574696c2f446973706c61794d6574726963733b00616e64726f69642f766965772f57696e646f774d616e6167657200000000000067657444656661756c74446973706c617900000000000000000000000000000028294c616e64726f69642f766965772f446973706c61793b0000000000000000616e64726f69642f766965772f446973706c6179000000006765745265616c4d65747269637300000000000000000000284c616e64726f69642f7574696c2f446973706c61794d6574726963733b29560000000064656e7369747944706900007769647468506978656c7300686569676874506978656c73000000000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000006a6176612f6c616e672f537472696e67000000000000000000000000000000006a6176612f7574696c2f456e756d65726174696f6e0000006861734d6f7265456c656d656e74730028295a006e657874456c656d656e7400000000000000000028294c6a6176612f6c616e672f4f626a6563743b000000000000000000000000676574486172647761726541646472657373000028295b4200000000574946495f534552564943450000000000000000616e64726f69642f6e65742f776966692f576966694d616e6167657200000000676574436f6e6e656374696f6e496e666f00000000000000000000000000000028294c616e64726f69642f6e65742f776966692f57696669496e666f3b000000616e64726f69642f6e65742f776966692f57696669496e666f0000006765744d616341646472657373000000000000006765744e6574776f726b496e746572666163657300000000000000000000000028294c6a6176612f7574696c2f456e756d65726174696f6e3b00000000000000253032583a253032583a253032583a253032583a253032583a253032580000006765744e616d6500776c616e300000006a6176612f6e65742f4e6574776f726b496e74657266616365000000746f5570706572436173650000000000000000007365637572655f6d61635f616464726573730000746f537472696e670000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000004c6a6176612f6c616e672f537472696e673b0000000000000000000000000000616e64726f69642f6f732f42756e646c650000006b6579536574000000000000284c6a6176612f6c616e672f537472696e673b29560000000000000000000000616e64726f69642f636f6e74656e742f496e74656e7446696c746572000000003c696e69743e000000000000000000006a6176612f6c616e672f4f626a656374000000006a6176612f7574696c2f536574000000746f4172726179000000000028295b4c6a6176612f6c616e672f4f626a6563743b000000000000000000000028294c6a6176612f7574696c2f5365743b00000025732c25732c000000000000616e64726f69642f636f6e74656e742f496e74656e7400000000000000000000414354494f4e5f424154544552595f4348414e474544000067657445787472617300000000000000000000000000000028294c616e64726f69642f6f732f42756e646c653b0000006765740000000000284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f4f626a6563743b000025732c2c0000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000636f6d2f78756e6d656e672f70696e64756f64756f2f7365637572652f4555006761640000000000000000000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000006a6176612f6c616e672f537472696e6700000000720000002f70726f632f637075696e666f0000004861726477617265000000007472696d00000000726f2e6275696c642e6964000000000000000000726f2e6275696c642e66696e6765727072696e74000000000000000000000000726f2e6275696c642e636861726163746572697374696373000000006c61737455706461746554696d65000000000000616e64726f69642f636f6e74656e742f706d2f5061636b6167654d616e616765720000006765745061636b616765496e666f0000000000000000000000000000284c6a6176612f6c616e672f537472696e673b49294c616e64726f69642f636f6e74656e742f706d2f5061636b616765496e666f3b0000000000000000000000616e64726f69642f636f6e74656e742f706d2f5061636b616765496e666f00006669727374496e7374616c6c54696d65000000004a000000000000000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000006a6176612f7574696c2f456e756d65726174696f6e0000006861734d6f7265456c656d656e74730028295a006e657874456c656d656e7400000000000000000028294c6a6176612f6c616e672f4f626a6563743b0000000000000000000000006765744e6574776f726b496e746572666163657300000000000000000000000028294c6a6176612f7574696c2f456e756d65726174696f6e3b0000006765744e616d65000000000000000000000000006a6176612f6e65742f4e6574776f726b496e746572666163650000006574683000000000657468310000000025733d257300000025733d257300000025733d00fc3004796ec5ccac71ff573ebbba4679b2d706e313c4ba4bcc1a37023849bfcff6a6a5b634bbe384925f067a076dde15e592cefabfaae924d04782d21b6864ce7b53dd0f9956772b688e5413f5ea13fe2eea2a26fcbde3e20476eb08228b26296d924b6e68abbca9edff25e94fd3cf30f50ed3930554150a5815eb1dd0de69327c808c5fa45563fd02c8a389d5ba8ae8deeb8fd38945d25e5c4f42db792aff66b573273603c5e9c008a5c9987ca76d0e32d99365248948cc69d953e0da8a79e89a31c790508069ff94366a32f93d98d95d39954bbe74925650ef0f6861eb80511247eb767e9402b5438ac45173ac03d6a2db9be1e21063f7688301274a8296f599a2c1c3e3c61cb7dac9709e23e06d24275818474fc1da78a67a9525ada5f71abf6134ef8577acb62858ae3e05e9daa5cc22c5737d7652e40325ce4e8389ec640451e114710e0d6b2469c8b37199be0a667f943133be6d02e8aa0b315060cb987e353e3f08b9342e0f8e4df9386918bdf36a507b22dc1dc9d92a5a15cc7ad2e9623ce4d2db150e4430ae5edd232685aecf4a1f3a83918a4ae0aff89dcc7efc28bead60edf0585f02cd87baae5f76764d0991b30eee7b2036e6b1a2d6fc9639315f0340d6558d48a888d30b6ff75d2455763864acf547523219eb9c08b63bc1b0cfddcb768f2bf792dd61477ea79715f710c220c7a9f3c4b11132c99cb4f32e9f2d707a395995e13899d06099b0a8a2b69388ed996c55c3ce61fa1b59675ecfbf783ca9a4dd19689daa8fd01c0c937f22eded9ef50bb0567436a0cfe2a77ec0812d0000000000000000000000000000008e64f2e11d0000000000000000000000b9ee22ada2e66c2dc07a6fcfbf3d463b070f9c72c500556af6a367fc592042582c430000000000000000000000000000f5bc7aafdf5f6670fccb75fb86acc2a3043c4fb0448dd1b8002f5e0f50c821d1afd1497d40ae7af095f307da6a0000000c4ae025a74716226a3bd104e6be15c400c51b1f27b34d52b17436e89cc7663928cce1323a84e2676c00000011200720d667166f5e1a00000000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e636573000000676574537472696e67000000284c6a6176612f6c616e672f537472696e673b4c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f537472696e673b0000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f72000000002829560065646974000000000000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e63657300000000000000000000000000000028294c616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f723b000000006170706c79000000707574537472696e67000000000000000000000000000000284c6a6176612f6c616e672f537472696e673b4c6a6176612f6c616e672f537472696e673b294c616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f723b00000000000000000000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e636573000000676574496e74000000000000284c6a6176612f6c616e672f537472696e673b49294900000000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f72000000002829560065646974000000000000000000000000616e64726f69642f636f6e74656e742f536861726564507265666572656e63657300000000000000000000000000000028294c616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f723b000000006170706c79000000707574496e740000284c6a6176612f6c616e672f537472696e673b49294c616e64726f69642f636f6e74656e742f536861726564507265666572656e63657324456469746f723b000000000000000000000000000000000011e480402e2c0ff6be088845eab6ea205dbd00956a09dc38b097b157dd56c91fdd8b7600000000000000000000000000196cf8e9ef29c0326d695ae06cbcf0303d91ee2df7000000000000000000000078e679d5e0c58960f697ec0c7357453abfc556b4f88283db1e140cbedf9a71b2ccaa620000000000000000000000000072abffb713d620d334a9418f3470e1db842a6164aa0e688e42c7013663b729f64ce4d10065a9495200000000000000002ca3a230b4b3cb7096b3181c13eeabff215610bd3a41f8caea27df80b31b7823e5149c002b18eb158d2f357ee13a7c14e5adee85240000000000000000000000bdccc51c4ff4a8c88fb9ef7ed6de4bc681fbbcc3d0f1e895868e90d4b3db7a6c5f8b13000000000000000000000000004242e9e8cb663bb4e4212c26bef9120131409b720f0000000000000000000000775b2f30ed7335904fbfe8eb25e9ebab71f335c207ad6a369d9cd98a7d9428048096dc00f3cbd75142ae48ad571b7a23deec8d00000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200002829490067657453696d53746174650000000000000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e61676572000000000000000000000000000067657453696d4f70657261746f724e616d65000000000000000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e61676572000000000000000000000000000067657453696d436f756e74727949736f00000000000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e616765720000282949006765744e6574776f726b547970650000000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000006765744e6574776f726b4f70657261746f72000000000000000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000006765744e6574776f726b4f70657261746f724e616d650000000000000000000028294c6a6176612f6c616e672f537472696e673b000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000006765744e6574776f726b436f756e74727949736f000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e61676572000028294900676574446174614163746976697479000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200002829490067657444617461537461746500000000000000000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000004c6a6176612f6c616e672f537472696e673b0000726f2e73657269616c6e6f00616e64726f69642f6f732f4275696c640000000053455249414c000067657453657269616c000000756e6b6e6f776e00616e64726f69642f6f732f4275696c64000000004d414e554641435455524552000000007265616c6d6500006f70706f000000002d5f2e217e2a2728290000002d5f2e217e2a27282900000025642e25642e25642e2564001f4799935ccc1a5c49709163000000000000000000000000fb8a444fa6ac0000282956007772697465000000285b422956000000746f4279746541727261790028295b42000000006a6176612f696f2f4279746541727261794f757470757453747265616d0000006a6176612f7574696c2f7a69702f475a49504f757470757453747265616d00003c696e69743e00000000000000000000284c6a6176612f696f2f4f757470757453747265616d3b295600000066696e6973680000636c6f736500000000000000720000000000000000000000000000007374617420202f73746f726167652f656d756c61746564004163636573733a20000000004d6f646966793a20000000007200000049443a2000000000496e6f6465733a0000000000000000000000000073746174202d66202f73746f726167652f656d756c61746564000000426c6f636b733a007200000049443a2000000000496e6f6465733a00000000000000000073746174202d66202f73797374656d2f65746300426c6f636b733a0072000000636174202f70726f632f7379732f6b65726e656c2f72616e646f6d2f626f6f745f696400720000004163636573733a200000000000000000000000000000000073746174202f646174612f73797374656d2f75736572732f300000000000000025733b25733b25733b25733b25733b25733b25733b25733b25733b25733b00006765745f6469645f696e666f206c656e2825642c25642c25642c25642c25642c25642c25642c25642c25642c2564292025640000000000000000000000000000504154480000000025732f73750000003a0000000000000000000000000000002f73797374656d2f6c69622f6c6962726972752e736f000000000000000000002f73797374656d2f6c696236342f6c6962726972752e736f00000000000000002f70726f632f25642f6d61705f66696c65732f0025732573000000002f646174612f6d6973632f7269727500000000007379732e6f656d5f756e6c6f636b5f616c6c6f77656400003100000000000000635f636d646c696e65000000635f656e7669726f6e0000000000000000000000656e765f696e666f3a6e6174697665476574537973496e666f207374617274007064645f7365637572655f6e617469766500000000000000000000000000000028294c6a6176612f6c616e672f537472696e673b0000000000000000000000002f70726f632f73656c662f636d646c696e6500000000000000000000000000006f72672f6a736f6e2f4a534f4e4f626a65637400707574000000000000000000284c6a6176612f6c616e672f537472696e673b4c6a6176612f6c616e672f4f626a6563743b294c6f72672f6a736f6e2f4a534f4e4f626a6563743b00635f6664000000000000000000000000000000002f70726f632f73656c662f656e7669726f6e0000635f6d6170730000746f537472696e670000000000000000000000006a6176612f6c616e672f537472696e670000000067657442797465730000000028295b42000000002f70726f632f73656c662f6d61707300720000002e736f002e6a6172000000002e646578000000002e7a6970000000002e61706b000000003130f96698ddda701fa8a0e3e288025b720000002f70726f632f73656c662f66640000002e0000002e2e0000000000002f70726f632f73656c662f66642f2573000000002573202d3e202573000000003b3f438bf08cef3ecf88f54c56acef47d6e07131071237e3c74162611383af409fe95e000000000000000000000000003074072451bc526c81ef2692ecd4496bd4f386000000000000000000000000001ed5841e3fb896219b92b787489fdd70b85cfb9d15b864bcff821fbba65a9dc552f409000000000000000000000000005d78bbeb4bc44f70fde4a6b6aed612cf126c7c2da96e00000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000004c6a6176612f6c616e672f537472696e673b0000676574496d65690000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000676574496d656900616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000676574496d656900616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000004c6a6176612f6c616e672f537472696e673b0000676574446576696365496400616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000676574446576696365496400000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000676574446576696365496400000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000004c6a6176612f6c616e672f537472696e673b00006765744d6569640000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b0000006765744d65696400616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b0000006765744d65696400616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000004c6a6176612f6c616e672f537472696e673b000067657453756273637269626572496400000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000676574537562736372696265724964000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b00000067657453756273637269626572496400000000000000000067657453696d53657269616c4e756d6265720000000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000004c6a6176612f6c616e672f537472696e673b000000000000000000000000000067657453696d53657269616c4e756d6265720000000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000000000000000000067657453696d53657269616c4e756d6265720000000000000000000000000000616e64726f69642f74656c6570686f6e792f54656c6570686f6e794d616e6167657200000000000000000000000000002849294c6a6176612f6c616e672f537472696e673b000000000000000000000076891a88d8eff8e4177f356255c95cb5e47e5d9f277819e5c8efdd32b9e40bd461b7bbd9076e5926c581768138b88dba0d90d5deabb405c607656bc2f094ddf32bdfa5a9a79a3044339c373423c824b09c9e01747877fedff4a23033f5b08ff61df202343bbf9b90346bf2b6600d84b207dcd4a0627b4a21d9ab38608f23fd5814224faacc8fe6be4f7a2d09cba7c4367b7d6eadd7c37fa16d81b6c4821ce9f0bc705d35b9435e6fae79fb014acf9b80fba1247458045d414e3f466f270e79057b115e0b4ca91deb5c351381ddb5f77358f208c11b0970f1ab07df7664bffd5d50900e061eae8230060c477e589624e4aafe5474d61dba32112487405027cc850e94686dedbea128ee7425fe6d752190994c63399ca5408c3c78a2c5522ca21be4b9186c798d281c3d002cefd0f5d84d252b1c94d34c5c834e381445624ccc0ce2eadaa74721718892cb505996d459175a6f8cadf09f3dbd795ea984a1da57dccc5743772a4d294e63d0ce09bcc08a69778956a6a6bcf46b6f72b09e383ef56e8df2231db6b8f1daf0ee431cfb38d58c1ac153be4691cd2f489e22f06b4b4b1b1a85271452ca9ba9606104d58c8d422e371324c5f46716394b2b3b38b3fbd8b76ae85535ae13f723c5fa74c83f4c692240978518b6ad65e79796c8d341be58f08777ea204ed37b7977e97f357fb3d1840c77d580a3ed44363677a68f2d72ade493c84a4c6b3a4718df2c891d110913893ecad20286433247c8a7472f06c57cd9132c7ceb26955d3d6b519c54f529393026c359bb66cefe4344cbc5d4253cbeff6c188dee0221e554ed148a2a14dac8e20129216e58824afb44ee20cef041125c8bce1d2de84c9eb63b360c2c725b56e6037eee6ec1a37103bb6e52c1743e80d5b74bf8ff10cdb660e82dba6a892a078ad17b982be40b0d93ae895874b38ea71fb49fae9721fc4405f5c5fe9cc8ee9e05781c6e7c222bd996b34dceb8a73c89efc776580480cb3bf047fa0e7b2812a05707082cf2cf751894966136d375ab4b91d85b7dcc8d9a606bf4d742eee258e761b7682620761e43d87da9ede4f647fce9637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b27509832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cfd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdbe0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9ee1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b27509832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cfd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdbe0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9ee1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb1652096ad53036a538bf40a39e81f3d7fb7ce339829b2fff87348e4344c4dee9cb547b9432a6c2233dee4c950b42fac34e082ea16628d924b2765ba2496d8bd12572f8f66486689816d4a45ccc5d65b6926c704850fdedb9da5e154657a78d9d8490d8ab008cbcd30af7e45805b8b34506d02c1e8fca3f0f02c1afbd0301138a6b3a9111414f67dcea97f2cfcef0b4e67396ac7422e7ad3585e2f937e81c75df6e47f11a711d29c5896fb7620eaa18be1bfc563e4bc6d279209adbc0fe78cd5af41fdda8338807c731b11210592780ec5f60517fa919b54a0d2de57a9f93c99cefa0e03b4dae2af5b0c8ebbb3c83539961172b047eba77d626e169146355210c7d712c55626db8a305e5addf55e3491d6a9968f0d71f41a65316580d24fb5fbc134fb76d6e4d17535a60a78569bedb0e07a31dba40124eaf78be1e83cdae1c96bc94000000000000000000000000000000f41ff81efd3cb48949239aad81492a40b1d09959b49c58897b29674dcc594e64688311ea507f0e7d16db9185f7ac9f4ea3abd7d17ce224e29d98b797c3216bb3e1000000000000000000000000000000e5320190271694f6c03358a9827feb0b50a640e4edfa3fbc4f43a178245ce96f60daee0684ebb4d55efd3ee3081260e675d62e073f05dc3bb998d94c3256fa7b9a0000000000000000000000000000004142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392b2f00000000000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000100000001000000f0de03b4730000000100000001000000086203b47300000001000000010000000c6203b4730000000100000001000000106203b4730000000100000001000000146203b4730000000100000001000000186203b47300000001000000010000001c6203b4730000000100000001000000206203b4730000000100000001000000246203b4730000000100000001000000286203b47300000001000000010000002c6203b473000000"
barr = bytes.fromhex(hex_string)
idaapi.patch_bytes(0x60000, barr)
查看 data 段的 Hex View
确实覆盖了大部分内容,出现了不少明文。大家应该还记得,在前面解密和 Patch 单个函数时,我们手动转化和识别了字符串。但现在我们一次性处理了这么多明文,逐个识别实在太麻烦了,需要更好的办法。我们需要 IDA 做重新分析,或至少对 data 段做重新分析,使其自动识别这些字符串。
在 IDA 中如何做大范围的重新分析?可以在 Options 菜单栏选择 General - Analyse 界面,点击 Reanalyse program 并确定,也可以在 IDA 左下角 状态栏右键,同样选择 Reanalyse program 。
但只通过 “重新分析”没法让 IDA 识别出字符串。猜测是因为 IDA 重新分析后,认为 byte 类型就挺对。
假如我们选中 0x60010 - 0x6002C,右键 undefined 或用快捷键 U,使其回归未定位的状态,或者严谨一些说,“取消 IDA 对它的数据类型判断”。
然后再 Reanalyse program ,发现正常识别出了字符串
.data:0000000000060010 aLandroidConten_3 DCB "(Landroid/content/Context;)Z",0
因此如果能将 data 段转化为尚未被分析的状态,再结合重新分析,就可以识别出大片字符串。del_items
就是这样的 API,可以将从 startAddress 到 endAddress 范围内的数据和指令都转化为尚未分析的状态。参数 2 默认为 0 ,不用深究。
ida_bytes.del_items(startAddress, 0, endAddress)
Reanalyse program 也可以用 API 实现,而且可以指定范围。
ida_auto.plan_and_wait(startAddress, endAddress)
结合在一起,我们可以对整个 data 段进行重新分析
import ida_auto
import ida_bytes
import idautils
for i in idautils.Segments():
seg = idaapi.getseg(i)
segName = idaapi.get_segm_name(seg)
if "data" in segName:
startAddress = seg.start_ea
endAddress = seg.end_ea
ida_bytes.del_items(startAddress, 0, endAddress)
ida_auto.plan_and_wait(startAddress, endAddress)
运行结果如下,字符串全部被良好识别。
需要注意,让 IDA 彻底重新分析的办法不止这一种,另一个方便的办法是将 IDA 中所作的 patch 保存回原文件,然后重新打开。操作如下,Edit - Patch program - Apply patches to input file,选择保存回原文件。然后重新打开 ,数据已经被良好呈现出来了。
关于这一小节所阐述的 dump 法,需要理解三点。
首先,我们发现一些字符串未被解密,这可能因为 SO 中的各种函数没有被充分运行,存在于函数内部的字符串尚未经过解密操作,dump 下来的依然是密文状态,前文已说过这一点。让 App 充分运行后再 dump,依然可能部分密文未解密,因此某些逻辑可能一般操作真执行不到,动态分析就一定存在代码覆盖率的问题,很难避免。
其次,在上文我们也说过,data 中除了加密字符串以外的数据会被误伤,甚至被篡改成运行时错误的值,这个问题有时候会很严重和糟糕,比如像下面这样,但在我们这个样例里,似乎影响不算特别大。
最后,dump 与回填不一定要依赖 IDA 和 Frida 。dump 也可以使用 ADB 的 dd 命令、IDA 的动态调试、GG 修改器、GDB/LLDB 等等。但如果不存在 Anti Frida,那么 Frida dump 就是最方便的选择。回填使用 IDA 脚本更不是唯一选择,只需要使得 dump 下来的内容覆盖原先 data 段的物理地址范围就行。需要注意区分物理偏移和虚拟地址,IDA 解析和展示 SO 时,采用虚拟地址(address),而处理静态文件时,需要基于实际偏移 offset 。以 data segment 的起始地址为例,其虚拟地址和实际物理偏移并不一定相同。IDA 中 patch 遵照其虚拟地址即可,因为 IDA 会替我们处理,映射到合适的物理地址上,而将 SO 作为二进制文件 patch 时,需要用实际物理地址。可以使用 readelf 查看详细的节信息。
C:\Users\13352>readelf -S "C:FileStorage\File\libpdd_secure.so"
There are 25 section headers, starting at offset 0x63420:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.bu[...] NOTE 0000000000000200 00000200
0000000000000024 0000000000000000 A 0 0 4
[ 2] .hash HASH 0000000000000228 00000228
000000000000029c 0000000000000004 A 4 0 8
[ 3] .gnu.hash GNU_HASH 00000000000004c8 000004c8
00000000000000f8 0000000000000000 A 4 0 8
[ 4] .dynsym DYNSYM 00000000000005c0 000005c0
0000000000000930 0000000000000018 A 5 3 8
[ 5] .dynstr STRTAB 0000000000000ef0 00000ef0
0000000000000879 0000000000000000 A 0 0 1
[ 6] .gnu.version VERSYM 000000000000176a 0000176a
00000000000000c4 0000000000000002 A 4 0 2
[ 7] .gnu.version_r VERNEED 0000000000001830 00001830
0000000000000040 0000000000000000 A 5 2 8
[ 8] .rela.dyn RELA 0000000000001870 00001870
00000000000006c0 0000000000000018 A 4 0 8
[ 9] .rela.plt RELA 0000000000001f30 00001f30
00000000000005a0 0000000000000018 AI 4 20 8
[10] .plt PROGBITS 00000000000024d0 000024d0
00000000000003e0 0000000000000010 AX 0 0 16
[11] .text PROGBITS 00000000000028b0 000028b0
0000000000058ea4 0000000000000000 AX 0 0 4
[12] .rodata PROGBITS 000000000005b760 0005b760
0000000000000874 0000000000000000 A 0 0 16
[13] .eh_frame_hdr PROGBITS 000000000005bfd4 0005bfd4
000000000000061c 0000000000000000 A 0 0 4
[14] .eh_frame PROGBITS 000000000005c5f0 0005c5f0
0000000000002110 0000000000000000 A 0 0 8
[15] .note.androi[...] NOTE 000000000005e700 0005e700
0000000000000098 0000000000000000 A 0 0 4
[16] .init_array INIT_ARRAY 000000000005fa70 0005ea70
00000000000000b0 0000000000000008 WA 0 0 8
[17] .fini_array FINI_ARRAY 000000000005fb20 0005eb20
0000000000000010 0000000000000008 WA 0 0 8
[18] .data.rel.ro PROGBITS 000000000005fb30 0005eb30
00000000000000a0 0000000000000000 WA 0 0 8
[19] .dynamic DYNAMIC 000000000005fbd0 0005ebd0
0000000000000220 0000000000000010 WA 5 0 8
[20] .got PROGBITS 000000000005fdf0 0005edf0
0000000000000210 0000000000000008 WA 0 0 8
[21] .data PROGBITS 0000000000060000 0005f000
00000000000042e0 0000000000000000 WA 0 0 16
[22] .bss NOBITS 00000000000642e0 000632e0
0000000000008450 0000000000000000 WA 0 0 16
[23] .comment PROGBITS 0000000000000000 000632e0
0000000000000054 0000000000000001 MS 0 0 1
[24] .shstrtab STRTAB 0000000000000000 00063334
00000000000000e9 0000000000000000 0 0 1
data 段的 address 是 0x60000,offset 是 0x5f000。可以使用 010Editor 之类的十六进制查看工具验证,文件中 从 0x5f000 开始的地址与 IDA 中从 0x60000 开始的数据一致。为了避免此类麻烦,我选择用 IDA 直接 patch。
3.2 样本二
文件zuiyou.latest.pc-guanwang.zip
打开 net_crypto ,有了之前的经验,我们直接来到 data 段
IDA 的自动注释显示,这些字符都被 decodexxx 函数引用,交叉引用跳过去看看。
我们看到十数个解密函数。请读者观察一下,可以发现,它也是原地解密,因此也可以 dump 回填。除此之外,decode 函数都位于 init_array 中。
这意味着,只要在 init_array 运行结束后的任意时机做 dump,字符串全部出于解密状态。这显然比上一个样本更好处理,不用担心代码覆盖率的问题,感兴趣的朋友可以拿它做 dump 回填的训练。
这里我们讨论一个有趣的话题,你认为哪种代码保护方案更好?
单从保护字符串的角度看,在运行的最开始时机对全体字符串做解密,称不上什么好的保护方案,很容易 dump 回填。而在字符串被使用的具体函数入口做解密,似乎更难 dump 更难一些,需要考虑函数是否被执行,也就是代码覆盖率的问题。
从开发的角度看,有 LLVM 处理经验读者应该清楚,上面两个样本用的是 OLLVM 的变种 Armariris 和 hikari。在字符串被使用的函数入口做解密,在逻辑上比在初始化统一解密复杂很多。
因此似乎在函数入口做加密是更复杂,也更有效的方案?但事实并非如此。
从逆向的角度看,在起始处对所有字符串做解密,那么字符串解密逻辑与字符串使用逻辑之间的关系相对松散,而在每个函数内部对所用字符串做解密,字符串解密和字符串使用逻辑的关系十分紧密。这至少带来两个问题
一是如果我们查看字符串解密后的明文清单,其中某一个字符串吸引了我们的注意,比如 /proc/self/maps ,我们认为它可能和我们关心的环境检测逻辑相关。那么通过任意某种方式确认这个字符串解密的发生点,它同时也是 /proc/self/maps 字符串的使用点。
二是字符串的解密顺序就是函数的具体执行顺序,对于上面的样本而言,解密通过内联代码片段完成,如果是通过函数完成,那么 hook 这个函数并打印堆栈,它就能反应样本的函数执行流,某种程度上是执行流 trace 。
在 SO 加载起始处对所有字符串做解密,就不存在这样的担忧。因此我们要说,局部最优的保护方案,整体上并不一定最优。安全防护既要打好组合拳,也要具有大局观。
四、hook 回填
4.1 样本一
样本:mt.7z
在之前的函数分析一节,我们通过被引用引用最多的函数
import idautils
from operator import itemgetter
import idc
functionList = []
for func in idautils.Functions():
xrefs = idautils.CodeRefsTo(func, 0)
xrefCount = len(list(xrefs))
oneFuncDict = {"funcName":idc.get_func_name(func), "Address": hex(func), "xrefCount": xrefCount}
functionList.append(oneFuncDict)
function_list_by_countNum = sorted(functionList, key=itemgetter('xrefCount'),reverse=True)
for func in function_list_by_countNum[:10]:
print(func)
在最高频被引用的函数中,分析到了 SO 的字符串解密函数。
_BYTE *__fastcall sub_5124(_BYTE *result, _BYTE *a2, int a3, int a4)
{
if ( a4 >= 1 )
{
do
{
*result = *a2 ^ a3;
--a4;
++result;
++a2;
a3 += 3;
}
while ( a4 );
}
return result;
}
上一个案例不存在字符串解密函数,或者说,它的解密操作内联在函数内,这个案例则是通过函数实现。
参数 1 用于存放解密后的字符串,参数 2 是密文字符串,参数 3 是 Key ,参数 4 是字符串长度。解密逻辑是简单的和 Key 做异或,key 每次运算后自增 3 。
我们可以 Hook 这个函数,验证我们的判断是否正确。使用 showfridacode 可以很方便的生成hook代码
function hookInit(){
var linkername;
var alreadyHook = false;
var call_constructor_addr = null;
var arch = Process.arch;
if (arch.endsWith("arm")) {
linkername = "linker";
} else {
linkername = "linker64";
}
var symbols = Module.enumerateSymbolsSync(linkername);
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("call_constructor") !== -1) {
call_constructor_addr = symbol.address;
}
}
if (call_constructor_addr.compare(NULL) > 0) {
console.log("get construct address");
Interceptor.attach(call_constructor_addr, {
onEnter: function (args) {
if(alreadyHook === false){
const targetModule = Process.findModuleByName("libmtguard.so");
if (targetModule !== null) {
alreadyHook = true;
inittodo();
}
}
}
});
}
}
function inittodo(){
hook_sub_5124();
}
function hook_sub_5124(){
var base_addr = Module.findBaseAddress("libmtguard.so");
Interceptor.attach(base_addr.add(0x5125), {
onEnter(args) {
console.log("call sub_5124");
console.log("arg0:"+args[0]);
console.log("arg1:"+args[1]);
console.log("arg2:"+args[2]);
console.log("arg3:"+args[3]);
},
onLeave(retval) {
console.log(retval);
console.log("leave sub_5124");
}
});
}
// 执行 hookinit
hookInit();
对 hook_sub_5124 稍加修改,使其更符合我们的需求。
function hook_sub_5124(){
var index = 0
var base_addr = Module.findBaseAddress("libmtguard.so");
console.log("enter")
Interceptor.attach(base_addr.add(0x5125), {
onEnter(args) {
index += 1;
this.buffer = args[0];
},
onLeave() {
console.log("index:"+ index + " decryptStr :"+this.buffer.readCString())
}
});
}
运行脚本 frida -U -f com.sankuai.meituan -l script.js --no-pause。尽可能多的点击、触发各种逻辑,函数打印出 1600 条左右解密字符串。查看 sub_5124 这个函数的交叉引用
应该说极大多数字符串都得到了解密,效果很好。可是动态分析的结果,如何作用于静态呢?dump 回填法中可以进行直接的内存覆盖,那么这个样本呢?它典型使用如下
sub_5124(byte_D7735, byte_D7A6A, 19, 3);
参数 1 用于存放解密后的字符串,参数 2 是密文字符串,可以发现,解密并非原地解密,而是置于 SO 中另一块区域。那么我们将解密后的明文 patch 覆盖参数 1 指向的解密后字符串不就行了?下面开干。
首先修改 hook_sub_5124 函数,我们需要其打印如下内容备用
- 参数1,即解密字符串的存放地址
- 调用地址
- 解密字符串长度
function hook_sub_5124(){
var index = 0
var base_addr = Module.findBaseAddress("libmtguard.so");
console.log("enter")
Interceptor.attach(base_addr.add(0x5125), {
onEnter(args) {
index += 1;
this.buffer = args[0];
this.length = args[3];
this.LR = this.context.lr - base_addr - 5;
},
onLeave() {
var decryptAddress = this.buffer - base_addr;
console.log("index:"+ index + " $&$ decryptAddress:" + decryptAddress.toString(16) + " $&$ length:" +
this.length + " $&$ decryptStr:"+toHex(this.buffer.readCString()) + " $&$ callAddress:"+this.LR.toString(16))
}
});
}
function toHex(str) {
var result = '';
for (var i=0; i<str.length; i++) {
result += str.charCodeAt(i).toString(16);
}
return result;
}
文件:backup.txt
需要注意,有一些命令行工具打印输出时,输出太长会换行,这需要避免,否则后续解析会出错。除此之外,明文字符串在打印时,采用了十六进制格式,因为担心部分字符串乱码影响解析,也是防止分隔符和字符串的内容产生混淆。将 Hook 打印的全部条目拷贝到文件中,下面进入IDA 处理。
import idaapi
# 全路径,否则担心IDA找不到
with open(r"C:\xxx\backup.txt", "r")as F:
infos = F.readlines()
for info in infos:
try:
info = info.strip().split(",")
decryptAddress = int(info[1].split(":")[1],16)
decryptInfo = bytes.fromhex(info[3].split(":")[1])
idaapi.patch_bytes(decryptAddress, decryptInfo)
except:
print("parse error")
运行后开始考虑字符串尚未识别的问题
使用上文提到的重新分析 data 段的脚本
可以发现确实很多字符串被识别出来了,但眼尖的读者可能会发现,依然有一部分没能正确识别为字符串,比如下图中这个,我用注释标出来了
这是为什么呢?因为这个样本存在一定的花指令,干扰了反汇编,进而干扰了函数识别、交叉引用、反编译等高级分析过程,IDA 在重新分析时,依然没有办法自动识别出部分字符串。
前面说过,我们可以框选出某个字符串区间,然后快捷键 A,手动转字符串。
.data:000CF182 aV5DeviceInfoCo DCB "v5_device_info_collect_time_first",0
但显然一个个做太麻烦了,这种情况下可以使用create_strlit
API,它用于将一个地址范围内的数据转为字符串,参数 3 为idaapi.STRTYPE_TERMCHR
时,意味着生成 C 风格的字符串(以 \0 结尾)。
idaapi.create_strlit(address, length, flag)
别忘了我们所收集的信息里包括解密的长度 。
import idaapi
# 全路径,否则担心IDA找不到
with open(r"C:\xxx\backup.txt", "r")as F:
infos = F.readlines()
for info in infos:
try:
info = info.strip().split(",")
decryptAddress = int(info[1].split(":")[1],16)
decryptInfo = bytes.fromhex(info[3].split(":")[1])
length = int(info[2].split(":")[1],16)
idaapi.patch_bytes(decryptAddress, decryptInfo)
idaapi.create_strlit(decryptAddress, length, idaapi.STRTYPE_TERMCHR)
except:
print("parse error")
运行脚本
完全符合我们预期。下面讨论一个问题,是不是只能用回填来处理字符串解混淆?当然不是,还有其他办法,比如通过注释。IDA 提供多形式的注释可供选择。
首先是汇编界面的注释
- 重复注释
- 常规注释
常规注释在注释后只会出现在当前汇编行的尾部,重复注释会在当前行尾部以及所有引用当前行的地方出现注释。我们做解密字符串的标注,用重复模式更清晰醒目。
IDAPython 中对应 API 为 idaapi.set_cmt(ea, commentStr, flag)
,参数 1 是地址,参数 2 是注释文本,参数 3 为 flag,1 表示重复注释, 0 表示常规注释。稍微修改一下回填脚本,测试效果。
import idaapi
# 全路径,否则担心IDA找不到
with open(r"C:\xxx\backup.txt", "r")as F:
infos = F.readlines()
for info in infos:
try:
info = info.strip().split(",")
decryptAddress = int(info[1].split(":")[1],16)
decryptInfo = bytes.fromhex(info[3].split(":")[1])
idaapi.set_cmt(decryptAddress, decryptInfo.decode("UTF-8"), 1)
except:
print("parse error")
如下图所示,在密文右侧出现了对应的明文注释
数据使用处也可以看到对应注释
但从效果上说,汇编注释不如回填数据“好看”。关于注释,还有一个好玩的项目 showcomments,它可以方便的管理和查看所有的注释。
除了汇编界面的注释外,我们也可以在反编译界面写注释,或者说在伪代码上做注释,这是大家更熟悉的方式。在大体上包括下面几种类型
- 函数注释
- 块注释
- 行注释
- 孤立注释
我们这里只讨论行注释,IDA 的反编译器并没有提供根据地址直接写注释的 API(相比之下,反汇编界面的 set_cmt
API 就十分友好) ,好在我们可以用别人封装好的方法,请读者自行尝试如下函数
# loc:address
# comment:your comment
def addDecompilerComment(loc, comment):
cfunc = idaapi.decompile(loc)
eamap = cfunc.get_eamap()
decompObjAddr = eamap[loc][0].ea
tl = idaapi.treeloc_t()
tl.ea = decompObjAddr
commentSet = False
for itp in range(idaapi.ITP_SEMI, idaapi.ITP_COLON):
tl.itp = itp
cfunc.set_user_cmt(tl, comment)
cfunc.save_user_cmts()
if not cfunc.has_orphan_cmts():
commentSet = True
cfunc.save_user_cmts()
break
cfunc.del_orphan_cmts()
if not commentSet:
print("pseudo comment error at %08x" % loc)
如果想充分发挥注释的效果,可以汇编+伪代码注释都添上。
def set_comment(address, text):
## Set in dissassembly
idc.set_cmt(address, text,1)
## Set in decompiled data
set_hexrays_comment(address, text)
但在这个样本上,并不建议在伪代码界面做这些操作。在伪代码界面,并不是任意地址都可以做注释,具体原因我们后续再讲。除此之外,样本存在花指令,导致大量函数未识别或无法F5,直接在函数和伪代码层面做操作的话,效果很差。需要注意,这并不意味着我们所讨论的思路和技术不适用于较复杂的样本,而是此类样本必须先做对应的去花去混淆,才能使得字符串解密这样的后续操作效果较好。在后续的文章里,我们也会讨论如何去花。
4.2 样本二
接下来我们看一个较复杂的例子。aweme_aweGW_v1015_210701_87c2_1658857393.zip
打开 libmetasec_ml.so,找几个函数看看 ,我选择 sub_910C。
int sub_910C()
{
int v0; // r0
int v1; // r0
int v2; // r0
int v3; // r0
int v4; // r0
int v5; // r0
_DWORD *v6; // r0
int v7; // r0
int result; // r0
sub_7DF68((int)&unk_B3DA0, (char *)&unk_99AF0);
_cxa_atexit((void (__fastcall *)(void *))sub_7E06C, &unk_B3DA0, &off_B3000);
if ( !dword_B3D90 )
{
v0 = operator new[](3u);
*(_WORD *)v0 = 31575;
*(_BYTE *)(v0 + 2) = 127;
sub_77C04();
if ( !dword_B3D90 )
dword_B3D90 = v1;
}
dword_B3D80 = dword_B3D90;
if ( !dword_B3D94 )
{
v2 = operator new[](0xAu);
*(_DWORD *)v2 = -381345581;
*(_DWORD *)(v2 + 4) = -2117098656;
*(_WORD *)(v2 + 8) = 4252;
sub_7715C();
if ( !dword_B3D94 )
dword_B3D94 = v3;
}
dword_B3D84 = dword_B3D94;
if ( !dword_B3D98 )
{
v4 = operator new[](0x12u);
*(_DWORD *)v4 = -105569581;
*(_DWORD *)(v4 + 4) = -2107694256;
*(_DWORD *)(v4 + 8) = -1142931556;
*(_DWORD *)(v4 + 12) = -1676642288;
*(_WORD *)(v4 + 16) = 4756;
sub_7727C();
if ( !dword_B3D98 )
dword_B3D98 = v5;
}
dword_B3D88 = dword_B3D98;
if ( !dword_B3D9C )
{
v6 = (_DWORD *)operator new[](4u);
*v6 = -408875388;
v7 = sub_77698(v6, 4);
if ( !dword_B3D9C )
dword_B3D9C = v7;
}
result = dword_B3D9C;
dword_B3D8C = dword_B3D9C;
return result;
}
其中有四段是字符串以及其解密操作
// part1
v0 = operator new[](3u);
*(_WORD *)v0 = 31575;
*(_BYTE *)(v0 + 2) = 127;
sub_77C04();
// part2
v2 = operator new[](0xAu);
*(_DWORD *)v2 = -381345581;
*(_DWORD *)(v2 + 4) = -2117098656;
*(_WORD *)(v2 + 8) = 4252;
sub_7715C();
// part3
v4 = operator new[](0x12u);
*(_DWORD *)v4 = -105569581;
*(_DWORD *)(v4 + 4) = -2107694256;
*(_DWORD *)(v4 + 8) = -1142931556;
*(_DWORD *)(v4 + 12) = -1676642288;
*(_WORD *)(v4 + 16) = 4756;
sub_7727C();
// part4
v6 = (_DWORD *)operator new[](4u);
*v6 = -408875388;
v7 = sub_77698(v6, 4);
这所谓的字符串解密,似乎和前面的都不太一样。sub_7727C 等四个解密函数的参数和返回值没有正确识别,这一定程度影响了大家的理解。读者换个 IDA 版本试一下,或者用 Ghidra 就可以看到比较好的反编译效果,比如下面是 Ghidra 对该函数的反编译效果。(需要注意,Ghidra 在 ARM32 中默认基地址 0x10000,ARM64 中默认基地址 0x100000,因此如下 FUN_00087c04 就是 IDA 中 _77C04。)
{
undefined2 *puVar1;
int iVar2;
undefined4 *puVar3;
FUN_0008df68(&DAT_000c3da0,&DAT_000a9af0);
__cxa_atexit(FUN_0008e06c + 1,&DAT_000c3da0,&DAT_000c3000);
if (DAT_000c3d90 == 0) {
puVar1 = (undefined2 *)operator.new[](3);
*puVar1 = 0x7b57;
*(undefined *)(puVar1 + 1) = 0x7f;
iVar2 = FUN_00087c04(puVar1,3);
if (DAT_000c3d90 == 0) {
DAT_000c3d90 = iVar2;
}
}
DAT_000c3d80 = DAT_000c3d90;
if (DAT_000c3d94 == 0) {
puVar3 = (undefined4 *)operator.new[](10);
*puVar3 = 0xe94520d3;
puVar3[1] = 0x81cfa360;
*(undefined2 *)(puVar3 + 2) = 0x109c;
iVar2 = FUN_0008715c(puVar3,10);
if (DAT_000c3d94 == 0) {
DAT_000c3d94 = iVar2;
}
}
DAT_000c3d84 = DAT_000c3d94;
if (DAT_000c3d98 == 0) {
puVar3 = (undefined4 *)operator.new[](0x12);
*puVar3 = 0xf9b522d3;
puVar3[1] = 0x825f2350;
puVar3[2] = 0xbbe03f9c;
puVar3[3] = 0x9c107810;
*(undefined2 *)(puVar3 + 4) = 0x1294;
iVar2 = FUN_0008727c(puVar3,0x12);
if (DAT_000c3d98 == 0) {
DAT_000c3d98 = iVar2;
}
}
DAT_000c3d88 = DAT_000c3d98;
if (DAT_000c3d9c == 0) {
puVar3 = (undefined4 *)operator.new[](4);
*puVar3 = 0xe7a10e84;
iVar2 = FUN_00087698(puVar3,4);
if (DAT_000c3d9c == 0) {
DAT_000c3d9c = iVar2;
}
}
DAT_000c3d8c = DAT_000c3d9c;
return;
}
我们拿 part3 举例
// IDA
if ( !dword_B3D98 )
{
v4 = operator new[](0x12u);
*(_DWORD *)v4 = -105569581;
*(_DWORD *)(v4 + 4) = -2107694256;
*(_DWORD *)(v4 + 8) = -1142931556;
*(_DWORD *)(v4 + 12) = -1676642288;
*(_WORD *)(v4 + 16) = 4756;
sub_7727C();
if ( !dword_B3D98 )
dword_B3D98 = v5;
}
// Ghidra
if (DAT_000c3d98 == 0) {
puVar3 = (undefined4 *)operator.new[](0x12);
*puVar3 = 0xf9b522d3;
puVar3[1] = 0x825f2350;
puVar3[2] = 0xbbe03f9c;
puVar3[3] = 0x9c107810;
*(undefined2 *)(puVar3 + 4) = 0x1294;
iVar2 = FUN_0008727c(puVar3,0x12);
if (DAT_000c3d98 == 0) {
DAT_000c3d98 = iVar2;
}
}
sub_7727C 应该有两个参数,可以根据 Ghidra 的结果对 IDA 的函数定义做修正
四个解密函数都这么做后,看起来好多了
int sub_910C()
{
int v0; // r0
int v1; // r0
int v2; // r0
int v3; // r0
int v4; // r0
int v5; // r0
_DWORD *v6; // r0
int v7; // r0
int result; // r0
sub_7DF68((int)&unk_B3DA0, (char *)&unk_99AF0);
_cxa_atexit((void (__fastcall *)(void *))sub_7E06C, &unk_B3DA0, &off_B3000);
if ( !dword_B3D90 )
{
v0 = operator new[](3u);
*(_WORD *)v0 = 31575;
*(_BYTE *)(v0 + 2) = 127;
v1 = sub_77C04(v0, 3);
if ( !dword_B3D90 )
dword_B3D90 = v1;
}
dword_B3D80 = dword_B3D90;
if ( !dword_B3D94 )
{
v2 = operator new[](0xAu);
*(_DWORD *)v2 = -381345581;
*(_DWORD *)(v2 + 4) = -2117098656;
*(_WORD *)(v2 + 8) = 4252;
v3 = sub_7715C(v2, 10);
if ( !dword_B3D94 )
dword_B3D94 = v3;
}
dword_B3D84 = dword_B3D94;
if ( !dword_B3D98 )
{
v4 = operator new[](0x12u);
*(_DWORD *)v4 = -105569581;
*(_DWORD *)(v4 + 4) = -2107694256;
*(_DWORD *)(v4 + 8) = -1142931556;
*(_DWORD *)(v4 + 12) = -1676642288;
*(_WORD *)(v4 + 16) = 4756;
v5 = sub_7727C(v4, 18);
if ( !dword_B3D98 )
dword_B3D98 = v5;
}
dword_B3D88 = dword_B3D98;
if ( !dword_B3D9C )
{
v6 = (_DWORD *)operator new[](4u);
*v6 = -408875388;
sub_77698((int)v6, 4);
if ( !dword_B3D9C )
dword_B3D9C = v7;
}
result = dword_B3D9C;
dword_B3D8C = dword_B3D9C;
return result;
}
解密函数的参数 1 是指向密文字符串的指针,参数 2 是长度。读者可能还是对它感到不适。我们来分析一下这种不舒服的感觉从何而来。
首先,我们前面接触的几个样例中,字符串密文存放在 SO 的段上,而这个样本用 operator new[] 分配了堆内存用来存放密文。在这些糟糕的数字上按 H,转成十六进制,看起来会清晰很多。
v2 = operator new[](0xAu);
*(_DWORD *)v2 = 0xE94520D3;
*(_DWORD *)(v2 + 4) = 0x81CFA360;
*(_WORD *)(v2 + 8) = 0x109C;
v3 = sub_7715C(v2, 10);
本质上就是存储了 D3 20 45 E9 60 A3 CF 81 9C 10 这十个字节,我随便在段里找个地方,给大家 patch 模拟一下
现在第一个点解释清楚了,找第二个让我们晦涩的点——就是这儿的字符串解密函数,先前的例子只有一个字符串解密函数,这个样本至少有四个。不必对此感到忧虑,复杂的样本中,甚至可以让加密字符串和解密函数实现一对一,但这并不是什么大不了的事。
第三个点是解密函数的具体逻辑,大家点进去解密函数,会感觉很复杂,不用管它看起来多复杂,对于我们 Hook 回填没影响,不管多复杂,真机能执行就行。
第四个点,解密后的明文放在哪儿?解密函数看不太真切,Frida Hook 分析会发现,解密明文覆盖了密文。
下面我们准备写 Hook 回填脚本,首先验证这四个解密函数,是否真是解密函数?
function hookInit(){
var linkername;
var alreadyHook = false;
var call_constructor_addr = null;
var arch = Process.arch;
if (arch.endsWith("arm")) {
linkername = "linker";
} else {
linkername = "linker64";
}
var symbols = Module.enumerateSymbolsSync(linkername);
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("call_constructor") !== -1) {
call_constructor_addr = symbol.address;
}
}
if (call_constructor_addr.compare(NULL) > 0) {
console.log("get construct address");
Interceptor.attach(call_constructor_addr, {
onEnter: function (args) {
if(alreadyHook === false){
const targetModule = Process.findModuleByName("libmetasec_ml.so");
if (targetModule !== null) {
alreadyHook = true;
inittodo();
}
}
}
});
}
}
function inittodo(){
hook_decrypt();
}
function hook_decrypt(){
var base_addr = Module.findBaseAddress("libmetasec_ml.so");
Interceptor.attach(base_addr.add(0x77c05), {
onEnter(args) {
},
onLeave(retval) {
console.log(retval.readCString());
}
});
Interceptor.attach(base_addr.add(0x7715d), {
onEnter(args) {
},
onLeave(retval) {
console.log(retval.readCString());
}
});
Interceptor.attach(base_addr.add(0x7727d), {
onEnter(args) {
},
onLeave(retval) {
console.log(retval.readCString());
}
});
Interceptor.attach(base_addr.add(0x77699), {
onEnter(args) {
},
onLeave(retval) {
console.log(retval.readCString());
}
});
}
hookInit();
运行发现,确实打印了大量的明文字符串,其中包括一些明显是环境检测的字符串
/data/data/yk.juejin
/data/data/com.cyjh.mobileanjian
/data/data/com.cyjh.mobileanjianen
/data/data/com.touchsprite.android
/data/data/net.aisence.Touchelper
/data/data/com.touch.fairy
/data/data/com.zdanjian.zdanjian
/data/data/simplehat.clicker
/system/xbin/su
/system/bin/shuamesu
/system/xbin/daemonsu
/system/bin/bdsu
/system/xbin/bdsu
/system/bin/.su
/system/xbin/ku.sud
/system/xbin/bstk/su
/system/bin/sudo
/sbin/su
/data/data/com.topjohnwu.magisk
/system/app/longeneroot.apk
/system/app/Supersupro/Supersupro.apk
/data/data/com.kingroot.kinguser
/data/data/com.meizu.mzroottools
/data/data/com.devadvance.rootcloak2
/data/data/com.qihoo.permmgr
/data/data/com.baidu.easyroot
/data/data/hh.root
/data/data/com.shuame.rootgenius
/data/data/com.mtk.user2root
/sbin/magiskhide
/sbin/magiskinit
/sbin/.magisk
但是 Hook 没过几秒钟 App 就会闪退,简单分析应该是有 Anti Frida。代码堆里找检测点实在太痛苦了,感兴趣的读者可以自行过掉 Frida 检测,然后做处理,我实在太讨厌分析这玩意啦!
对具体算法内容感兴趣的朋友,可以使用 Unidbg 对样本做模拟执行,分析其密文的执行流,会发现解密逻辑很简单。
解密函数 sub_7715C 的具体逻辑
def fun_7715C(barr, length):
result = ""
key = [0xA5, 0x10, 0x71, 0xC7, 0x50, 0x90, 0xE1, 0xB1]
for i in range(length):
tmp = barr[i] ^ key[i & 7]
result += chr(tmp)
return result
以下面这处调用为例
v12 = operator new[](0x17u);
*(_DWORD *)v12 = 0xA61B5C8D;
*(_DWORD *)(v12 + 4) = 0xDDCEF126;
*(_DWORD *)(v12 + 8) = 0xE8167EC4;
*(_DWORD *)(v12 + 12) = 0xD893E403;
*(_WORD *)(v12 + 16) = 0x77CB;
*(_DWORD *)(v12 + 18) = 0xD20BEE4A;
*(_BYTE *)(v12 + 22) = 0xE1;
v13 = sub_7715C();
测试
print(fun_7715C(b"\x8D\x5C\x1B\xA6\x26\xF1\xCE\xDD\xC4\x7E\x16\xE8\x03\xE4\x93\xD8\xCB\x77\x4A\xEE\x0B\xD2\xE1", 0x17))
输出(Ljava/lang/String;)[B
。
解密函数 sub_77C04 的具体逻辑
def fun_77C04(barr, length):
result = ""
key = [0x35, 0x1f, 0x7f, 0xf7, 0x90, 0x9f, 0xe1, 0xb1]
for i in range(length):
tmp = barr[i] ^ key[i & 7]
result += chr(tmp)
return result
以下面这处调用为例
v9 = operator new[](9u);
*(_DWORD *)v9 = 0xB50B7A52;
*(_DWORD *)(v9 + 4) = 0xC284EBE9;
*(_BYTE *)(v9 + 8) = 0x35;
v10 = sub_77C04();
测试
print(fun_77C04(b"\x52\x7A\x0B\xB5\xE9\xEB\x84\xC2\x35", 9))
输出getBytes
。
解密函数 sub_77814 的具体逻辑
def fun_7727C(barr, length):
result = ""
key = [0xa5, 0x12, 0x81, 0xd7, 0x60, 0x10, 0x71, 0xb2]
for i in range(length):
tmp = barr[i] ^ key[i & 7]
result += chr(tmp)
return result
以下面这处调用为例
v0 = operator new[](0x13u);
*(_DWORD *)v0 = 0xA1E078E9;
*(_DWORD *)(v0 + 4) = 0xD31D3F01;
*(_DWORD *)(v0 + 8) = 0x84AE75CB;
*(_DWORD *)(v0 + 12) = 0xDC186214;
*(_WORD *)(v0 + 16) = 0x29C2;
*(_BYTE *)(v0 + 18) = 0x81;
v1 = sub_7727C(v0);
测试
print(fun_7727C(b"\xE9\x78\xE0\xA1\x01\x3F\x1D\xD3\xCB\x75\xAE\x84\x14\x62\x18\xDC\xC2\x29\x81", 0x13))
输出Ljava/lang/String;
。
解密函数 sub_77698 的具体逻辑
def fun_77698(barr, length):
result = ""
key = [0xb5, 0x20, 0x91, 0xe7, 0x40, 0xa0, 0xe3, 0xb6]
for i in range(length):
tmp = barr[i] ^ key[i & 7]
result += chr(tmp)
return result
以下面这处调用为例,读者朋友们应该注意到,这里的反编译效果很差,数组被识别成数个变量。
v107 = 0xB3F544D4;
v108 = 0xC58DC132;
v109 = 0x93E34FC5;
v110 = 0xD393D914;
LOBYTE(v111) = 0xB5;
sub_77698((int)&v107, 17);
测试
print(fun_77698(b"\xD4\x44\xF5\xB3\x32\xC1\x8D\xC5\xC5\x4F\xE3\x93\x14\xD9\x93\xD3\xB5", 17))
输出addTransportType
。
除了这四个,样本还存在许多其他字符串解密函数,可以在 sub_2AC58 中查看和分析它们。
4.3 样本三
树美.7z
交叉引用排第四的这个函数看着像字符串解密函数。
{'funcName': '.__stack_chk_fail', 'Address': '0x7760', 'xrefCount': 104}
{'funcName': '.free', 'Address': '0x7930', 'xrefCount': 102}
{'funcName': '.memset', 'Address': '0x78f0', 'xrefCount': 91}
{'funcName': 'sub_80D8', 'Address': '0x80d8', 'xrefCount': 77}
{'funcName': '.strlen', 'Address': '0x7890', 'xrefCount': 66}
{'funcName': 'sub_5D998', 'Address': '0x5d998', 'xrefCount': 65}
{'funcName': '.memcpy', 'Address': '0x78e0', 'xrefCount': 57}
{'funcName': 'sub_7FEC8', 'Address': '0x7fec8', 'xrefCount': 55}
{'funcName': 'sub_4C488', 'Address': '0x4c488', 'xrefCount': 45}
{'funcName': '.malloc', 'Address': '0x78d0', 'xrefCount': 36}
调用处是这样
sub_80D8("UWRYubYl2XXvaG3S9r5ezWcxX/VsRigluNW58+nIYq4=", 44LL);
函数体较大,请读者自行查看。介绍这个例子的原因在于,前面所展示的样本,字符串解密函数都是简单异或或变形的异或(dy 除外),读者不应该因此认为,字符串加解密总是使用简单算法。事实上,加解密可以采用任意一种可逆的古典或现代算法。我们看到很多字符串加解密使用异或,仅仅是因为异或简单好用以及最常用的几种字符串混淆方案使用异或。
下面首先确认是否是解密算法, Frida Hook 测试一下
function hookInit(){
var linkername;
var alreadyHook = false;
var call_constructor_addr = null;
var arch = Process.arch;
if (arch.endsWith("arm")) {
linkername = "linker";
} else {
linkername = "linker64";
}
var symbols = Module.enumerateSymbolsSync(linkername);
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("call_constructor") !== -1) {
call_constructor_addr = symbol.address;
}
}
if (call_constructor_addr.compare(NULL) > 0) {
console.log("get construct address");
Interceptor.attach(call_constructor_addr, {
onEnter: function (args) {
if(alreadyHook === false){
const targetModule = Process.findModuleByName("libsmsdk.so");
if (targetModule !== null) {
alreadyHook = true;
inittodo();
}
}
}
});
}
}
function inittodo(){
hook_sub_80D8();
}
function hook_sub_80D8(){
var base_addr = Module.findBaseAddress("libsmsdk.so");
Interceptor.attach(base_addr.add(0x80d8), {
onEnter(args) {
},
onLeave(retval) {
console.log(retval.readCString());
}
});
}
hookInit();
输入如下
ro.serialno
ro.build.fingerprint
/system/bin/su
/system/xbin/su
/proc/self/maps
substrate
XposedBridge.jar
/system/bin/ls
arm64-v8a
[{"key":"maps2","type":"file","path":"file:///proc/self/maps","option":"regex","words":["/data/.+\\.so"]},{"key":"wlan0","type":"dir","path":"file:///sys/class/net/wlan0","option":"exists"},{"key":"eth0","typ
e":"dir","path":"file:///sys/class/net/eth0","option":"exists"},{"key":"iomem","type":"file","path":"file:///proc/iomem","option":"match","words":["qemu-pipe","goldfish","vbox"]},{"key":"misc","type":"file","
path":"file:///proc/misc","option":"match","words":["vbox","qemu"]},{"key":"hnf1","type":"file","path":"file:///sdcard/user","option":"exists"},{"key":"hnf2","type":"file","path":"file:///data/local/tmp/user"
,"option":"exists"},{"key":"hnf3","type":"file","path":"file:///sdcard/fenshen/device.zip","option":"exists"},{"key":"hnf4","type":"dir","path":"file:///data/local/tmp/configs","option":"exists"},{"key":"hnf5
","type":"file","path":"file:///sdcard/fenshen/xiansi.json","option":"exists"},{"key":"hnf6","type":"file","path":"file:///sdcard/fenshen/gps.json","option":"exists"},{"key":"hnf7","type":"file","path":"file:
///data/local/tmp/configs/.a","option":"exists"},{"key":"hnf8","type":"file","path":"file:///data/local/tmp/configs/.d","option":"exists"},{"key":"hnf9","type":"file","path":"file:///data/local/tmp/configs/.e
n","option":"exists"},{"key":"hnf10","type":"file","path":"file:///data/local/tmp/configs/.gg","option":"exists"},{"key":"hnf11","type":"file","path":"file:///data/local/tmp/configs/.me","option":"exists"},{"
key":"vgd1","type":"dir","path":"file:///sdcard/wgzs","option":"exists"},{"key":"vgd2","type":"dir","path":"file:///dev/wgzs","option":"exists"},{"key":"sbsf1","type":"file","path":"file:///proc/self/maps","o
ption":"match","words":["/system/framework/ZpoosedBridge.jar"]},{"key":"sbsf2","type":"file","path":"file:///data/user_de/0/zpp.wjy.zposed.installer","option":"exists"},{"key":"sbsf3","type":"file","path":"fi
le:///data/data/zpp.wjy.zposed.installer","option":"exists"}]
我们确认它是字符串解密函数,其次,解密函数只调用几十次,相较于其他样本,这个数量非常少。但是,其他样本中,每次解密只解密一条明文,而这个样本的单次解密中,有时会解密大的字符串表。
下面具体分析字符串解密算法,使用Unidbg做处理会很方便,下面是简单的架子,大家可以用星球先前教过的各种技巧与知识去分析这个算法。
package com.sm;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import java.io.File;
import java.nio.charset.StandardCharsets;
public class SM {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private SM(){
emulator = AndroidEmulatorBuilder.for64Bit().build(); // 创建模拟器实例
Memory memory = emulator.getMemory();
// 设置sdk版本
LibraryResolver resolver = new AndroidResolver(23);
// 加载到内存中
memory.setLibraryResolver(resolver);
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/sm/bixin_8.7.3.apk"));
// 是否打印日志
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("smsdk", true);
module = dm.getModule();
}
public static void main(String[] args) {
SM sm = new SM();
sm.callDecrypt();
}
public void callDecrypt(){
int length = 44;
MemoryBlock memoryBlock = emulator.getMemory().malloc(length + 1, true);
memoryBlock.getPointer().write(0, "UWRYubYl2XXvaG3S9r5ezWcxX/VsRigluNW58+nIYq4=\0".getBytes(StandardCharsets.UTF_8), 0 , length+1);
UnidbgPointer pointer = memoryBlock.getPointer();
Number number = module.callFunction(emulator, 0x80d8, pointer.peer, length);
UnidbgPointer ret = UnidbgPointer.pointer(emulator, number);
// result:/proc/self/maps
System.out.println("result:"+ret.getString(0));
}
}
分析出算法流程,如下所示
密文首先做 base64 解码,然后经过AES - 256 解密,模式为 CBC,IV 是固定的 0102030405060708 字符串,Key 是对字符串 “smsdkshumeiorganizationflag” MD5 后的结果。
五、总结
无论 dump 回填还是 hook 回填,本质上都属于利用动态分析辅助和增强静态分析的路子。既然是动态分析,就存在代码覆盖率的问题,即有些字符串信息未被解密,因为它们所涉及的函数没有被触发和执行。为了缓解这个问题,不管是 dump 还是 hook,我们都会尽可能多的操作 App 使其运行尽可能多的逻辑。在具体操作上,dump 使用 attach 即可,而 hook 还需要避免遗漏较早的解密操作,因此选择 spawn 配合 init 的时机进行 Hook。