使用 C++(非托管)保护 .NET 4.+ 应用程序
将您的 4.5 框架 .NET 应用程序注入到 C++ 非托管主机应用程序中 - 快速、安全且无需任何额外工具或库。
战争开始以来
好吧,我们开始吧。众所周知,使用 C# 和 .NET 创建应用程序比开发 C++ 或 Delphi 等原生语言要容易得多。但是,每个 CLR 应用程序的主要问题是……安全性!
我告诉你,在 Flash SWF 文件之后,这是你能开发的最不安全的应用程序平台!
嗯……安全!
说实话,安全是个笑话!但是……它也有级别。
想象一下你有一块金表,你非常喜欢它!你不想和任何人分享。
- A > 你可以把它放在玻璃盒子里,好好保管。
- B > 你可以把它放在玻璃盒子里,周围放很多激光。
- C > 你可以把它放在金属盒子里,用一把只有你拥有的独特钥匙锁上。
- D > 你可以把它放在玻璃盒子里,然后把玻璃盒子放在金属盒子里锁起来。
你的安全方法的级别不同,但最终都可以被击败。你只是决定了难度级别……然后……我们也一样!
开始前,了解结局!
让我们面对现实吧!成百上千的公司声称他们可以保护您的软件!但是……
如你所知人们撒谎!只是为了骗你的钱!从来没有错,对吧?!
混淆器、加密器、反调试器、反转储器……
所有这些都可以非常容易地被反编译!有些工具只需单击一下,几秒钟内就能反编译您的应用程序……您甚至不需要成为工程师!
你可以在这里找到一些这样的应用程序
de4Dot , .Net Generic Unpacker , .NetBreak , DefuscatorNet , DeSpy , .Net Reflector , ILDasm ...
引用如果你了解你敌人的武器的正确识别方法,你获胜的几率就更大。我不知道是谁说的,但这是有道理的 :D
方法
目前,我们有几种将 .NET 应用程序注入非托管 C++ 应用程序的方法……
- 使用 BoxedAppSDK 的 DotNetAppLauncher - 您需要 8000 美元,它只支持 2.0 框架!开发人员收到了用户的大量电子邮件,要求支持 4.+ 框架,但他们的方法需要将 400MB 以上的内容嵌入到 C++ EXE 文件中!哈哈,这太他妈搞砸了!
- 嵌入您的 .NET 应用程序,将其提取到临时文件夹,然后启动! - 黑客只需要任务管理器!xD
- 使用虚拟应用程序,从内存中读取 .NET。 - 也只支持 2.0 框架
- 使用 C++/CLI 框架。 - 抱歉,发布的都是 .NET 文件!
- 花大量时间开发 Armageddon Packer 来结束战争! - 去吧,士兵!
- 使用这篇文章,支持 WPF 等。 使用起来很困难,而且有一些错误!
- 使用 CLRHosting 在 C++ 中从内存中的加密数据托管 .NET 应用程序! - 来了……它简单、编译快速且更安全!
你应该知道……
- 而且,您可以使用任何
PEPacker
、PEProtector
和PECrypter
来处理您的最终 EXE 文件。这是 .NET!但你可以! - 它也支持 WPF!
- 它是 100% 无病毒的!魔法存在!<VirusCheck>
准备项目
你需要什么
- Visual Studio 2012 或更高版本(C++ 原生 / .NET C# 4.0 - 4.7)
- 自定义哈希到字符串转换器“包含在源文件中”
- Fody/Costura .NET 扩展,用于将所有 DLL 引用嵌入到最终的 .NET EXE 文件中作为资源。从 nuget 获取
- HxD 哈希编辑器 它是免费的,下载它
- VMProtect 3、UPXGUI、MEW 或您想要的任何打包器
- CFF Explorer、Detect It Easy 和 Exeinfo,全部下载
步骤 A. 开发 .NET 应用程序
- 创建您的 .NET 应用(UWP、WPF、WinForm)。您需要的一切,没有任何限制!
我使用了 4.5 框架、
64位模式
和我的自定义 WPF 库(*image.a*) - 从 Nuget 下载并安装 Fody/Costura,然后重新生成项目,您将看到只有一个 EXE 文件。
- 在 Release 模式下构建。
步骤 B. 准备原生启动器应用程序
- 按照:新建 > 项目 > Visual C++ > Win32 > Win32 控制台应用程序 创建新项目。
- 将 Includes 添加到您的主 cpp 文件中。(在 headers 中创建 *RawFiles.h* 并保持为空。)
#include <cstdlib> #include <iostream> #include <sstream> #include <cassert> #include <fstream> #include "RawFiles.h" #pragma region Includes and Imports #include <windows.h> #include <metahost.h> #include <vector> #pragma comment(lib, "mscoree.lib") #import "mscorlib.tlb" raw_interfaces_only\ high_property_prefixes("_get","_put","_putref")\ rename("ReportEvent", "InteropServices_ReportEvent") using namespace mscorlib; #pragma endregion
- 定义一个值:
#define RAW_ASSEMBLY_LENGTH 1000
- 将
"int _tmain(int argc, _TCHAR* argv[])"
替换为int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, _ LPTSTR lpCmdLine, int nCmdShow)
- 转到链接器属性,将子系统更改为Windows (/SUBSYSTEM:WINDOWS),将目标计算机更改为MachineX64 (/MACHINE:X64)。
步骤 C. 开始编写主机代码!
- 通过添加:
CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
来安装 WPF 模式提示您只需要添加
CoInitializeEx
当您:- 构建 WPF 应用程序
- 在您的 WinForms 中使用了 WPF 用户控件
如果不是,则无需添加。
- 安装
CLRHost
、MetaHost
和Instances
ICLRMetaHost *pMetaHost = NULL; /// Metahost installed ICLRMetaHostPolicy *pMetaHostPolicy = NULL; /// Metahost Policy installed ICLRDebugging *pCLRDebugging = NULL; /// Metahost Debugging installed HRESULT hr; hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost); hr = CLRCreateInstance (CLSID_CLRMetaHostPolicy, IID_ICLRMetaHostPolicy, (LPVOID*)&pMetaHostPolicy); hr = CLRCreateInstance (CLSID_CLRDebugging, IID_ICLRDebugging, (LPVOID*)&pCLRDebugging);
- 安装 .NET Runtime
提示:您可以在此处设置运行时框架版本
GetRuntime(L"RUNTIMEVERSION")
。对于此项目,我使用了“v4.0.30319”版本。
ICLRRuntimeInfo* pRuntimeInfo = NULL; /// Runtime Info Installed. hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);
- 启动并加载
CLRApp
BOOL bLoadable; hr = pRuntimeInfo->IsLoadable(&bLoadable); ICorRuntimeHost* pRuntimeHost = NULL; hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&pRuntimeHost); hr = pRuntimeHost->Start();
- 安装
AppDomain
IUnknownPtr pAppDomainThunk = NULL; hr = pRuntimeHost->GetDefaultDomain(&pAppDomainThunk); _AppDomainPtr pDefaultAppDomain = NULL; hr = pAppDomainThunk->QueryInterface (__uuidof(_AppDomain), (VOID**) &pDefaultAppDomain); _AssemblyPtr pAssembly = NULL; SAFEARRAYBOUND rgsabound[1]; rgsabound[0].cElements = RAW_ASSEMBLY_LENGTH; rgsabound[0].lLbound = 0; SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, rgsabound); void* pvData = NULL; hr = SafeArrayAccessData(pSafeArray, &pvData);
- 添加、执行、清理和内存读取器
memcpy(pvData, Raw_Net_Data, RAW_ASSEMBLY_LENGTH); hr = SafeArrayUnaccessData(pSafeArray); hr = pDefaultAppDomain->Load_3(pSafeArray, &pAssembly); _MethodInfoPtr pMethodInfo = NULL; hr = pAssembly->get_EntryPoint(&pMethodInfo); VARIANT retVal; ZeroMemory(&retVal, sizeof(VARIANT)); VARIANT obj; ZeroMemory(&obj, sizeof(VARIANT)); obj.vt = VT_NULL; SAFEARRAY *psaStaticMethodArgs = SafeArrayCreateVector(VT_VARIANT, 0, 0); hr = pMethodInfo->Invoke_3(obj, psaStaticMethodArgs, &retVal);
如果编译时遇到任何问题,请重新启动 Visual Studio。
步骤 D. 使用代码!
- 好的,现在我们有了一个从内存中执行我们的 dotnet 应用程序的主机,现在是时候注入我们的应用程序了。
- 启动 HxD 并拖放您的 .NET release 应用程序。
- 选择所有哈希并单击:编辑 > 复制为 > C。
- 将剪贴板粘贴到 *RawFiles.h* 文件中。
- 将无符号
char
名称更改为Raw_Net_Data
unsigned char Raw_Net_Data[120399] = { 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, ... };
- 将
RAW_ASSEMBLY_LENGTH
设置为Raw_Net_Data
[120399] 数字#define RAW_ASSEMBLY_LENGTH 120399
现在构建应用程序。
如您所见,您的应用程序运行完美,没有任何问题!
战争开始!
- 您在 Visual Studio 中输入的十六进制数组编译速度很快,但问题是它被编译为嵌入式资源,您可以轻松地用 Exeinfo Ripper 提取它。
- 让我们开始下一个解决方案。
- 我们将创建字符串数组的十六进制,并在运行时加载它。Ripper 无法识别位于您的资源中的 PE 十六进制。
- 到目前为止我们编写的所有代码……
#include <cstdlib>
#include <iostream>
#include <sstream>
#include "stdafx.h"
#include <cassert>
#include <fstream>
#include "RawFiles.h"
#pragma region Includes and Imports
#include <windows.h>
#include <metahost.h>
#include <vector>
#pragma comment(lib, "mscoree.lib")
#import "mscorlib.tlb" raw_interfaces_only\
high_property_prefixes("_get","_put","_putref")\
rename("ReportEvent", "InteropServices_ReportEvent")
using namespace mscorlib;
#pragma endregion
#define RAW_ASSEMBLY_LENGTH 1715200
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
ICLRMetaHost *pMetaHost = NULL;
ICLRMetaHostPolicy *pMetaHostPolicy = NULL;
ICLRDebugging *pCLRDebugging = NULL;
HRESULT hr;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost,
(LPVOID*)&pMetaHost);
hr = CLRCreateInstance (CLSID_CLRMetaHostPolicy, IID_ICLRMetaHostPolicy,
(LPVOID*)&pMetaHostPolicy);
hr = CLRCreateInstance (CLSID_CLRDebugging, IID_ICLRDebugging,
(LPVOID*)&pCLRDebugging);
ICLRRuntimeInfo* pRuntimeInfo = NULL;
hr = pMetaHost->GetRuntime
(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);
BOOL bLoadable;
hr = pRuntimeInfo->IsLoadable(&bLoadable);
ICorRuntimeHost* pRuntimeHost = NULL;
hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(VOID**)&pRuntimeHost);
hr = pRuntimeHost->Start();
IUnknownPtr pAppDomainThunk = NULL;
hr = pRuntimeHost->GetDefaultDomain(&pAppDomainThunk);
_AppDomainPtr pDefaultAppDomain = NULL;
hr = pAppDomainThunk->QueryInterface(__uuidof(_AppDomain), (VOID**) &pDefaultAppDomain);
_AssemblyPtr pAssembly = NULL;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].cElements = RAW_ASSEMBLY_LENGTH;
rgsabound[0].lLbound = 0;
SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, rgsabound);
void* pvData = NULL;
hr = SafeArrayAccessData(pSafeArray, &pvData);
//////// Add Functions Here ////////////////////
int startint = 0;
////////////////////////////////////////////////
memcpy(pvData, Raw_Net_Data, RAW_ASSEMBLY_LENGTH);
hr = SafeArrayUnaccessData(pSafeArray);
hr = pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
_MethodInfoPtr pMethodInfo = NULL;
hr = pAssembly->get_EntryPoint(&pMethodInfo);
VARIANT retVal;
ZeroMemory(&retVal, sizeof(VARIANT));
VARIANT obj;
ZeroMemory(&obj, sizeof(VARIANT));
obj.vt = VT_NULL;
SAFEARRAY *psaStaticMethodArgs =
SafeArrayCreateVector(VT_VARIANT, 0, 0);
hr = pMethodInfo->Invoke_3
(obj, psaStaticMethodArgs, &retVal);
return 0;
}
您可以复制/粘贴 xD
等等……
步骤 E. 反 Ripper 解决方案
我们有四种方法可以击败 Ripper
- 在执行 CLR 之前,从数组中删除随机哈希,并在运行时替换为另一个丢失的部分数组。
- 它很复杂,但很快。
- 它经过压缩,使最终 PE 文件体积更小。
- 此方法的安全性低于其他方法。
- 将所有哈希转换为字符串块,并在运行时创建哈希数组。
- 它很重,需要 10 多分钟才能编译。
- 它在运行时快速执行 CLR,请放心。
- 它在防 ripper 和调试器方面安全性非常好。
- 它未压缩,并且会生成大型 EXE 文件,但您可以使用压缩器以 5-10% 的比例压缩它。
- 将所有哈希转换为字符串,然后使用 XOR 加密加密它们,并在运行时解密。
- 它很快但有点复杂。
- 它经过压缩。
- 极好的安全性!
- 像以前一样非常简单地创建它,然后使用像 VMProtect 3.0 这样的 PE Protector 来加密资源。
- 只有专家级黑客才能反编译。
- 它经过高度压缩。
- 它非常易于使用,并且不需要额外的时间。
- 它是虚拟的,具有完美的安全性!
哪个更好?!这完全取决于您选择哪种方法。
来了,这是哈希到字符串
的转换器和一个用于重新构建哈希的函数
字符串
数组有限,因此我们需要创建块链并将它们链接在一起。- *Data.txt* 包含
字符串
数组,*F_N.txt* 包含重建十六进制数组的函数。 - 将函数粘贴到主代码中的
//////// 添加函数
区域。
int blockchainsize = 10000; ///Block Size of strings
int time_in_block = (_countof(rawData))/(blockchainsize); /// Data Parts
FILE *filex = fopen("C:\\Data.txt", "w");
for( int ax = 0; ax <= time_in_block; ax = ax++ ) {
fprintf(filex, "std::string Raw");
fprintf(filex , "%d", ax);
fprintf(filex, "[");
fprintf(filex, "] = { \n");
for( int i = (blockchainsize*ax); i < (blockchainsize*(ax+1)-1); i = i++ ) {
fprintf(filex,"\"0x%X\"", (unsigned char)rawData[i]);
fprintf(filex, ",");
};
fprintf(filex,"\"0x%X\"", (unsigned char)rawData[(blockchainsize*(ax+1)-1)]);
fprintf(filex, "\n};");
fprintf(filex, "\n");
}
fclose(filex);
std::cout << "file created!" << "\n";
FILE *filex2 = fopen("C:\\F_N.txt", "w");
for( int ax = 0; ax <= time_in_block-1; ax = ax++ ) {
fprintf(filex2, "for (int vx = 0; vx < _countof(Raw");
fprintf(filex2 , "%d", ax);
fprintf(filex2, "); vx++) {\n ");
fprintf(filex2, "char valuex = strtol(Raw");
fprintf(filex2 , "%d", ax);
fprintf(filex2, "[vx");
fprintf(filex2, "].c_str(),NULL,16);\n rawData[startint] = valuex;
\n startint = startint+1; \n }; \n");
}
fclose(filex2);
这是一个简单的加密器/解密器,供您在项目中使用的
std::string encryptDecrypt(std::string toEncrypt) {
char key = 'X';
std::string output = toEncrypt;
for (int i = 0; i < toEncrypt.size(); i++)
output[i] = toEncrypt[i] ^ key;
return output;
}
步骤 F. 最后一步!
- 恭喜!引用
Wubba Lubba Dub Dub!您做到了! - R.Sanchez
- 最后一步是使用简单/高级保护器 VMProtect 3.0(最佳!)或 Microsoft 的 MPRESS 来保护您的应用程序。
- 我使用了 MPRESS 和自定义存根。
已知问题和修复
- 第一个问题 > 应用程序仍可能被 .NET 通用解包器提取
- 在您的 .NET 应用程序中添加此代码,并使用计时器
foreach (Process process_get in Process.GetProcesses()) { try { if (process_get.ProcessName.Contains("Unpack")) { process_get.Kill(); } } catch { } }
- 您可以通过文件信息、MD5 哈希等来使此功能更复杂,例如终止进程。
- 在您的 .NET 应用程序中添加此代码,并使用计时器
- 第二个问题 > 应用程序的线程仍然可以被暂停,您可以在 C++ 程序中编写反暂停功能。
您可以使用这篇文章。
结论
希望这篇文章对您有所帮助。正如我所提到的,没有完全起作用的安全功能,但您可以做得最好!
- 我必须特别感谢 RandomVertex 为他提供的从内存中托管 2.0 框架 CLR。
- 如果您有任何问题或疑问,请随时在下方发表评论,我很乐意为您提供帮助。
下一步?
我将向您展示如何使用自定义 PEPacker 保护您的 C++,是的!未知保护器 \m/!
历史
- 2018 年 3 月 25 日:添加了提示
- 2018 年 3 月 23 日:创建了文章