在C/C++中,常规字符串都是以0字节结尾的ASCII字符串,因此又称ASCIIZ字符串。
这是历史上硬件局限性决定的。在参考书目【Rit79】中,我们可以看到以下说明:
I/O操作的最小单位是word而不是byte。毕竟PDP-7是一种以字为单位寻址的设备。字和字节的差异性在这方面的唯一影响就是:处理字符串的程序必须要忽略字符串中的Null字符,因为在构造字符串的时候必须使用null字节将字符串凑成偶数个字节、形成word字。
在Hiew或者FAR Manager中,下述程序的字符串在可执行文件中会如图57.1所示:
int main()
{
printf ("Hello, world!\n");
};
图57.1 Hiew
如指令清单57.1所示,在Pascal及Borland Delphi编译的可执行程序中,字符串之前都会有一个声明字符串长度的8位/32位数据。
指令清单57.1 Delphi
CODE:00518AC8 dd 19h
CODE:00518ACC aLoading___Plea db 'Loading... , please wait.',0
...
CODE:00518AFC dd 10h
CODE:00518B00 aPreparingRun__ db 'Preparing run...',0
多数人认为,所谓Unicode编码就是用两个字节/16位数据来编码一个字符的字符封装格式。实际上这是一种常见的术语理解错误。Unicode实际上是一个标准,它规定的只是将一个数字写成字符的方法,但是没有定义具体的编码方式。
而目前比较流行的编码方式有UTF-8和UTF-16LE。前者广泛被利用在互联网和*NIX系统中,而后者主要使用在Windows环境下。
UTF-8
UTF-8是目前使用最广泛也最成功的字符编码方法之一。所有的拉丁字符都像ASCII码一样进行编码,ASCII码表以外的字符则采用多字节来编码。因为0的作用不变,所以所有的标准 C字符串函数都能正确处理包括UTF-8编码在内的所有字符串。
下面我们通过一个对照表来看看不同语言下的UTF-8的对比显示情况,采用的工具是FAR,测试样例出自己的http://go.ywrichev.com/17304。如图57.2所示。
图57.2 FAR UTF-8
从以上的对照,我们可以清楚地看到,只有英文的字符串看起来和ASCII表中的完全一样。匈牙利语言使用一些拉丁字符以及音节分隔标记来表示。这些符号使用多个字节来编码,我们这里采用了红色的下画线表示。从这个表,我们还可以看到爱尔兰语和波兰语也采用了同样的办法。而这个字符串对比的开始处,我们采用了一个欧元符号,它是用三个字节表示的。其余的系统与拉丁文没有关系。至少在俄语、阿拉伯语、希伯来语以及北印度语中,我们会发现其中一个字节是反复出现的,这也不奇怪:一个语言系统中的字符往往是在Unicode表中的相同位置处,因此它们的代码总是以系统的数字打头。
在最开始,也就是在第一个可见字符串“How much?”之前,我们会看到还有三个字节,实际上它们是字节顺序标记(Byte order mark,BOM)。BOM声明了字符串的编码系统。
UTF-16LE
很多Windows系统下的win32函数有-A和-W后缀。前面这种函数用于处理常规字符串,而后面这种带有-w的函数则是UTF-16LE字符串的专用函数(w代表wide)。
在UTF-16字符串的拉丁符号中,我们用工具Hiew或者FAR可以看到,这些字符都被字节0间隔开了,如图57.3所示。
图57.3 Hiew
程序如下所示。
int wmain()
{
wprintf (L"Hello, world!\n");
};
而在Windows NT系统中,我们可以经常看到的显示如图57.4所示。
图57.4 Hiew
在IDA的提示信息中,严格采用双字节对单字符编码的编码方式称为Unicode。
例如:
.data:0040E000 aHelloWorld:
.data:0040E000 unicode 0, <Hello, world!>
.data:0040E000 dw 0Ah, 0
而图57.5所示的则是俄语的字符串,它采用的是UTF-16LE编码方式。
图57.5 Hiew工具,编码方式UTF-16LE
我们比较容易分辨的是这些字符被星型的字符分割,而这个星型字符的ASCII值是4。实际上,西里尔字母位于Unicode表的第4映射区,因此所有的西里尔字母在UTF-16LE中的编码范围是0x400~0x4ff。详情请参考https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)
再回过头来看看我们上面列出的一个显示多语言字符串的例子。图57.6所示的是其在 UTF-16LE编码方式下的样子。
图57.6 采用工具软件FAR,编码格式为UTF-16LE
从以上图中我们可以看到,字节分割符BOM位于文件的开头,而所有的拉丁字母都用字节零来分割。一些带读音分割标志的字符(主要是匈牙利语和爱尔兰语)也采用红色的下划线标出来了。
Base64编码十分流行,是把二进制数据转换为文本字符串的常用标准。本质上说,这种算法用4个可显示字符封装3个二进制字节。它的字符集包括 26个拉丁字母(含大小写)、0~9共10个数字、加号“+”及反斜杠“/”,总共64个字符。
Base64编码的一个显著特征是它通常(但不一定)以1到2个等号“=”为结尾。
比如说以下两个Base64编码:
AVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuWs=
WVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuQ==
可以肯定的是,等号“=”绝不会出现在Base64编码字符串的中间。
对于逆向分析来说,程序中的调试信息都很重要。在某种程度上,调试信息能报告程序正在运行的状态。调试信息通常会由printf()一类的函数显示出来,或者被输出到日志文件中。但是在release/发行版、而非debug/测试版的软件中,即使有关指令调用了相关调试函数、也不会有任何实质性的输出内容。如果调试信息的转储数据中含有局部变量或者全局变量的信息,那么逆向工程人员就算赚到了—我们至少知道了变量的名称。比如说,我们可以通过转储信息确定Oracle RDBMS有一个函数叫做ksdwrt()。
有实际意义的字符串通常是逆向分析的重点。IDA反编译器可以显示出字符串的调用方函数和调用指令。熟练掌握这种分析之后,您可能会找到一些有趣的东西(可以参考https://yurichev.com/ blog/32/
)。
错误信息有时也很重要。Oracle RDBMS 构造了一系列函数专门处理错误信息。有兴趣的读者可以访问https://yurichev.com/blog/43/
了解详细信息。
多数情况下,我们能够迅速判断出汇报错误的函数以及引发它们报错的具体条件。有意思的是,正因如此,一些注重版权保护的程序会刻意在程序出错的时候临时调整错误信息或错误代码。毕竟,开发人员不会希望别人马上就能摸清他的防盗版措施。
本书的78.2节就演示了一个对错误信息加密的程序。
一些经常被用在后门程序中的魔数字符串看起来就很可疑。比如说,我们注意到一个关于TP-Link WR740家用路由器存在后门的报道(参见http://sekurak.pl/tp-link-httptftp-backdoor/
)。只有当他人访问下述URL时,才会触发这个后门:
http://192.168.0.1/userRpmNatDebugRpm26525557/start_art.html。
事实上,字符串userRpmNatDebugRpm26525557必定存在于固件中的某个文件。然而在这个后门东窗事发之前,Google搜索不到任何信息,当然这个后门被曝光后的情况完全相反。像这种后门类的字符串,查遍RFC资料你也找不到它。再怎么调整字节序,它也不会和科学算法沾边。它也绝不是错误信息或者调试信息。因此,尽快地定位类似这个的可疑字符串是一个好主意。
字符串通常采用Base64编码。因此把文件中的字符串全都进行解码处理,再扫一眼就知道哪个文件含有这个字符串了。
更准确来讲,这种隐藏后门的办法被称为“不公开即安全(security through obscurity)”,也就是见光死。