逆向工程Snapchat:混淆技术

2020-06-18 07:53:12

当你每天有2亿以上的用户时,你肯定想让你的API不让垃圾邮件制造者和公司知道,所以你必须在授权它对你的服务器进行API调用的二进制文件中保守一个秘密。Snapchat(10.81.6.81版)通过在每个请求中包括X-Snapchat-Client-Auth-Token标头来实现此目的,典型的请求如下所示:

忘了对这个二进制文件进行静态分析吧。以下是他们在高层次上所做的事情:CFG(控制流图)被销毁(不是扁平化),死代码、库调用大多是动态的,令牌生成函数(让我们称其为gen_Token)及其被调用者的所有符号都被剥离。出于这个原因,它们是用C实现的,而不是Objective-C,因为您总是可以针对其本身使用objc运行时1(琐碎之处:他们最近才开始使用SWIFT,但用于其他任务。)。

让我们看一下gen_Token中的第一个块。该块从二进制文件的不同部分加载一些值,然后:

orr w8,wzr,#0x3cmp x8,#0xborr w9,wzr,#0x6csel x8,x9,x8,hiadrp x28,0x106941000添加x28,x28,#0xe40;跳转表dr x8,[x28,x8,lsl#0x3]br x8。

请参阅前两个说明。为什么他们要在存储0x3之后立即将x8与0xb进行比较?不透明谓词1.CSEL条件始终为false,但这并不重要,因为就反汇编程序而言,这是一个条件,需要在运行时计算条件。将每一次跳转(包括合法条件)替换为类似的块,您已经完全破坏了任何现代反汇编程序的CFG。现在,Ghidra/IDA会很乐意显示它认为是带有尾部调用的小函数,这实际上是一个巨大的函数。我将告诉Ghidra它能够计算br x8中的地址,但只能计算第一个块的地址(因为它认为这是函数结束的地方)。现在这是一个插件的想法:使用仿真来计算具有不透明谓词的间接分支中的所有地址,这将需要仿真。我实际上为实现这一点做了一些工作,但这还没有完成这个二进制代码的一半战斗。

每隔几个块,您就会发现一个加载全局常量的块,对其执行一些看起来很复杂的操作,然后将其丢弃并转移到其他地方。它们只是为了迷惑你,一旦你看到它们几次就很容易被察觉,所以这不是什么阻碍。

例如,为了使代码尽可能平淡无奇,并防止您在看到对SecItemCopyMatching的调用时做出有根据的猜测,大多数库调用都是动态的。因此,与简单的bl SecItemCopyMatching不同,它们将执行以下操作:

反汇编程序不知道这里x23的值,因为如上所述,它将该块视为不阻塞当前函数。

当您有一个带有预定/固定计数器的循环时,您可以去掉计数器,并对循环迭代进行硬编码。这是以二进制大小为代价的,并且比使用计数器稍微快一些。Snap在加密函数中使用此技术。此块将一个巨大的字节数组移动到另一个,请注意偏移量是如何增加的,取代了计数器:

ldr w8,[sp,#0x278]字符串w8,[sp,#0x226c]ldr w8,[sp,#0x27c]字符串w8,[sp,#0x2268]ldr w8,[sp,#0x280]str w8,[sp,#0x2264]ldr w8,[sp,#0x284]str w8,[sp,#0x2260]ldr w8#0x2258]LDR W8,[sp,#0x290]字符串W8,[sp,#0x2254];诸若此类。

假设您有一个函数用正确的数据填充某些结构,另一个函数将字节转换为ASCII:

只要稍加努力,您就可以拦截对两者的呼叫,并通过观察他们的行为来了解他们做了什么。Snapchat有一个相当聪明的方法来阻止这一点。除上述两个外,还将有以下两个选项:

void join_function(uint64_t function_id,void*retval,void*argv[]){switch(Function_Id){case set_STRUCT_field_fi://从argv set_struct_field(P);Break;case BINS2ASCII_FI:bin2saii(in,out,nbytes);Break;//etc}}。

argv将包含所有需要的参数。现在去掉所有的符号,加上上面的混淆,你就得到了一个难以理解的庞大的函数。您可能认为仍然可以跟踪对联合函数的所有调用,并将PATH_KEY视为您感兴趣的函数的标识符。但是断点不会像您期望的那样起作用。请参见下一页。

现在大多数控制流模糊处理都是针对静态分析的,使用调试器来通过上面的操作就可以做到这一点。还没那么快。大多数函数都调用反调试函数,我对它进行了适当的命名,其签名是:

至少有9个这样的函数,都是相同的行为。我没有花时间去扭转他们,但他们的行为很明显。\软件断点通过修补内存中指定地址的指令来工作。补丁是一条指令,它触发由父进程(调试器3)处理的中断。这使得它们很容易被检测到;如果你有内存中某个区域的校验和,那么该区域中的断点将使校验和无效。或者,您可以在二进制中查找中断指令的BRK字节。

在进行检查之后,FUKUP_DEBUGING将返回uint64_t,它的值取决于是否检测到断点。所以实际上只有两个可能的值。那不是叫布尔吗?是。但是布尔值对于修补来说是微不足道的。但是对于int,您无法猜测“正确”的值。FUKUP_DEBUGING调用者使用返回值(我将其称为PATH_KEY)从跳转表加载地址,如果存在断点,获取的地址将导致无限循环,导致应用程序继续加载而没有反馈,这是正确的做法。

在这个二进制文件中,数据混淆是较难处理的事情之一,这里我们有很多MBA(混合布尔算术)和临时参数传递给函数,只是为了分散您的注意力。

在模糊技术方面研究较少的领域之一是MBA(大声呼喊给令人敬畏的Quarkslet,感谢他们对这一点和其他许多事情的研究)。这些通常在密码学中使用,但可以用于浮点。基本上,它们是混合了逻辑运算和纯算术的表达式。例如,x=(a^b)+(a&;b)。\这里有趣的是身份,例如x+y可以重写为(x^y)+2*(x&;y)[7]。现在想象一下,如果你递归地用相当于MBA的,疯狂的东西替换每一项,这个简单的x+y表达式会变得多么庞大。\这是汇编语言中的一个例子。该块所做的一切都是时间戳*1000:

add x0,sp,#0x1b8;struct timeval*tvalmov x1,#0x0;struct timezonze*tzoneadrp x8,0x109499000ldr x8,[x8,#0x1d0]blr x8;gettimeof day(tval,one)ldr x8,[sp,#0x1b8];tval->;tv_secmov w9,#0x3e8mul x8,x8,x9ldrsw x9,[sp,#0x1c0]lsr x9,x9,#0x3mov x10,#0xf7cfmovk x10,#0xe353,LSL#16movk x10,#0x9ba5,LSL#32movk x10,#0x20c4,LSL#48umulh x10x8e或x9、x8、x10mov x10、#0xe6b3movk x10、。#0x7dba,lsl#16movk x10,#0xecfa,lsl#32movk x10,#0x50e1,lsl#48bic x8,x10,x8subx8,x9,x8,lsl#0x1;有效TV_SEC*=1000。

这在二进制代码中不是很流行,但仍然值得一提。我见过它在一个函数中使用,该函数在指针处读取前8个字节。它有这样的造型:

scratch1和scratch2在完全没有使用的情况下被覆盖,这再次降低了您的速度。

让你的生活更悲惨的是,Snap有时会通过实现一些基本的标准库函数(即memmove)来剥夺你对它们的认识,或者可能只是复制源代码。花一两天时间颠倒一个函数,最后才发现它的Memmove,你不会很高兴的。

又一次荣誉奖。这个函数有一个基地址和一个索引,它使用循环从数组中加载字节。它们不是简单地将基址加到计数器来获得字节,而是进行计算,产生两个将溢出的64位大整数,但其和将等同于简单的计算。所以不是:

添加x10,sp,#0x338;baseldr x9,[sp,#0x270];Countermov x11,#0x5bddmovk x11,#0x7d38,LSL#16movk x11,#0x1e74,LSL#32movk x11,#0x6d7c,LSL#48add x9,x9,x11mov x12,#0x3f94movk x12,#0x7886,LSL#16movk x12,#0xf6b2,LSL#32movk。x11=0x6bd25fd9b8b5d943sub x9,x9,x11sub x9,x9,x12添加x9,x10,x9;x9=0x942da027b272bb75ldrb W9,[x9,x11];溢出和,但右堆栈偏移。

在Mach-O二进制文件中,指针位于__mod_init_funcs中的函数在main之前运行。使用otool查看Snap中有多少函数,我们发现惊人的816个函数:

%otool-s__data__mod_init_func Snapchat Snapchat:(__data,__mod_init_func)部分的内容0000000106819610 0042de58 00000001 0042de58 000000010000000106819620 0042de58 00000001 0042de58 000000010000000106819630 0042de58 00000001 0042de58 000000010000000106819640 0042de58。

因为每行有两个函数指针,所以它们的实际数量是816(去掉前两行之后)。但是等等,所有这些都指向相同的函数?他们可能是在用复制品来分散你的注意力,为了让你的工作更难做,让我们看看有多少复制品。在执行了一些正则表达式以获取函数指针之后,我发现有769个独特的函数,仍然是一个很大的数字。

其中一些是没有任何用处的伪函数。例如,第一个加载一个常量,将其存储在堆栈中,然后丢弃并返回:

在这769个函数中,有些函数肯定会执行一些真正的初始化,有些函数可能会作为另一个秘密越狱/调试器检测出现。过滤掉虚拟对象应该很容易,但我们仍然在讨论700多个功能,因此要找到您感兴趣的功能,您必须对Snap是如何做到的有所了解,这样您就可以在不筛选所有这些功能的情况下到达那里。

我可能会做一个关于如何绕过所有这些的第二部分。