自定义Shellcode

admin 2024年12月23日13:57:18评论8 views字数 6540阅读21分48秒阅读模式

欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新76+ PDF文档

加好友备注(星球)!!!

自定义Shellcode

资源截图:

自定义Shellcode
自定义Shellcode
等等......
Shellcode基础概念

Shellcode是一种以机器码形式存在的代码片段,设计用于直接插入和执行在目标进程或内存中,不依赖于特定环境(如全局变量,标准库等等)。

编写shellcode的注意事项

不能使用全局变量:

全局变量在普通应用程序中通常存放在数据段和全局变量区中,而Shellcode的特点是不依赖环境,因此执行时无法保证有这样的存储区存在。如果我们在Shellcode中使用全局变量,因为全局变量的地址是固定的,在嵌入式时无法正常解析,在目标环境中访问全局变量可能会导致崩溃或非法访问的错误。全局变量通常存放在程序的数据段,也就是.data段,编译时编译器会将全局变量的地址硬编码到程序中。

比如如下代码:

int global_var = 42; // 全局变量void Shellcode() {    int local_var = global_var + 1; // 试图访问全局变量}int main(){  Shellcode();}
反汇编如下,在如下图中,这里将其地址中
0x03A000
中的值给到
eax
寄存器中,然后对其进行相加。这个地址其实就是写死的。
自定义Shellcode

避免使用常量字符串:

CC++程序中,常量字符串会被编译器放到一个特定的内存区域,通常是只读区或常量段。比如如下代码:

编译之后字符串abc会被放到常量区中,而变量str会指向这块常量区的内存地址。

char *str = "abc";
我们来举一个例子:比如如下代码:
void Shellcode() {    char *str = "abc"; // 常量字符串存放在常量区    some_function(str);}

我们反汇编查看:

这里的在0x010D7B30是固定的地址,如果将这段代码插入到shellcode中,目标程序的内存布局和这个地址0x010D7B30很可能不存在或已经被其他数据占用。

自定义Shellcode
那么为了避免固定地址,我们可以将其字符串存储在栈上。
char str[] = { 'a''b''c'0 }; // 字符串存储在栈上
自定义Shellcode
如上图需要注意的是字符数组其实和整型数组是差不多的。只不过占用的字节不同而已。这里存储的是十六进制数值。我们可以将其转换为十进制,来对照
ASCII
表。比如
0x61
转换为十进制为
97
,而对应
ASCII
码为
a
自定义Shellcode

避免使用绝对地址:

WindowsDLL模块(如kernel32.dll)由于地址空间布局随机化(ASLR)的存在,加载的地址也是动态变化的,不能直接使用绝对地址访问其中的函数后资源。

及时没有ASLR,不同系统版本,更新也可能导致DLL的加载机制和函数偏移发生变化。

我们可以使用LoadLibraryA函数加载目标模块,比如kernel32.dll。加载这个DLL模块之后,可以使用GetProcAddress函数来获取目标函数的基地址,最后根据查找到的地址进行调用。

一般我们需要从PEB进程环境快中获取模块基地址,编译该模块导出表,找到目标函数。

避免空字节:

空字节在C字符串中被认为是结束符,Shellcode插入到其他程序时,如果包含空字节,可能导致字符串截断,代码无法完整执行。

避免直接使用空字节或操作会产生空字节的指令。

编写Shellcode

我们先来看看编写后的效果:

自定义Shellcode

接下来我们首先第一步如果我们要调用Windows API函数的话,那么我们就需要通过LoadLibrary函数去加载某个DLL模块。

首先我们来看看asm这里的编写:

asm(  "Start:                         n"  " push rsi                      n"  " mov  rsi, rsp                 n"  " and  rsp, 0xFFFFFFFFFFFFFFF0  n"  " sub  rsp, 0x20                n"  " call Main                     n"  " mov  rsp, rsi                 n"  " pop  rsi                      n"  " ret                           n");

这段汇编是Shellcode的入口,首先它会保存当前的栈指针寄存器值(rsi)。对栈指针(rsp)进行对其,以确保函数调用的栈帧满足16字节对齐的要求。然后调用Main函数,最后恢复栈指针并返回。

我们再来看看Main函数这里。我们本节的主要目的是是通过获取到WinExec的基地址,然后调用该函数起一个Calc计算器。

首先第一步肯定是获取到Kernel32.dll的基地址。我们可以通过模块名称的哈希值来查找指定模块,并返回该模块的基地址。函数定义如下:

PVOID LdrModulePeb(_In_ ULONG Hash)//Hash表示目标模块名称的哈希值 用于与模块列表中的名称进行匹配。

首先我们需要去定义一个LDR_DATA_TABLE_ENTRY结构,该结构是Windows内核中描述已加载模块的一个重要结构。通常是由PEB中的PEB_LDR_DATA去管理的。它提供了关于模块的各种信息,例如模块的名称,基地址,加载顺序等等。

如下是该结构的定义:

typedef struct _LDR_DATA_TABLE_ENTRY {    LIST_ENTRY InLoadOrderLinks;       // 链接到加载顺序的链表    LIST_ENTRY InMemoryOrderLinks;     // 链接到内存加载顺序的链表    LIST_ENTRY InInitializationOrderLinks; // 初始化顺序的链表    PVOID      DllBase;                // 模块的基地址    PVOID      EntryPoint;             // 模块的入口点(如 DLLMain)    ULONG      SizeOfImage;            // 模块镜像的大小    UNICODE_STRING FullDllName;        // 模块的完整路径(宽字符)    UNICODE_STRING BaseDllName;        // 模块的文件名(宽字符)    ULONG      Flags;                  // 特定标志(如模块加载状态)    USHORT     LoadCount;              // 模块的加载计数    USHORT     TlsIndex;               // TLS 的索引(如有)    LIST_ENTRY HashLinks;              // 模块的哈希链表    ULONG      TimeDateStamp;          // 模块的时间戳    // 更多字段(不同版本的 Windows 上可能有所不同)} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
我们可以通过
windbg
来进行查看,首先我们可以通过
! process 0 0
来查看所有进程。
自定义Shellcode
示的信息中每一个进程都有一个
PEB
(进程环境块)。那么我们就可以通过
dt _peb c56c8c6000
命令来查看某个进程的
PEB
结构。
自定义Shellcode
获取到
PEB
结构之后,我们就可以获取到他的
_PEB_LDR_DATA
结构了。使用
dt _PEB_LDR_DATA 0x00007ffe`349dc4c0
命令即可。
自定义Shellcode
获取到
_PEB_LDR_DATA
结构之后,就可以获取该
_LIST_ENTRY
结构了。使用
dt _LIST_ENTRY 0x00000250`b7591860
即可。
自定义Shellcode
接下来就可以查看
_LDR_DATA_TABLE_ENTRY
结构了,这是一个数据结构,用于描述已加载模块的详细信息。
自定义Shellcode
如果我们想要查看下一个结构,我们只需要查看下一个
_LIST_ENTRY
即可。
自定义Shellcode
获取到下一个
_LIST_ENTRY
之后,就可以查看下一个
_LDR_DATA_TABLE_ENTRY
结构了。
自定义Shellcode
自定义Shellcode

首先我们可以通过NtCurrentTeb函数来获取到当先线程的TEB指针。获取到TEB指针之后就可以获取到PEB了。获取到PEB结构之后,就可以获取到Ldr成员了,这个成员指向PEB_LDR_DATA结构,这个结构包含了进程级别的信息,比如加载的模块,进程环境变量,启动信息等等。

Ldr结构中有几个链表成员,管理了与加载模块相关的数据,如加载顺序,内存顺序以及初始化顺序等等。

Ldr结构中有一个InLoadOrderModuleList成员,它的类型是_LIST_ENTRY,他是一个双向链表,用来按加载顺序记录当前进程已加载的所有模块。通过这个链表我们可以访问到所有加载的模块。

然后将其地址赋值给Head变量。这样你就可以遍历整个链表,访问每个加载的模块了。

Head  = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InLoadOrderModuleList;

Windows内核使用双向链表来组织和管理数据,每个链表节点都由_LIST_ENTRY结构表示,这个结构包含了两个指针。分别是FlinkBlink。这两个指针使得链表能够双向遍历。

如上代码Head是链表的头指针,它指向链表的第一个节点,如果我们想要访问下一个节点,则我们需要Head->Flink的方式来访问,这样的话就指向了链表中的第二个节点。

Entry = Head->Flink

那么接下来就可以循环遍历了。

这里很简单,这里判断Head是否和Entry相等,如果相等的话,表示已经遍历完链表的所有节点。如果不相等继续遍历链表中的下一个节点。每次循环时,Entry会被更新为链表的下一个节点。

for ( ; Head != Entry ; Entry = Entry->Flink )
紧接着将其
Entry
强制转换为
PVOID
类型。并将其赋值给
PLDR_DATA_TABLE_ENTRY
结构。
Data = C_PTR( Entry );

那么接下来调用了HashString函数,将其Data->BaseDllName.BufferData->BaseName.Length作为参数传递进去。

其实就是将其Dll的名称和DLL名称的长度传递进去了。

这里会将其传递进去的哈希值和通过HashString函数生成的Hash值进行比较,如果比较成功的话,返回该Dll模块的基址。

for ( ; Head != Entry ; Entry = Entry->Flink ) {        Data = C_PTR( Entry );        if ( HashString( Data->BaseDllName.Buffer, Data->BaseDllName.Length ) == Hash ) {            return Data->DllBase;        }    }

这里调用LdrModulePeb函数所传递进去的哈希值为0xadd31df0

如下是HashString函数:

ULONG HashString(    _In_ PVOID String,    _In_ ULONG Length) {    ULONG  Hash = { 0 };    PUCHAR Ptr  = { 0 };    UCHAR  Char = { 0 };    Hash = 5381;    Ptr  = String;    do {        Char = *Ptr;        if ( ! Length ) {            if ( !*Ptr ) break;        } else {            if ( U_PTR( Ptr - U_PTR( String ) ) >= Length ) break;            if ( !*Ptr ) ++Ptr;        }        if ( Char >= 'a' ) {            Char -= 0x20;        }        Hash = ( ( Hash << 5 ) + Hash ) + Char;        ++Ptr;    } while ( TRUE );    return Hash;}

现在我们已经拿到了Kernel32.dll模块的基址了。下一步就是获取该模块中的函数了。获取模块中的函数非常简单,我们可以直接遍历导出表来获取到该函数的地址。

那么我们首先肯定是需要获取到DOS头的。获取到DOS头之后就可以获取到导出数据目录了。紧接着就可以获取到导出表相关的信息了。

这里就不详细说了。

NtHeader         = C_PTR( Module + ( ( PIMAGE_DOS_HEADER ) Module )->e_lfanew );    ExpDirectory     = C_PTR( Module + NtHeader->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress );    ExpDirectorySize = U_PTR( Module + NtHeader->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].Size );    AddrOfNames      = C_PTR( Module + ExpDirectory->AddressOfNames );    AddrOfFunctions  = C_PTR( Module + ExpDirectory->AddressOfFunctions );    AddrOfOrdinals   = C_PTR( Module + ExpDirectory->AddressOfNameOrdinals );    for ( int i = 0; i < ExpDirectory->NumberOfNames; i++ )    {      FunctionName = ( PCHAR ) Module + AddrOfNames[ i ];      if ( HashString( FunctionName, 0 ) == Hash ) {        return C_PTR( Module + AddrOfFunctions[ AddrOfOrdinals[ i ] ] );      }    }
拿到导出函数的地址之后就可以进行调用了。需要注意的是我们需要定义该函数的原型:
typedef UINT WINEXEC (  _In_ LPCSTR lpCmdLine,  _In_ UINT   uCmdShow); typedef WINEXEC* PWINEXEC;

这样我们的shellcode代码就编写好了。

接下来我们就可以编译了,这里编译我们采用GCC编译器来进行编译。

x86_64-w64-mingw32-gcc shellcode.c -Os -nostdlib -s -masm=intel -fPIC -o shellcode.exe > /dev/null2>&1

这里的参数有必要给大家介绍一下,首先-Os告诉编译器在编译过程中优化生成的代码大小,-Os会使得编译器尽可能的减小生成目标文件的大小。-nostdlib告诉编译器在链接时不使用标准库,也就是说编译器在生成目标文件时不会自动链接标准的C库。比如stdio.hstdlib.h等等。-s会让编译器在生成可执行文件时去除符号表和调试信息,这样可以减少生成文件的大小。-masm=intel表示告诉编译器生成intel风格的汇编语法。-fPIC选项表示编译为位置无关的代码,这样的话生成的代码可以在内存中的任何位置加载和执行。

-o指定输出文件的名称。> /dev/null 2>&1表示丢弃掉所有输出。

自定义Shellcode
现在当我们双击执行的时候就可以弹出计算器了。
自定义Shellcode

现在我们在想如何将exe转换为shellcode呢?我们都知道在PE文件中,.text段通常是存储程序的可执行代码的区域。Shellcode本质上是一段可以直接执行的汇编指令,通常嵌入在.text段中,因此我们可以提取.text段来直接获取目标程序的指令数据。

我们就可以将其shellcode.exe拉入到x64dbg中。在这里找到.text段。

自定义Shellcode
我们只需右键
.text
段->将内存转存为文件即可得到
shellcode.bin
文件。
自定义Shellcode
拿到
Shellcode.bin
文件之后,只需使用
winhex
打开,全选->复制为
c
源码即可。执行之后就可以弹出计算器了。
自定义Shellcode
自定义Shellcode
本节就先到这里期待和您的下次相遇!!!

原文始发于微信公众号(Relay学安全):自定义Shellcode

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月23日13:57:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   自定义Shellcodehttps://cn-sec.com/archives/3540382.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息