文章首发于先知社区从0到1编写shellcode

摘要

在c语言中我们使用的MessageBoxA函数,其实存放在User32.dll动态链接库中,它是一个windows API函数

当c语言在链接时,它会将源文件中的代码、库函数等链接在一起,生成最终的可执行代码,形成可执行程序

当程序运行时,操作系统会加载程序所需的动态链接库到内存中,而User32.dll中的MessageBoxA函数也被放在了内存中的一个位置,供给程序使用

硬编码shellcode

硬编码shellcode是直接通过地址调用相应API函数的一段16进制机器代码

例如:User32.dll中的MessageBoxA函数的内存地址我们知道,直接使用,这就是硬编码

先使用C语言编写一段弹窗程序,并修改visual stdio配置,能够让程序可以在xp的电脑上运行

c语言弹窗

#include <Windows.h>

int main() {
MessageBoxA(NULL, "ming", "test", MB_OK | MB_ICONINFORMATION);
return 0;
}

visual stdio设置

image-20231114133618906

image-20231114133659488

image-20231114133836852

当在xp环境中执行的时候,发现可以弹窗,程序正常执行

image-20231114133920845

ollydbg定位MessageBoxA函数内存地址

通过查找字符串功能选项寻找对应的汇编语言命令位置

image-20231114141356860

通过call命令可以发现MessageBoxA函数地址为0x77D5050B

也就是说内存中0x77D5050B的位置存放着windows操作系统User32.dll文件中的MessageBoxA函数

image-20231114141503028

编写内联汇编代码

#include<windows.h>
void main()
{
LoadLibrary(L"user32.dll");
__asm
{
xor ebx, ebx //ebx置0
push ebx
push 0x74736574; test //这些push会将数据压入栈中,而esp对应的为当前栈顶的地址
mov esi, esp //mov指令会将esp,当前栈顶的地址放入到eax
push ebx //push ebx的作用为插入一个空字节,隔开两个数据
push 0x676e696d; ming //将ming字符串压入到栈中
mov ecx, esp //将当前栈顶的地址放入到ecx,现在eax与ecx分别存放着test、ming数据的内存地址
push ebx
push esi //将eax压入栈中,为后续MessageBoxA函数执行提供参数
push ecx //将ecx压入栈中,为后续MessageBoxA函数执行提供参数
push ebx
mov esi, 0x77D5050B
call esi //调用MessageBoxA函数,实现弹窗
}
}

编译为可执行程序之后放入到xp中运行弹窗

image-20231130163125923

生成shellcode

IDA反汇编

image-20231202085147161

这些就是对应的shellcode

33DB5368746573748BC453686D696E678BCC53505153BE0B05D577FFD6

通过反汇编网站转换一下格式

https://defuse.ca/online-x86-assembler.htm

image-20231202085443107

通过shellcode加载器执行shellcode

#include<windows.h>
unsigned char shellcode[] = "\x33\xDB\x53\x68\x74\x65\x73\x74\x8B\xC4\x53\x68\x6D\x69\x6E\x67\x8B\xCC\x53\x50\x51\x53\xBE\x0B\x05\xD5\x77\xFF\xD6";

void main(int arc,char **argv)
{
LoadLibrary(L"user32.dll");
__asm
{
lea eax,shellcode
push eax
ret
}
}

image-20231202085622905

可是不同的操作系统对应的api函数位置不同

以下是在windows10操作系统下使用x32dbg调试得到的MessageBoxA函数地址,与xp下的0x77D5050B不同

image-20231202092307779

修改代码重新编译才可以执行

image-20231202092655291

image-20231202092723779

所以,因为不同操作系统下api函数加载的内存位置不同导致了硬编码shellcode没有可移植性,在xp下使用,但是不能在windows10上使用。

函数动态获取的shellcode

编写逻辑

所有的windows32程序都会加载kerner32.dll基础的动态链接库,而GetProcAddress函数就在kernel32.dll中

GetProcAddress这个函数的作用是从指定的动态链接库 (DLL) 检索导出函数 (也称为过程) 或变量的地址

使用GetProcAddress获取到kernel32.dll中LoadLibraryA的地址,在通过LoadLibraryA加载User32.dll动态链接库

也就能找到MessageBoxA函数,也就能实现弹窗

定位kernel32.dll地址

#include "windows.h"
#include "stdio.h"

int main()
{
int kernel32 = 0;
__asm {
mov ebx, fs: [0x30] //通过FS寄存器访问PEB结构
mov ebx, [ebx + 0xc] //获取PEB结构中Ldr字段的值
mov ebx, [ebx + 0x14] //获取PEB结构中InMemoryOrderModuleList字段的值
mov ebx, [ebx]; ntdll //获取InMemoryOrderModuleList第一个模块的基址
mov ebx, [ebx]; kernel
mov ebx, [ebx + 0x10]; DllBase //获取kernel32.dll模块的基址
mov kernel32, ebx
}
printf("kernel32=0x%x", kernel32);
return 0;
}

使用windbg查询kernel32.dll的基址

为了验证内联汇编程序得到的kerenl32.dll的基址位置正确,我们可以使用windbg运行一个32位程序查看kerenl32.dll的基址

dt _PEB @$peb

image-20231212144819362

dt _PEB_LDR_DATA 0x777ddb00

image-20231212144947311

dt _LIST_ENTRY 0x777ddb00+0x014
dt _LDR_DATA_TABLE_ENTRY 0x1153eb8

image-20231212145154446

dt _LDR_DATA_TABLE_ENTRY 0x11543c8

image-20231212145258924

使用x32dbg查询kernel32.dll的基址

image-20231212145600176

程序运行的结果与windbg、x32dbg查询的对应

image-20231212145358195

找出kernel32.dll导出表的地址

dt _IMAGE_DOS_HEADER 75f30000

image-20231212161744813

dt _IMAGE_NT_HEADERS  kernel32+0n248

image-20231212161850504

dt _IMAGE_OPTIONAL_HEADER kernel32+0n248+0x018

image-20231212161958085

dt _IMAGE_DATA_DIRECTORY kernel32+0n248+0x018+0x060

image-20231212162027062

导出表的地址为75f30000+0x93e40,及0x75fc3e40

汇编语言定位导出表地址

#include "windows.h"
#include "stdio.h"

int main()
{
int kernel32 = 0;
int Export_table = 0;
__asm {
mov ebx, fs: [0x30] //通过FS寄存器访问PEB结构
mov ebx, [ebx + 0xc] //获取PEB结构中Ldr字段的值
mov ebx, [ebx + 0x14] //获取PEB结构中InMemoryOrderModuleList字段的值
mov ebx, [ebx]; ntdll //获取InMemoryOrderModuleList第一个模块的基址
mov ebx, [ebx]; kernel
mov ebx, [ebx + 0x10]; DllBase //获取kernel32.dll模块的基址
mov kernel32, ebx
mov edx, [ebx + 0x3c]
add edx, ebx
mov edx, [edx + 0x78]
add edx, ebx
mov Export_table, edx
}
printf("Export=0x%x", Export_table);
return 0;
}

image-20231212162441985

遍历地址,找出GetProcaddress

导出表地址偏移0x20处为AddressOfNames,而AddressOfNames的结构是一个数组指针,每个机器位(4字节)都指向一个函数名的字符串

通过比较函数名字符串的方式确定GetProcAress函数的索引值

Get_Function:                                             
inc ecx
lodsd
add eax, ebx
cmp dword ptr[eax], 0x50746547 // GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 // rocA
jnz Get_Function

image-20231213093457732

这样就找到了GetProcAddress函数的索引值

image-20231213093629097

内联汇编代码

然后在寻找GetProcAddress函数的绝对地址,使用GetProcAddress获取到kernel32.dll中LoadLibraryA的地址,在通过LoadLibraryA加载User32.dll动态链接库

就能找到MessageBoxA函数,就能实现弹窗

#include "windows.h"
#include "stdio.h"

void main()
{
int kernel32 = 0;
int Export_table = 0;
__asm {
mov ebx, fs: [0x30] //通过FS寄存器访问PEB结构
mov ebx, [ebx + 0xc] //获取PEB结构中Ldr字段的值
mov ebx, [ebx + 0x14] //获取PEB结构中InMemoryOrderModuleList字段的值
mov ebx, [ebx]; ntdll //获取InMemoryOrderModuleList第一个模块的基址
mov ebx, [ebx]; kernel
mov ebx, [ebx + 0x10]; DllBase //获取kernel32.dll模块的基址
mov kernel32, ebx
mov edx, [ebx + 0x3c]
add edx, ebx
mov edx, [edx + 0x78]
add edx, ebx //获取导出表的地址
mov esi, [edx + 0x20]
add esi, ebx //获取AddressOfNames
xor ecx, ecx

Get_Function: // 搜索GetProcAddress,在kernel32.dll的导出函数中,GetProcA就能确认到
inc ecx
lodsd
add eax, ebx
cmp dword ptr[eax], 0x50746547 // GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 // rocA
jnz Get_Function
mov esi, [edx + 0x24]
add esi, ebx
mov cx, [esi + ecx * 2]
dec ecx
mov esi, [edx + 0x1c]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx // EDX = GetProcAddress

xor ecx, ecx; ECX = 0
push ebx // Kernel32.dll的基地址
push edx // GetProcAddress的基址
push ecx; 0
push 0x41797261; aryA
push 0x7262694c; Libr
push 0x64616f4c; Load
push esp; "LoadLibrary"
push ebx;
call edx // GerProcAddress(Kernel32,"LoadLibraryA") 使用GetProcAddress加载loadlibraryA-

add esp, 0xc; pop "LoadLibrary"
pop ecx; ECX = 0
push eax; EAX = LoadLibrary
push ecx
mov cx, 0x6c6c; ll
push ecx
push 0x642e3233; d.23
push 0x72657355; resU
push esp; "User32.dll"
call eax; LoadLibrary("User32.dll")

add esp, 0x10 // Clean stack
mov edx, [esp + 0x4] // EDX = GetProcAddress
xor ecx, ecx
push ecx
mov ecx, 0x6c41786f; obAl
push ecx
sub dword ptr[esp + 0x3], 0x6c; Remove “l”
push 0x42656761; Bega
push 0x7373654d; sseM
push esp; MessageBoxA
push eax; User32.dll address
call edx; GetProc(MessageBoxA)

add esp, 0x18; Cleanup stack
push ebp
xor ebx, ebx //ebx置0
push ebx
push 0x74736574; test //这些push会将数据压入栈中,而esp对应的为当前栈顶的地址
mov esi, esp //mov指令会将esp,当前栈顶的地址放入到eax
push ebx //push ebx的作用为插入一个空字节,隔开两个数据
push 0x676e696d; ming //将ming字符串压入到栈中
mov ecx, esp //将当前栈顶的地址放入到ecx,现在eax与ecx分别存放着test、ming数据的内存地址
push ebx
push esi //将eax压入栈中,为后续MessageBoxA函数执行提供参数
push ecx //将ecx压入栈中,为后续MessageBoxA函数执行提供参数
push ebx
call eax //调用MessageBoxA函数,实现弹窗

}
}

对应的shellcode

IDA反汇编exe程序,将对应的16进制代码取出

image-20231213095739766

image-20231213095832621

拿到反汇编网站上转换一下

https://defuse.ca/online-x86-assembler.htm

image-20231213095949152

\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF4\x8B\x53\x3C\x03\xD3\x8B\x52\x78\x03\xD3\x8B\x72\x20\x03\xF3\x33\xC9\x41\xAD\x03\xC3\x81\x38\x47\x65\x74\x50\x75\xF4\x81\x78\x04\x72\x6F\x63\x41\x75\xEB\x8B\x72\x24\x03\xF3\x66\x8B\x0C\x4E\x49\x8B\x72\x1C\x03\xF3\x8B\x14\x8E\x03\xD3\x33\xC9\x53\x52\x51\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x53\xFF\xD2\x83\xC4\x0C\x59\x50\x51\x66\xB9\x6C\x6C\x51\x68\x33\x32\x2E\x64\x68\x55\x73\x65\x72\x54\xFF\xD0\x83\xC4\x10\x8B\x54\x24\x04\x33\xC9\x51\xB9\x6F\x78\x41\x6C\x51\x83\x6C\x24\x03\x6C\x68\x61\x67\x65\x42\x68\x4D\x65\x73\x73\x54\x50\xFF\xD2\x83\xC4\x18\x55\x33\xDB\x53\x68\x74\x65\x73\x74\x8B\xF4\x53\x68\x6D\x69\x6E\x67\x8B\xCC\x53\x56\x51\x53\xFF\xD0

shellcode效果

使用shellcode加载器加载到内存执行

#include <stdio.h>
#include <windows.h>
using namespace std;
int main()
{
char shellcode[] = "\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF4\x8B\x53\x3C\x03\xD3\x8B\x52\x78\x03\xD3\x8B\x72\x20\x03\xF3\x33\xC9\x41\xAD\x03\xC3\x81\x38\x47\x65\x74\x50\x75\xF4\x81\x78\x04\x72\x6F\x63\x41\x75\xEB\x8B\x72\x24\x03\xF3\x66\x8B\x0C\x4E\x49\x8B\x72\x1C\x03\xF3\x8B\x14\x8E\x03\xD3\x33\xC9\x53\x52\x51\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x53\xFF\xD2\x83\xC4\x0C\x59\x50\x51\x66\xB9\x6C\x6C\x51\x68\x33\x32\x2E\x64\x68\x55\x73\x65\x72\x54\xFF\xD0\x83\xC4\x10\x8B\x54\x24\x04\x33\xC9\x51\xB9\x6F\x78\x41\x6C\x51\x83\x6C\x24\x03\x6C\x68\x61\x67\x65\x42\x68\x4D\x65\x73\x73\x54\x50\xFF\xD2\x83\xC4\x18\x55\x33\xDB\x53\x68\x74\x65\x73\x74\x8B\xF4\x53\x68\x6D\x69\x6E\x67\x8B\xCC\x53\x56\x51\x53\xFF\xD0";
LPVOID lpAlloc = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(lpAlloc, shellcode, sizeof shellcode);
((void(*)())lpAlloc)();
return 0;
}

image-20231213101015824

参考链接

https://xz.aliyun.com/t/2108

https://migraine-sudo.github.io/2019/12/21/Shellcode-Write/

https://blog.csdn.net/mdp1234/article/details/110287856?spm=1001.2014.3001.5502

https://www.youtube.com/watch?v=mN9LopGgkjk