思考
查了下网络上的通用ShellCode和目前为止出到第四版的《加密与解密》,发现关于ShellCode的编写基本上是在汇编这一个层级编写的。但是使用汇编语言写应该是十分不方便了,联想到C语言实际上是零抽象的高级语言,能不能直接使用C语言来编写ShellCode呢?
如何使用C语言来编写ShellCode那么,仔细想一想我们ShellCode的运行环境和平时的代码的运行环境有什么不一样的地方呢?
其中最大的一个区别就是我们的ShellCode的变量位置是不确定的,因此必须使用地址无关技术来实现。而实际上,C语言的局部变量也是通过ESP+偏移的方式来确定的,并且C的局部变量的地址也是不确定的。所以我们可以通过在一个函数里面编写ShellCode来实现通过高级语言来编写ShellCode。
使用C编写ShellCode时还有什么不能使用的语法呢?需要注意以下几点:
- 不能使用全局变量:全局变量位于.data段,对于EXE文件来说,一般的变量地址是固定的,但是对于DLL来说位置则根据加载位置不同而改变;
- 不能进行函数调用:C语言的编译器将函数调用编译成call xxx,对应的机械码为E8 xx xx xx xx,后四个字节是目标地址和指令地址的补码;
- 不能使用复杂数据,如字符串类型。类似于:
If(a==”String is invaild”)
这样的语句是不行的,原因在于字符串类型实际上是存储于程序的数据段。而如果想要使用字符串,正确的方式应该是:
char Buffer[10000];
Buffer[0] = 'I';
Buffer[1] = 'm';
Buffer[2] = '\0';
Debug版代码和Release版代码的区别
Debug版是为了尽可能尽早的找到出错代码的位置,这意味着编译器会在编译过程中插入检测代码。让我们看如下的一段代码:
void test() {
int a = 1;
int* b = &a;
}
在Debug版会编译出如下的代码:
void test() {
001F20F0 55 push ebp
001F20F1 8B EC mov ebp,esp
001F20F3 81 EC D8 00 00 00 sub esp,0D8h
001F20F9 53 push ebx
001F20FA 56 push esi
001F20FB 57 push edi
001F20FC 8D BD 28 FF FF FF lea edi,[ebp-0D8h]
001F2102 B9 36 00 00 00 mov ecx,36h
001F2107 B8 CC CC CC CC mov eax,0CCCCCCCCh
001F210C F3 AB rep stos dword ptr es:[edi]
001F210E B9 56 D0 1F 00 mov ecx,offset _9793C0DE_源@cpp (01FD056h)
001F2113 E8 62 F7 FE FF call @__CheckForDebuggerJustMyCode@4 (01E187Ah)
int a = 1;
001F2118 C7 45 F8 01 00 00 00 mov dword ptr [a],1
int* b = &a;
001F211F 8D 45 F8 lea eax,[a]
001F2122 89 45 EC mov dword ptr [b],eax
}
001F2125 52 push edx
001F2126 8B CD mov ecx,ebp
001F2128 50 push eax
001F2129 8D 15 4C 21 1F 00 lea edx,ds:[1F214Ch]
001F212F E8 E0 F3 FE FF call @_RTC_CheckStackVars@8 (01E1514h)
001F2134 58 pop eax
001F2135 5A pop edx
001F2136 5F pop edi
001F2137 5E pop esi
001F2138 5B pop ebx
001F2139 81 C4 D8 00 00 00 add esp,0D8h
001F213F 3B EC cmp ebp,esp
001F2141 E8 D2 F4 FE FF call __RTC_CheckEsp (01E1618h)
001F2146 8B E5 mov esp,ebp
001F2148 5D pop ebp
001F2149 C3 ret
其中有三条call指令,这三条指令都是为了检查栈溢出的,但是违反了不能再ShellCode中使用Call指令的原则,所以这样的代码是不正确的。也就是说必须使用release版的代码。
实例
这里是一个示例,用于获取GetProcAddr函数的地址。
#include"InlineFunctions.h"
#include<stdio.h>
#include<Windows.h>
typedef HANDLE (WINAPI*PGetFunction)(_In_ HMODULE hModule,_In_ LPCSTR lpProcName);
int ShellCode_Srouce() {
PGetFunction PGetProcAddress = nullptr;
char* DllBase;
char* ProcessBase;
__asm {
mov ebx, fs: [0x30] //得到peb结构体的地址
mov ebx, [ebx + 0xc] //得到Ldr结构体的地址
mov ebx, [ebx + 0xc] //得到ldr.InLoadOrderModuleList.Flink 第一个模块,当前进程
mov ProcessBase, ebx
mov ebx, [ebx] //得到第二个模块地址 ntdll.dll
mov ebx, [ebx] //得到第三个模块地址 kernel32.dll
mov ebx, [ebx + 0x18] //得到第三个模块地址(kernel32模块的dllbase)
mov DllBase, ebx
}
IMAGE_DOS_HEADER* DOS_Header = (IMAGE_DOS_HEADER*)DllBase;
IMAGE_NT_HEADERS* NT_Header = (IMAGE_NT_HEADERS*)(DOS_Header->e_lfanew + (char*)DOS_Header);
IMAGE_DATA_DIRECTORY* Export_Dir = (IMAGE_DATA_DIRECTORY*)&(NT_Header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
IMAGE_EXPORT_DIRECTORY* Export = (IMAGE_EXPORT_DIRECTORY*)(Export_Dir->VirtualAddress + DllBase);
for (int i = 0; i < Export->NumberOfNames; i++)
{
DWORD* FunctionNameRVA = (DWORD*)(DllBase + (DWORD)(Export->AddressOfNames + i * sizeof(DWORD)));
char* FunctionName = DllBase + *FunctionNameRVA;
if (FunctionName[0] != 'G')//这样做是为了使数据嵌入到指令中
continue;
if (FunctionName[1] != 'e')
continue;
if (FunctionName[2] != 't')
continue;
if (FunctionName[3] != 'P')
continue;
if (FunctionName[4] != 'r')
continue;
if (FunctionName[5] != 'o')
continue;
if (FunctionName[6] != 'c')
continue;
if (FunctionName[7] != 'A')
continue;
if (FunctionName[8] != 'd')
continue;
if (FunctionName[9] != 'd')
continue;
if (FunctionName[10] != 'r')
continue;
if (FunctionName[11] != 'e')
continue;
if (FunctionName[12] != 's')
continue;
if (FunctionName[13] != 's')
continue;
if (FunctionName[14] != '\0')
continue;
WORD Offset = *((WORD*)(Export->AddressOfNameOrdinals + DllBase) + i);
DWORD FunctionRVA = *((DWORD*)(DllBase + Export->AddressOfFunctions) + Offset);
PGetProcAddress = (PGetFunction)(DllBase + FunctionRVA);
break;
}
return;
}