欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新76+ PDF文档
加好友备注(星球)!!!
资源截图:
等等......
Shellcode基础概念
Shellcode
是一种以机器码形式存在的代码片段,设计用于直接插入和执行在目标进程或内存中,不依赖于特定环境(如全局变量,标准库等等)。
编写shellcode的注意事项
不能使用全局变量:
全局变量在普通应用程序中通常存放在数据段和全局变量区中,而Shellcode
的特点是不依赖环境
,因此执行时无法保证有这样的存储区存在。如果我们在Shellcode
中使用全局变量,因为全局变量的地址是固定的,在嵌入式时无法正常解析,在目标环境中访问全局变量可能会导致崩溃或非法访问的错误。全局变量通常存放在程序的数据段,也就是.data
段,编译时编译器会将全局变量的地址硬编码到程序中。
比如如下代码:
int global_var = 42; // 全局变量
void Shellcode() {
int local_var = global_var + 1; // 试图访问全局变量
}
int main(){
Shellcode();
}
0x03A000
eax
避免使用常量字符串:
在C
或C++
程序中,常量字符串会被编译器放到一个特定的内存区域,通常是只读区或常量段。比如如下代码:
编译之后字符串abc
会被放到常量区中,而变量str
会指向这块常量区的内存地址。
char *str = "abc";
void Shellcode() {
char *str = "abc"; // 常量字符串存放在常量区
some_function(str);
}
我们反汇编查看:
这里的在0x010D7B30
是固定的地址,如果将这段代码插入到shellcode
中,目标程序的内存布局和这个地址0x010D7B30
很可能不存在或已经被其他数据占用。
char str[] = { 'a', 'b', 'c', 0 }; // 字符串存储在栈上
ASCII
0x61
97
ASCII
a
避免使用绝对地址:
Windows
的DLL
模块(如kernel32.dll
)由于地址空间布局随机化(ASLR
)的存在,加载的地址也是动态变化的,不能直接使用绝对地址访问其中的函数后资源。
及时没有ASLR
,不同系统版本,更新也可能导致DLL
的加载机制和函数偏移发生变化。
我们可以使用LoadLibraryA
函数加载目标模块,比如kernel32.dll
。加载这个DLL
模块之后,可以使用GetProcAddress
函数来获取目标函数的基地址,最后根据查找到的地址进行调用。
一般我们需要从PEB
进程环境快
中获取模块基地址,编译该模块导出表,找到目标函数。
避免空字节:
空字节在C
字符串中被认为是结束符,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
PEB
dt _peb c56c8c6000
PEB
PEB
_PEB_LDR_DATA
dt _PEB_LDR_DATA 0x00007ffe`349dc4c0
_PEB_LDR_DATA
_LIST_ENTRY
dt _LIST_ENTRY 0x00000250`b7591860
_LDR_DATA_TABLE_ENTRY
_LIST_ENTRY
_LIST_ENTRY
_LDR_DATA_TABLE_ENTRY
首先我们可以通过NtCurrentTeb
函数来获取到当先线程的TEB
指针。获取到TEB
指针之后就可以获取到PEB
了。获取到PEB
结构之后,就可以获取到Ldr
成员了,这个成员指向PEB_LDR_DATA
结构,这个结构包含了进程级别的信息,比如加载的模块,进程环境变量,启动信息等等。
在Ldr
结构中有几个链表成员,管理了与加载模块相关的数据,如加载顺序,内存顺序以及初始化顺序等等。
在Ldr
结构中有一个InLoadOrderModuleList
成员,它的类型是_LIST_ENTRY
,他是一个双向链表,用来按加载顺序记录当前进程已加载的所有模块。通过这个链表我们可以访问到所有加载的模块。
然后将其地址赋值给Head
变量。这样你就可以遍历整个链表,访问每个加载的模块了。
Head = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InLoadOrderModuleList;
Windows
内核使用双向链表来组织和管理数据,每个链表节点都由_LIST_ENTRY
结构表示,这个结构包含了两个指针。分别是Flink
和Blink
。这两个指针使得链表能够双向遍历。
如上代码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.Buffer
和Data->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.h
stdlib.h
等等。-s
会让编译器在生成可执行文件时去除符号表和调试信息,这样可以减少生成文件的大小。-masm=intel
表示告诉编译器生成intel
风格的汇编语法。-fPIC
选项表示编译为位置无关的代码,这样的话生成的代码可以在内存中的任何位置加载和执行。
-o
指定输出文件的名称。> /dev/null 2>&1
表示丢弃掉所有输出。
现在我们在想如何将exe
转换为shellcode
呢?我们都知道在PE
文件中,.text
段通常是存储程序的可执行代码的区域。Shellcode
本质上是一段可以直接执行的汇编指令,通常嵌入在.text
段中,因此我们可以提取.text
段来直接获取目标程序的指令数据。
我们就可以将其shellcode.exe
拉入到x64dbg
中。在这里找到.text
段。
.text
shellcode.bin
Shellcode.bin
winhex
c
原文始发于微信公众号(Relay学安全):自定义Shellcode
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论