安全矩阵

 找回密码
 立即注册
搜索
查看: 1010|回复: 0

shellcode开发

[复制链接]

417

主题

417

帖子

2391

积分

金牌会员

Rank: 6Rank: 6

积分
2391
发表于 2023-12-8 22:29:58 | 显示全部楼层 |阅读模式
relaySec Relay学安全 2023-11-17 18:29 发表于陕西

CppDevShellcode
前言:
网络上推荐开发shellcode是用Clang、或者python开发,有些博客直接用的汇编在写。
为了简化开发的难度,并且windows上还是主推Visual Studio写cpp。
所以就有了这个使用Visual Studio开发shellcode的项目。
开发基础:cpp、win32编程、pe结构、x64/x32汇编



0x01 开发环境配置
  • 保证随机基址开启(VS默认开启随机基址)
  • Debug改成Release模式 (测试的时候也可以用debug,最后生成的时候改成Release就行)
  • 修改入口函数(不能使用默认的main函数,vs在调用main函数之前做了很多处理导致代码会出现绝对地址)
  • 关闭GS选项
  • 关闭优化
  • 使用静态库


图文
1)修改入口函数(32位)



2)关闭gs


3)关闭优化,调成静态库


64位环境配置
由于微软不给在64位cpp代码中插入汇编指令,并且所有调用约定都是FASTCALL。需要使用联合编译汇编才行

  • 新建一个name.asm文件(文件名不能和项目名一样)
  • 项目属性-》生成依赖项-》生成自定义 ----》吧masm选项勾上
  • 右键asm文件属性-》项目类型选择asm


图文:

1)项目生成依赖项



2)asm属性


0x02 拿kernel32模块基址
GetProcAddress函数和LoadLibraryA函数都是在kernel32.dll模块中的。



那么首先我们需要去获取到kernel32.dll模块的基址,然后通过kernel32的基址拿到GetProcAddress函数和LoadLibraryA函数,这里可以通过自实现的getProAddress来去拿到地址。

kernel32.dll模块的基址可以使用GetModuleHandle来进行获取,但是我们如果使用GetModuleHandle来获取的话就会进入死循环,因为GetModuleHandle也是一个函数,那么GetModuleHandle又在那个模块呢?这样的话就会陷入一个死循环。

在exe启动的时候系统会分配给它一个4GB的虚拟空间,在这个虚拟空间中有一个内存块,专门来存放进程相关的一些信息,比如说进程的一些名字或者参数,以及是否是调试状态等等,这个是通过结构体进行存储的,也就是PEB。

还有一些线程类的一些相关信息,里面存放一些线程相关的信息,这里是通过TEB结构体进行存储的。

  1. PEB: 进程相关的信息
  2. TEB: 线程相关的信息
复制代码


那么既然我们无法直接通过函数进行获取的话,那么我们可以找到一个PEB这个结构体,然后通过这个结构体拿到这个模块的基址。

PEB存储在什么地方?

它存储在FS寄存器中。


000
指向SEH链指针

004线程堆栈顶部
008线程堆栈底部
00CSubSystemTib
010FiberData
014ArbitraryUserPointer
018FS段寄存器在内存中的镜像地址
020进程PID
024线程ID
02C指向线程局部存储指针
030PEB结构地址(进程结构}
034上个错误号

那么只需要FS[0x30]就可以拿到PEB结构地址了。

如下代码:

  1. int* x;
  2. __asm {
  3.     mov eax,fs:[0x30]
  4.     mov x,eax
  5. }
复制代码


那么这一块就是我们PEB的结构了。


PEB结构体如下:
  1. #pragma once
  2. #include<Windows.h>
  3. typedef struct _UNICODE_STRING {
  4.     USHORT Length;
  5.     USHORT MaximumLength;
  6.     PWSTR  Buffer;
  7. } UNICODE_STRING, * PUNICODE_STRING;
  8. typedef struct _LDR_DATA_TABLE_ENTRY
  9. {

  10.     LIST_ENTRY InLoadOrderLinks;
  11.     LIST_ENTRY InMemoryOrderLinks;
  12.     LIST_ENTRY InInitializationOrderLinks;
  13.     PVOID DllBase;
  14.     PVOID EntryPoint;
  15.     ULONG SizeOfImage;
  16.     UNICODE_STRING FullDllName;
  17.     UNICODE_STRING BaseDllName;
  18.     ULONG Flags;
  19.     USHORT LoadCount;
  20.     USHORT TlsIndex;
  21.     union
  22.     {
  23.         LIST_ENTRY HashLinks;
  24.         struct
  25.         {
  26.             PVOID SectionPointer;
  27.             ULONG CheckSum;
  28.         };
  29.     };
  30.     ULONG TimeDateStamp;

  31. } LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
  32. typedef struct _PEB_LDR_DATA
  33. {
  34.     ULONG                   Length;
  35.     BOOLEAN                 Initialized;
  36.     PVOID                   SsHandle;
  37.     LIST_ENTRY              InLoadOrderModuleList;
  38.     LIST_ENTRY              InMemoryOrderModuleList;
  39.     LIST_ENTRY              InInitializationOrderModuleList;
  40. } PEB_LDR_DATA, * PPEB_LDR_DATA;
  41. typedef struct _PEB {
  42.     BYTE                          Reserved1[2];
  43.     BYTE                          BeingDebugged;
  44.     BYTE                          Reserved2[1];
  45.     PVOID                         Reserved3[2];
  46.     PPEB_LDR_DATA                 Ldr;
  47. }PEB, * PPEB;
复制代码



如下方法是固定拿到模块的基址的,具体原理就是遍历链表。
  1. HMODULE GetPebModule1(wchar_t* p)
  2. {
  3.   PPEB pPeb;
  4.   PPEB_LDR_DATA pLdr;
  5.   PLDR_DATA_TABLE_ENTRY pLdrData;
  6.   //0048E000//0048E000
  7.   __asm
  8.   {
  9.     MOV EAX, FS: [0x18]
  10.     MOV EAX, [EAX + 0x30]
  11.     MOV pPeb, EAX
  12.   }

  13.   pLdr = pPeb->Ldr;
  14.   pLdrData = (PLDR_DATA_TABLE_ENTRY)pLdr->InLoadOrderModuleList.Flink;

  15.   for (; (pLdr->InLoadOrderModuleList.Flink) != (pLdrData->InLoadOrderLinks.Flink); )
  16.   {
  17.     if (wcscmp(pLdrData->BaseDllName.Buffer, p) == 0)
  18.     {
  19.       return (HMODULE)pLdrData->DllBase;
  20.     }
  21.     pLdrData = (PLDR_DATA_TABLE_ENTRY)(pLdrData->InLoadOrderLinks.Flink);
  22.   }

  23.   return NULL;
  24. }
复制代码


可以看到成功拿到了KERNEL32.DLL的基地址。


0x03 自己实现GetProcAddress函数
自己实现GetProcAddress函数,去获取kernel32中的 LoadLibraryA 、GetProcAddress等函数地址

获取到函数地址之后转成函数指针调用。

原理:

  1. 1. 拿到kernel模块基址就是MZ的地址
  2. 1. 解析pe文件格式
  3. 1. 定位导出表
  4. 1. 找到指定导出表中函数名称
  5. 1. 再去拿指定函数的地址,并且返回
复制代码


代码如下:
  1. #include <iostream>
  2. #include <Windows.h>
  3. DWORD RAVTOVA(DWORD RVA, DWORD hModule)
  4. {
  5.   return RVA + hModule;
  6. }
  7. FARPROC
  8. WINAPI
  9. MyGetProcAddress(
  10.   _In_ HMODULE hModule,
  11.   _In_ LPCSTR lpProcName
  12. ){
  13.     //获取DOS头
  14.   PIMAGE_DOS_HEADER pIMAGE_DOS_HEADER = (PIMAGE_DOS_HEADER)hModule;
  15.   //获取NT头
  16.   PIMAGE_NT_HEADERS pIMAGE_NT_HEADERS = (PIMAGE_NT_HEADERS)(pIMAGE_DOS_HEADER->e_lfanew +
  17.     (DWORD)hModule);
  18.   //获取导出表
  19.   PIMAGE_EXPORT_DIRECTORY pIMAGE_EXPORT_DIRECTORY = (PIMAGE_EXPORT_DIRECTORY)RAVTOVA(
  20.     (DWORD)pIMAGE_NT_HEADERS->OptionalHeader.DataDirectory[0].VirtualAddress,
  21.     (DWORD)hModule);
  22.   //导出函数名称表
  23.   DWORD* NameAddress = (DWORD*)RAVTOVA(pIMAGE_EXPORT_DIRECTORY->AddressOfNames, (DWORD)hModule);

  24.   //导出函数名称序号表
  25.   WORD* NameOrdinalAddress = (WORD*)RAVTOVA(pIMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals, (DWORD)hModule);
  26.   //导出函数地址表
  27.   DWORD* AddressFun = (DWORD*)RAVTOVA(pIMAGE_EXPORT_DIRECTORY->AddressOfFunctions, (DWORD)hModule);
  28.   for (size_t i = 0; i < pIMAGE_EXPORT_DIRECTORY->NumberOfNames; i++)
  29.   {

  30.     char* FunName = (char*)RAVTOVA(NameAddress[i], (DWORD)hModule);
  31.     if (strcmp(FunName, lpProcName) == 0)
  32.     {
  33.       printf("%d\n", NameOrdinalAddress[i] + pIMAGE_EXPORT_DIRECTORY->Base);
  34.       printf("%x\n", RAVTOVA(AddressFun[NameOrdinalAddress[i]], (DWORD)hModule));
  35.       printf("找到了\n");
  36.     }

  37.     //printf("%s\n", FunName);
  38.   }

  39.   return NULL;


  40. }
  41. int main()
  42. {
  43.   HMODULE Hmodule = GetModuleHandleA("ntdll.dll");
  44.   //MessageBoxA(0, 0, 0, 0);
  45.   MyGetProcAddress(Hmodule, "RtlDispatchAPC");
  46.     std::cout << "Hello World!\n";
  47. }
复制代码


注意:需要解析pe文件所以64位的和32位的MyGetProcAddress不通用。

0x04 保存函数地址
shellcode开发不能有绝对地址也就不能使用全局变量,所以需要一个结构体来保存函数指针。

函数调用的时候专递结构体指针就能拿到函数地址。

这里就新建一个头文件,把结构体和函数声明都放到这个头文件里面去

  1. //windowsapi 函数指针声明
  2. typedef int (WINAPI* PFN_MessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
  3. typedef HMODULE(WINAPI* PFN_LoadLibraryA)(LPCSTR);
  4. typedef FARPROC(WINAPI* PFN_GetProcAddress)(HMODULE, LPCSTR);

  5. //存函数地址的结构体
  6. typedef  struct SellcodeEvn
  7. {
  8.   PFN_MessageBoxA m_pfnMessageBoxA;
  9.   PFN_LoadLibraryA m_pfnLoadLibraryA;
  10.   PFN_GetProcAddress m_pfnGetProcAddress;


  11. }SCENV, * PSCENV;

  12. //函数的声明
  13. EXTERN_C HMODULE GetModuleKernel();
  14. FARPROC MyGetProcAddress(HMODULE hModule, char* lpProcName);
复制代码


0x05 拿api函数地址
1)MyGetProcAddress函数第一个参数是模块基址,第二个参数是函数名。

2)不能有char*类型的变量(否则会被放到数据区,产生绝对地址)

  1. void InitEnv(PSCENV pEnv)
  2. {
  3.   char sz_MessageBoxA[] = { 'M','e','s','s','a','g','e','B','o','x','A','\0' };
  4.   char sz_LoadLibraryA[] = { 'L','o','a','d','L','i','b','r','a','r','y','A','\0' };
  5.   char sz_GetProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };
  6.   char sz_user32[] = { 'u','s','e','r','3','2','\0' };

  7.   //拿kernel32的地址
  8.   HMODULE hKernel32 = GetModuleKernel();
  9.   pEnv->m_pfnGetProcAddress = (PFN_GetProcAddress)MyGetProcAddress(hKernel32, sz_GetProcAddress);
  10.   pEnv->m_pfnLoadLibraryA = (PFN_LoadLibraryA)pEnv->m_pfnGetProcAddress(hKernel32, sz_LoadLibraryA);
  11.   
  12.   HMODULE hUser32 = pEnv->m_pfnLoadLibraryA(sz_user32);
  13.   pEnv->m_pfnMessageBoxA = (PFN_MessageBoxA)pEnv->m_pfnGetProcAddress(hUser32, sz_MessageBoxA);
  14. }
复制代码


PS:自己开发一个char* 转char数组的工具,以及api函数转函数指针。或者让GPT给你转也行。

0x06 入口点调用函数完成自己的功能
入口函数之前不能放别的函数。第一个函数只能是入口函数。

  1. //这是shellcode的入口函数
  2. void ShellCodeOEP()
  3. {
  4.   SCENV env; //存放函数地址的结构体
  5.   InitEnv(&env); //给结构体赋值函数地址

  6.   //测试代码messageboxa
  7.   char sz_hello[] = { 'h','e','l','l','o','\0' };
  8.   env.m_pfnMessageBoxA(NULL, sz_hello, sz_hello, MB_OK);
  9.   //代码从这开始写


  10. }
复制代码


这里只做了一个messageboxa,需要什么功能照着模板写就是了 。
也可以先生成DEBUG版本的调试一下,无错误再生成Release版本。
用2进制编辑器打开生成的pe文件,拷贝.text节区的所有内容即可完成shellcode制作。
2进制编辑器 010Editor、winhex都行。

0x07 64位--汇编拿kernel32基址
需要联合编译,拿基址的汇编代码只能单独写到.asm  文件里面。联合编译在头文件里面定义函数声明调用。

shellcode.asm

  1. .code
  2. GetModuleKernel proc
  3.   xor r8, r8
  4.   xor rax, rax
  5.   xor r10, r10
  6.   add r10, 60h
  7.   mov rax, gs:[r10]     ;通过GS寄存器获取PEB基址
  8.   mov rax, [rax + 18h]  ;获取PEB中Ldr数据结构的基址
  9.   mov rax, [rax + 10h]  ;获取Ldr数据结构的InmemoryOrderModuleList字段的基址
  10.   mov rax, [rax]       ;获取InmemoryOrderModuleList链表第一个节点 用这个取就是ntdll的基址
  11.   mov rax, [rax]       ;获取InmemoryOrderModuleList链表第一个节点  用这个就是kernen32的基址
  12.   mov rax, [rax + 30h]  ;获取节点中BaseAddress字段,既kernel32.dll的基址
  13.   ret
  14. GetModuleKernel endp
  15. end
复制代码


结语
有几个开发的注意事项。
  • 声明变量不要给初始值。
  • 入口函数在第一个。否则oep会在shellcode中间。
  • 存api函数地址不能使用全局变量,可以定义一个结构体来存放。
  • MyGetProcAddress 由于要解析pe头,所以32位和64位不通用。
  • strcmp、memcpy这种函数得自己实现, 可以去vc++6.0 里面拷贝。

生成完shellcode之后吧可执行文件丢到x64dbg里面去,f9运行到OEP往下翻一下,看看地址下面有没有横线。
出现横线就代表代码中出现了绝对地址的变量。
右键在内存窗口中转到,结合pdb符号文件找出哪个函数中出现的绝对地址。

特别鸣谢:
wangda:https://github.com/wangda38

此项目参照好兄弟提供的upx壳代码shellcode部分二开。

最终项目地址:

  1. https://github.com/clownfive/CppDevShellcode
复制代码


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 01:32 , Processed in 0.013424 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表