最近研究PE的时候,用到一些技术涉及到ShellCode,于是想找一个通用型的,不用手工再去修正的代码,找了一些做参考,稍作修改,终于做成了一个通用ShellCode,163字节,还是挺大的,不过不影响,抛砖引玉,仅作参考,大家可以自行去优化。
一、全部汇编代码如下:
void __declspec(naked) myShellCode( const char * lpszDllPathName )
{
__asm
{
push ebp
mov ebp, esp
pushad
pushfd
sub esp, 0x28
mov ecx, 0xA
mov eax, 0xCCCCCCCC
lea edi, dword ptr [ebp-0x4c]
rep stosd
push 0x0
push 0x41797261
push 0x7262694c
push 0x64616f4c
mov eax, fs:[0x30] // fs points to teb in user mode,get pointer to peb
mov eax, [eax+0x0c] // get peb->ldr
mov eax, [eax+0x14] // get peb->ldr.InMemoryOrderModuleList.Flink(1st entry)
module_loop:
mov eax, [eax] // skip the first entry or get the next entry
mov esi, [eax+0x28] // get the BaseDllName->Buffer
cmp byte ptr [esi+0x0c], '3' // test the module's seventh's wchar is '3' or not,kernel32.dll
jne module_loop
mov eax, [eax+0x10] // LDR_DATA_TABLE_ENTRY->DllBase
mov edi, eax
add edi, [edi+0x3c] // IMAGE_DOS_HEADER->e_lfanew
mov edi, [edi+0x78] // IMAGE_NT_HEADERS->OptinalHeader.DataDirectory[EAT].VirtualAddress
add edi, eax // RVA + IMAGEBASE = EAT VA
mov ebx, edi // ebx is EAT's virtual address,we’ll use it later
mov edx, [ebx+0x20] // PIMAGE_EXPORT_DIRECTORY -> AddressOfNames
add edx, eax // edi -> EAT NAME TABLE
mov dword ptr [ebp-0x4c], 0
name_loop: // 遍历EAT名称表
mov esi, ebp
sub esi, 0x5c
mov ecx, dword ptr [ebp-0x4c]
mov edi, [edx+ecx*4]
add edi,eax // esi -> FUNCTION NAME Addr
add dword ptr [ebp-0x4c], 0x1
mov ecx, 0xc
rep cmpsb
jne name_loop // 名称不同下一个
mov edi, [ebx+0x24] // PIMAGE_EXPORT_DIRECTORY -> AddressOfNameOrdinals
add edi, eax
mov ecx, dword ptr [ebp-0x4c]
mov ecx, [edi+ecx*2] // 取得函数序号
and ecx, 0xFFFF // cause ordinal is USHORT of size,so we just use its lower 16-bits
mov edi, dword ptr [ebx+0x10]
sub ecx, edi // 减去基得到实际数组下标
mov edi, [ebx+0x1c] // PIMAGE_EXPORT_DIRECTORY -> AddressOfFunctions
add edi, eax
mov edi, [edi+ecx*4]
add eax, edi // 函数VA
mov ebx, dword ptr [ebp+8] // CALL LoadLibraryA
push ebx
call eax
add esp, 0x38
popfd
popad
mov esp, ebp
pop ebp
ret
}
}
二、我修改之后也加了一些注释,下面来总结下整个流程吧:
1、构建函数新栈,保存寄存器数据,填充,在栈中构建 LoadLibraryA字符串 。
2、获取TEB,再得到PEB,遍历模块链表查找 kernel32.dll,这里偷懒了,直接对比第七位是不是'3',感觉有些悬。。。。懒得改了。
3、通过模块结构得到DLL基址,跳过IMAGE_DOS_HEADER找到NT Option Header, 通过数据目录找到EAT表。
4、遍历EAT表的名称表,对比函数名字是否是 LoadLibraryA,并保存在表中的索引
5、直接定位EAT表中对应的序数表,序数减去base得到函数索引。
6、定位对应的EAT函数地址表,取得函数RVA加上DLL基址既是函数内存地址。
7、取得传入的DLL名字,调用 LoadLibraryA 加载dll到当前进程空间。
8、恢复寄存器,平衡堆栈。
三、提取shellCode字节码。
我们的ShellCode已经打造好了,直接调用,没有问题,至于提取成字节码,我看到很多人都是通过反汇编,然后一个一个去整理,163个字节啊兄弟,163个0x,163个逗号啊,,,想想都觉得蛋疼,于是我就想着能不能投机取巧呢?
答案是肯定的,我们可以直接用C++代码来提取:
DWORD dwShellCode = (DWORD)myShellCode;
while ( true )
{
unsigned char data = *((unsigned char *)dwShellCode);
if ( data == 0xCC )
{
if ( *((PDWORD)dwShellCode) == 0xCCCCCCCC && *((PDWORD)dwShellCode + 1) == 0xCCCCCCCC )
break;
}
char stBuff[10] = {0};
sprintf_s(stBuff, 10, "0x%02X,", data);
OutputDebugStringA(stBuff);
dwShellCode++;
}
注意,要用Release版本调试运行,因为Debug版本没有优化,函数是间接调用,用这个的话肯定就出错了,优化之后,函数名才是函数真正的起始地址,这样,我们的ShellCode就全部打印出来了,省时省力,轻松简单。我们只需要加上括号就可以了:
unsigned char shellcode[] = { 0x55,0x8B,0xEC,0x60,0x9C,0x83,0xEC,0x28,0xB9,0x0A,0x00,0x00,0x00,0xB8,0xCC,0xCC,0xCC,
0xCC,0x8D,0x7D,0xB4,0xF3,0xAB,0x6A,0x00,0x68,0x61,0x72,0x79,0x41,0x68,0x4C,0x69,0x62,
0x72,0x68,0x4C,0x6F,0x61,0x64,0x64,0xA1,0x30,0x00,0x00,0x00,0x8B,0x40,0x0C,0x8B,0x40,
0x14,0x8B,0x00,0x8B,0x70,0x28,0x80,0x7E,0x0C,0x33,0x75,0xF5,0x8B,0x40,0x10,0x8B,0xF8,
0x03,0x7F,0x3C,0x8B,0x7F,0x78,0x03,0xF8,0x8B,0xDF,0x8B,0x53,0x20,0x03,0xD0,0xC7,0x45,
0xB4,0x00,0x00,0x00,0x00,0x8B,0xF5,0x83,0xEE,0x5C,0x8B,0x4D,0xB4,0x8B,0x3C,0x8A,0x03,
0xF8,0x83,0x45,0xB4,0x01,0xB9,0x0C,0x00,0x00,0x00,0xF3,0xA6,0x75,0xE6,0x8B,0x7B,0x24,
0x03,0xF8,0x8B,0x4D,0xB4,0x8B,0x0C,0x4F,0x81,0xE1,0xFF,0xFF,0x00,0x00,0x8B,0x7B,0x10,
0x2B,0xCF,0x8B,0x7B,0x1C,0x03,0xF8,0x8B,0x3C,0x8F,0x03,0xC7,0x8B,0x5D,0x08,0x53,0xFF,
0xD0,0x83,0xC4,0x38,0x9D,0x61,0x8B,0xE5,0x5D,0xC3 };
四、怎么测试我们的ShellCode 呢?
int _tmain(int argc, _TCHAR* argv[])
{
unsigned char shellcode[] = { 0x55,0x8B,0xEC,0x60,0x9C,0x83,0xEC,0x28,0xB9,0x0A,0x00,0x00,0x00,0xB8,0xCC,0xCC,0xCC,
0xCC,0x8D,0x7D,0xB4,0xF3,0xAB,0x6A,0x00,0x68,0x61,0x72,0x79,0x41,0x68,0x4C,0x69,0x62,
0x72,0x68,0x4C,0x6F,0x61,0x64,0x64,0xA1,0x30,0x00,0x00,0x00,0x8B,0x40,0x0C,0x8B,0x40,
0x14,0x8B,0x00,0x8B,0x70,0x28,0x80,0x7E,0x0C,0x33,0x75,0xF5,0x8B,0x40,0x10,0x8B,0xF8,
0x03,0x7F,0x3C,0x8B,0x7F,0x78,0x03,0xF8,0x8B,0xDF,0x8B,0x53,0x20,0x03,0xD0,0xC7,0x45,
0xB4,0x00,0x00,0x00,0x00,0x8B,0xF5,0x83,0xEE,0x5C,0x8B,0x4D,0xB4,0x8B,0x3C,0x8A,0x03,
0xF8,0x83,0x45,0xB4,0x01,0xB9,0x0C,0x00,0x00,0x00,0xF3,0xA6,0x75,0xE6,0x8B,0x7B,0x24,
0x03,0xF8,0x8B,0x4D,0xB4,0x8B,0x0C,0x4F,0x81,0xE1,0xFF,0xFF,0x00,0x00,0x8B,0x7B,0x10,
0x2B,0xCF,0x8B,0x7B,0x1C,0x03,0xF8,0x8B,0x3C,0x8F,0x03,0xC7,0x8B,0x5D,0x08,0x53,0xFF,
0xD0,0x83,0xC4,0x38,0x9D,0x61,0x8B,0xE5,0x5D,0xC3 };
const char * lpszDll = "asm_dll.dll";
SIZE_T nSize = sizeof(shellcode);
DWORD dwOldProtect = NULL;
VirtualProtect( shellcode, nSize, PAGE_EXECUTE_READWRITE, &dwOldProtect );
__asm
{
push lpszDll
lea eax, shellcode
call eax
add esp, 0x4
}
VirtualProtect( shellcode, nSize, dwOldProtect, NULL );
}
注意:因为我们定义的这个ShellCode所在的内存没有执行权限,所以我们得修改内存属性,可读可写可执行,然后传参数,取ShellCode地址,直接调用,平衡堆栈。我们只要在dll加载时创建线程,这样就能成功让我们的代码自由自在的飞了。