65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C++(非托管)保护 .NET 4.+ 应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2018年3月24日

CPOL

7分钟阅读

viewsIcon

73038

downloadIcon

2712

将您的 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 应用程序! - 来了……它简单、编译快速且更安全!

你应该知道……

  1. 而且,您可以使用任何 PEPackerPEProtectorPECrypter 来处理您的最终 EXE 文件。这是 .NET!但你可以!
  2. 它也支持 WPF!
  3. 它是 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 当您:

    1. 构建 WPF 应用程序
    2. 在您的 WinForms 中使用了 WPF 用户控件

    如果不是,则无需添加。

  • 安装 CLRHostMetaHostInstances
    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

  1. 在执行 CLR 之前,从数组中删除随机哈希,并在运行时替换为另一个丢失的部分数组。
    1. 它很复杂,但很快。
    2. 它经过压缩,使最终 PE 文件体积更小。
    3. 此方法的安全性低于其他方法。
  2. 将所有哈希转换为字符串块,并在运行时创建哈希数组。
    1. 它很重,需要 10 多分钟才能编译。
    2. 它在运行时快速执行 CLR,请放心。
    3. 它在防 ripper 和调试器方面安全性非常好。
    4. 它未压缩,并且会生成大型 EXE 文件,但您可以使用压缩器以 5-10% 的比例压缩它。
  3. 将所有哈希转换为字符串,然后使用 XOR 加密加密它们,并在运行时解密。
    1. 它很快但有点复杂。
    2. 它经过压缩。
    3. 极好的安全性!
  4. 像以前一样非常简单地创建它,然后使用像 VMProtect 3.0 这样的 PE Protector 来加密资源。
    1. 只有专家级黑客才能反编译。
    2. 它经过高度压缩。
    3. 它非常易于使用,并且不需要额外的时间。
    4. 它是虚拟的,具有完美的安全性!
    哪个更好?!

    这完全取决于您选择哪种方法。

来了,这是哈希到字符串的转换器和一个用于重新构建哈希的函数

  1. 字符串数组有限,因此我们需要创建块链并将它们链接在一起。
  2. *Data.txt* 包含字符串数组,*F_N.txt* 包含重建十六进制数组的函数。
  3. 将函数粘贴到主代码中的 //////// 添加函数区域。
            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 通用解包器提取
    1. 在您的 .NET 应用程序中添加此代码,并使用计时器
         foreach (Process process_get in Process.GetProcesses())
                  {
                      try
                      {
                          if (process_get.ProcessName.Contains("Unpack"))
                          {
                              process_get.Kill();
                          }
                      }
                      catch { }
                  }
    2. 您可以通过文件信息、MD5 哈希等来使此功能更复杂,例如终止进程。
  • 第二个问题 > 应用程序的线程仍然可以被暂停,您可以在 C++ 程序中编写反暂停功能。

    您可以使用这篇文章

结论

希望这篇文章对您有所帮助。正如我所提到的,没有完全起作用的安全功能,但您可以做得最好!

  • 我必须特别感谢 RandomVertex 为他提供的从内存中托管 2.0 框架 CLR。
  • 如果您有任何问题或疑问,请随时在下方发表评论,我很乐意为您提供帮助。

下一步?

我将向您展示如何使用自定义 PEPacker 保护您的 C++,是的!未知保护器 \m/!

历史

  • 2018 年 3 月 25 日:添加了提示
  • 2018 年 3 月 23 日:创建了文章
© . All rights reserved.