利用Shellcode注入PE文件加载计算器

时间:2024-04-03 22:35:29

简介

采用C语言查看和修改一些PE文件的关键结构,结合shellcode,完成功能:

  1. 先用C语言编写通过LoadLibrary()和GetProcAddress()调用msvcrt.dll中的system()函数来弹出计算器的代码。在OllyDbg中打开该程序,查看对应的汇编代码和机器码。
  2. 用PEview查看notepad.exe的结构,在UltraEdit中,参考OllyDbg中的汇编代码和机器码,直接修改notepad.exe的二进制码。先实现修改entrypoint和目标节的可执行权限,通过shellcode实现简单跳转,不影响原程序功能;再实现shellcode中通过PEview查看kernel32.dll中IAT地址,计算函数的VA,调用LoadLibrary()函数加载msvcrt.dll,接着使用GetProcAddress()获取system()函数在msvcrt.dll中此时内存中的地址,通过push参数,然后call这个地址,实现弹出计算器,在之后通过jmp跳回到原先的入口点,不影响原来程序的功能。
  3. 编写代码实现上述过程,并对程序进行测试。

C语言版shellcode

#include <windows.h>
#include <stdio.h>
typedef void (*MYPROC)(LPTSTRR);
int main()
{
	HINSTANCE LibHandle;
	MYPROC ProcAdd;
	LibHandle = LoadLibrary("msvcrt.dll");
	ProcAdd = (MYPROC) GetProcAddress(LibHandle,"system");
	(ProcAdd)("C:/windows/system32/calc.exe");
	printf("hello!\n");
	return 0;
}

  先用C语言实现shellcode功能,即先用LoadLibrary函数加载msvcrt.dll,然后获得msvcrt.dll中system函数的地址,然后调用system函数实现功能。C语言看上去非常简洁,在PE文件中,LoadLibrary()和GetProcAddress()一般都在kernel32.dll中,PE文件的IAT表中一般都能找到这两个函数。

汇编版shellcode

  因为机器不能执行C语言,只能执行机器码,所以在OllyDbg下查看刚刚那段代码的汇编和机器码,如图3-4所示,最左栏是内存地址,接着这一栏是机器码【地址从左到右是增长方向,与文件中看到的小端格式是一致的】,靠右这一栏是汇编代码,最右边是注释。
重点需要关注的就是红色部分的LoadLibraryA和GetProcAddress。
先看LoadLibraryA,在图3-2(a)中。在C语言中,这个函数有1个参数,是字符串格式的,返回一个结果。首先,将字符串压栈,把“msvcrt.dll”放在了ESP所在位置,这是该函数的仅需的一个参数。“msvcrt.dll”的地址是0x00404000,imagebase是0x00400000,打开PEview中查看,RVA为0x4000的位置,存放的的确是字符串“msvcrt.dll”,如图3-2(b)所示。
  在这之后的汇编指令,是把一个DWORD PTR放入了EAX,通过提示可以看出,这个是LoadLibraryA函数在内存中的地址,是0x00406138。
利用Shellcode注入PE文件加载计算器
利用Shellcode注入PE文件加载计算器
  该程序的入口地址是0x00040000,在PEview中查看IAT表,如图3-3所示。RVA=0x6138处的,正好对应着LoadLibraryA()条目。所以应该是IAT中对应函数的RVA,加上imagebase,就是函数在内存中的地址,可以直接call该地址调用该函数。
利用Shellcode注入PE文件加载计算器
  在调用完LoadLibraryA()之后,是GetProcAddress()部分,如图3-4所示。返回值放在EAX中,之后的指令腾出了4字节栈空间,然后把EAX的值暂存在栈中,把字符串“system”压栈,放在[esp+4]的位置,而[esp]中,放的是恢复后EAX,也就是原来的调用完LoadLibraryA()之后的返回值。然后把GetProcAddress()的地址放到EAX中,call EAX执行GetProcAddress指令。这个过程与C语言中是一致的,GetProcAddress需要2个输入参数,参数反向压栈,所以esp+4的位置放的是system,而esp中放的是msvcrt.dll的地址。
利用Shellcode注入PE文件加载计算器
  这之后,就是调用system()函数的部分,如图3-5所示。先将system的有且仅有的1个参数,也就是计算机所在的地址,放到esp中,在EAX中的是GetProcAddress()的返回值,也就是system()函数在内存中的地址,CALL EAX,完成弹出计算器。
利用Shellcode注入PE文件加载计算器

shellcode跳转到原入口地址

  前面弹出了计算器,若把该段shellcode直接插入到目标程序中,虽然能弹出计算器,但是目标程序的功能受到影响。所以应该在执行完shellcode后,使用一个跳转指令,跳转到原来程序的入口地址。
  汇编中跳转指令可以使用jmp相对目标地址,跳转到相对位置,继续执行。
  对应的机器码是E9 78563412,假设E9 78563412这条指令的E9所在的开始地址是A,12所在的地址是B=A+5,目标地址是C,那么C=B+0x12345678。jmp指令也可以往低地址跳,如图3-6,这是在notepad.exe中实现的跳转,E9后跟的是负数,往回跳转到了原来notepad.exe的入口地址0x739D。
利用Shellcode注入PE文件加载计算器

代码编写思路

  1. 判断文件是否是PE文件,如果是,继续执行,否则报错,方法同上一次。
  2. 在可选头中读取imagebase和entrypoint,并在DataDirectory的第二项中获取到引入表的地址和大小。
  3. 读取节表,获取每个节的起始RVA和在文件中偏移,通过这两个参数,计算出在一个节中,RVA和文件中偏移的转换delta。
  4. 在PE文件中寻找空闲区,估算需要200个字节,往往在.data节中有足够大的空余区域。找到空闲区后,确认此时在哪个节,并确认这个节的属性中是否有0x20000000可执行权限,如果没有,则修改节属性,增加可执行权限,.data节往往没有可执行权限。如果找不到空闲区域,就报错。
  5. 读取IDT,找到kernel32.dll,并通过kernel32.dll的IDT项,找到它的INT,在INT中,寻找到LoadLibraryA和GetProcAddress这两项,分别记录它们在INT中是处于第[A]个和第[B]个。如果找不到,则报错。在IAT中,对应的第[A]个和第[B]个就是LoadLibraryA和GetProcAddress这两项,记录第[A]个和第[B]个的RVA,这个RVA加上imagebase就是它们在内存中的地址。
  6. 将字符串参数合理的安排在空闲区中,模仿之前的汇编和机器代码,把机器代码插入到空闲区中,编写代码,对于不同程序,不相同的地方主要是函数的地址,函数的地址由第5步完成,将它们安排为小端填入,实现功能。

进一步

  可以通过shellcode,获取程序加载入内存是kernel32.dll的基址,进而获取其导出表,找到CreateProcess等函数的入口地址,call这些地址使用kernel32.dll的函数。