Naiiive的文章

  • DLL加载模拟器直接在内存中加载DLL不通过API加载

    背景在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
    在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载DLL,并获取DLL的导出函数,调用并执行。当然,这种技术并非没有积极的一面。如果你的程序需要很多DLL文件进行动态调用,你完全可以把这些DLL作为资源插入到自己的程序中,然后使用这个内存加载运行DLL的技术,直接在内存中加载运行就好,不需要再将DLL释放到本地上。这样做的好处应该不言而喻了,就是你的程序会因此变得很整洁,只有一个EXE程序,其他的DLL均包含在EXE程序里了。
    现在,把实现的思路和实现过程,写成文档,分享给大家。
    程序实现原理要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。
    1. DLL加载到内存的过程实现原理
    首先,从DLL文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是DLL的加载基址
    然后,根据DLL中的PE结构格式获取其映像对齐大小SectionAlignment,然后把DLL文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
    接着,根据PE结构的重定位表,重新对重定位表进行修正
    接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
    接着,修改DLL的加载基址ImageBase
    最后,根据PE结构获取DLL的入口地址,然后构造并调用 DllMain 函数,实现DLL的加载

    2. 获取DLL导出函数的实现过程
    首先,根据DLL的加载基址以及PE结构,获取DLL导出表
    然后,获取导出表的信息,如NumberOfNames、AddressOfNames等信息
    接着,遍历导出表的导出函数名字列表,与请求获取的导出函数名称进行匹配
    匹配成功,则获取导出函数的地址,并返回

    3. DLL的释放DLL的释放就比较好理解了,就是直接释放模拟加载DLL时候申请的可读、可写、可执行的内存即可。
    编码实现DLL加载过程函数// 模拟LoadLibrary加载内存DLL文件到进程中// lpData: 内存DLL文件数据的基址// dwSize: 内存DLL文件的内存大小// 返回值: 内存DLL加载到进程的加载基址LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize){ LPVOID lpBaseAddress = NULL; // 获取镜像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中开辟一个可读、可写、可执行的内存块 lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { ShowError("VirtualAlloc"); return NULL; } ::RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { ShowError("MmMapFile"); return NULL; } // 修改PE文件重定位表信息 if(FALSE == DoRelocationTable(lpBaseAddress)) { ShowError("DoRelocationTable"); return NULL; } // 填写PE文件导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { ShowError("DoImportTable"); return NULL; } //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。 //统一设置成一个属性PAGE_EXECUTE_READWRITE DWORD dwOldProtect = 0; if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { ShowError("VirtualProtect"); return NULL; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { ShowError("SetImageBase"); return NULL; } // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint if (FALSE == CallDllMain(lpBaseAddress)) { ShowError("CallDllMain"); return NULL; } return lpBaseAddress;}
    获取DLL导出函数// 模拟GetProcAddress获取内存DLL的导出函数// lpBaseAddress: 内存DLL文件加载到进程中的加载基址// lpszFuncName: 导出函数的名字// 返回值: 返回导出函数的的地址LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName){ LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]); if (0 == ::lstrcmpi(lpFuncName, lpszFuncName)) { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc;}
    DLL释放// 释放从内存加载的DLL到进程内存的空间// lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址// 返回值: 成功返回TRUE,否则返回FALSEBOOL MmFreeLibrary(LPVOID lpBaseAddress){ BOOL bRet = FALSE; if (NULL == lpBaseAddress) { return bRet; } bRet = ::VirtualFree(lpBaseAddress, 0, MEM_RELEASE); lpBaseAddress = NULL; return bRet;}
    程序测试我们编写一个用来测试的DLL程序TestDll,导出函数的代码如下所示:
    BOOL ShowMessage(char *lpszText, char *lpszCaption){ ::MessageBox(NULL, lpszText, lpszCaption, MB_OK); return TRUE;}
    在 main 函数中,调用上述封装好的函数接口进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szFileName[MAX_PATH] = "TestDll.dll"; // 打开DLL文件并获取DLL文件大小 HANDLE hFile = ::CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError("CreateFile"); return 1; } DWORD dwFileSize = ::GetFileSize(hFile, NULL); // 申请动态内存并读取DLL到内存中 BYTE *lpData = new BYTE[dwFileSize]; if (NULL == lpData) { ShowError("new"); return 2; } DWORD dwRet = 0; ::ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL); // 将内存DLL加载到程序中 LPVOID lpBaseAddress = MmLoadLibrary(lpData, dwFileSize); if (NULL == lpBaseAddress) { ShowError("MmLoadLibrary"); return 3; } printf("DLL加载成功\n"); // 获取DLL导出函数并调用 typedef BOOL(*typedef_ShowMessage)(char *lpszText, char *lpszCaption); typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MmGetProcAddress(lpBaseAddress, "ShowMessage"); if (NULL == ShowMessage) { ShowError("MmGetProcAddress"); return 4; } ShowMessage("CDIY - www.coderdiy.com - 专注计算机技术交流分享\n", "CDIY"); // 释放从内存加载的DLL BOOL bRet = MmFreeLibrary(lpBaseAddress); if (FALSE == bRet) { ShowError("MmFreeLirbary"); } // 释放 delete[] lpData; lpData = NULL; ::CloseHandle(hFile); system("pause"); return 0;}
    测试结果:
    直接成功弹窗!

    所以,DLL文件直接在内存中成功被加载并执行。
    总结这个程序对于初学者来说,理解起来比较复杂。但是,你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。上面讲解中,对于重定位表、导入表以及导出表部分的具体操作并没有细讲,如果你没有了解PE结构,那么理解起来会有困难。如果你了解过PE结构,那么那部分知识应该也不用细讲的。
    2  留言 2019-01-01 13:24:04
  • 基于Crypto++库的RSA非对称加密实现对数据的加解密

    背景写了一个基于Crypto++加密库中RSA非对称加密算法实现的对数据加密和解密的一个小程序,Crypto++加密库就不详细介绍了,这个库提供了很多知名的加解密算法,直接调用就好了,使用起来还是比较方便的。
    写这篇文章,就是分享自己的学习心得。自己的密码学部分的知识学得不怎么好,还好有Crypto++开源库可以使用,弥补了对加解密部分的不足。现在,向大家分享使用Crypto++中的RSA非对称加密算法实现对数据的加密解密方面的知识。
    程序编译设置注意事项首先,先要下载Crypto++库的开源代码,然后,自己编译得到Crypto++的库文件。下载链接还有具体的编译步骤,可以参考这个平台上其他用户写的分享文章“使用VS2013编译Crypto++加密库”,里面有详细介绍。
    在导入Crypto++的库文件到自己的工程项目的时候,要对自己的工程项目进行编译设置。主要一点就是:项目工程的属性中的“运行库”设置,要与编译Crypto++的库文件时设置的“运行库”选项要对应一致,否则程序会编译不过的。也就是要检查LIB库工程和本测试工程的:属性 —> C/C++ —> 代码生成 —> 运行库 是否统一。
    如果编译出错,报告XX重复定义等错误,同样,要检查LIB库工程和本测试工程的:属性 —> C/C++ —> 代码生成 —> 运行库 是否统一。
    程序设计原理1. 产生公钥和私钥文件原理RSA是非对称加密算法,所以它的加密和解密的密钥是不同的,它有自己的公钥和私钥。在正常的使用过程中,公钥一般是用来加密数据,私钥用来解密数据。反之,也可以。公钥可以公开,但是私钥不可以公开。
    对于Crypto++库产生公私钥的原理如下:

    首先用类RandomPool的方法Put()产生种子seed的byte型伪随机
    RSAES_OAEP_SHA_Decryptor是一个解密的公钥密码系统在文件rsa.h 有如下定义:

    typedef RSAES<OAEP<SHA>>::Decryptor RSAES_OAEP_SHA_Decryptor; 就是在这个类用前面产生的伪随机数和密钥长度keyLength生解密的密钥
    接着,通过类FileSink打开文件szPrivateKeyFileName实行序列化操作,用HexEncoder把它转换为十六进制
    最后,用DEREncode把上面处理好的密码对象写入文件

    这样就得到私钥密码的密钥文件了。产生公钥文件的方法和产生私钥密码文件不同的地方就是使用了RSAES_OAEP_SHA_Encryptor是一个加密的公钥密码系统, 在文件rsa.h 有如下定义:
    typedef RSAES<OAEP<SHA>>::Encryptor RSAES_OAEP_SHA_Encryptor; 是用上面产生的密钥密码系统priv来生成相应公钥。
    2. 使用RSA公钥加密数据实现原理RSA公钥加密可以分成以下两种形式:一种是公钥存储在文件中,加密时,从文件获取密钥;另一种是公钥存储在程序中,直接传递存储密钥的地址。虽然,这两种形式只是公钥传递的形式上的区别,RSA加密的原理还是一样的。但,从编程的角度,把这两个进行区分。
    先介绍公钥存储在文件中的方式,那么RSA公钥加密的实现原理是:

    首先用类RandomPool在种子seed下用方法Put()产生伪随机数,Seed可以任取
    用类FileSource对公钥文件pubFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型
    然后用FileSource的对象pubFile 实例化公钥密码系统RSAES_OAEP_SHA_Encryptor生成对象pub
    用类StringSink 把outstr添加到一个String对象,接着用HexEncoder把这个对象转换为十六进制
    然后用伪随机数randPool、公钥密码系统pub和十六进制的String对象实例化一个公钥密码加密的过滤器,再用这个过滤器对字符串message进行加密把结果放到十六进制的字符串result里,这样就完成了对字符串的加密

    那么,对于另一种公钥存储在程序中的方式,RSA公钥加密的原理和上面的区别,只是在第 2 步的区别,也就是用类StringSource对公钥文件pubFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型。
    3. 使用RSA私钥钥解密数据实现原理同样,对应上述的公钥加密,私钥解密同样区分两种私钥获取的形式。
    先介绍私钥存储在文件中的方式,那么RSA私钥解密的实现原理的基本流程跟加密的基本流程差不多,就使用了几个不同的类,但是这些类跟加密函数的对应类的功能是相对的,很容易理解。

    用类FileSource对私钥文件privFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型
    然后用FileSource的对象privFile 实例化公钥密码系统RSAES_OAEP_SHA_Decryptor生成对象priv
    用类StringSink 把outstr添加到一个String对象,接着用HexEncoder把这个对象转换为十六进制
    然后用伪随机数randPool、私钥密码系统prov和十六进制的String对象实例化一个私钥密码解密的过滤器,再用这个过滤器对字符串message进行解密把结果放到十六进制的字符串result里,这样就完成了对字符串的解密

    和加密一样,对于另一种私钥存储在程序中的方式,RSA私钥解密的原理和上面的区别,只是在第 1 步的区别,也就是用类StringSource对公钥文件provFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型。
    编程实现1. 导入Crypt++库文件//*************************************************// crypt++加密库的头文件和静态库//*************************************************#include "crypt\\include\\rsa.h"#include "crypt\\include\\randpool.h"#include "crypt\\include\\hex.h"#include "crypt\\include\\files.h"using namespace CryptoPP; // 命名空间#ifdef _DEBUG #ifdef _WIN64 #pragma comment(lib, "crypt\\lib\\x64\\debug\\cryptlib.lib") #else #pragma comment(lib, "crypt\\lib\\x86\\debug\\cryptlib.lib") #endif#else #ifdef _WIN64 #pragma comment(lib, "crypt\\lib\\x64\\release\\cryptlib.lib") #else #pragma comment(lib, "crypt\\lib\\x86\\release\\cryptlib.lib") #endif#endif//*************************************************
    2. RSA产生公私钥实现BOOL GenerateRSAKey(DWORD dwRSAKeyLength, char *pszPrivateKeyFileName, char *pszPublicKeyFileName, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); // 生成RSA私钥 RSAES_OAEP_SHA_Decryptor priv(randPool, dwRSAKeyLength); HexEncoder privFile(new FileSink(pszPrivateKeyFileName)); // 打开文件实行序列化操作 priv.DEREncode(privFile); privFile.MessageEnd(); // 生成RSA公钥 RSAES_OAEP_SHA_Encryptor pub(priv); HexEncoder pubFile(new FileSink(pszPublicKeyFileName)); // 打开文件实行序列化操作 pub.DEREncode(pubFile); // 写密码对象pub到文件对象pubFile里 pubFile.MessageEnd(); return TRUE;}
    3. RSA公钥加密实现1> 公钥存储在文件实现string RSA_Encrypt_ByFile(char *pszOriginaString, char *pszPublicKeyFileName, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); FileSource pubFile(pszPublicKeyFileName, TRUE, new HexDecoder); RSAES_OAEP_SHA_Encryptor pub(pubFile); // 加密 string strEncryptString; StringSource(pszOriginaString, TRUE, new PK_EncryptorFilter(randPool, pub, new HexEncoder(new StringSink(strEncryptString)))); return strEncryptString;}
    2> 公钥存储在程序中string RSA_Encrypt_ByMem(char *pszOriginaString, char *pszMemPublicKey, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); StringSource pubStr(pszMemPublicKey, TRUE, new HexDecoder); RSAES_OAEP_SHA_Encryptor pub(pubStr); // 加密 string strEncryptString; StringSource(pszOriginaString, TRUE, new PK_EncryptorFilter(randPool, pub, new HexEncoder(new StringSink(strEncryptString)))); return strEncryptString;}
    4. RSA私钥解密实现1> 公钥存储在文件实现string RSA_Decrypt_ByFile(char *pszEncryptString, char *pszPrivateKeyFileName){ FileSource privFile(pszPrivateKeyFileName, TRUE, new HexDecoder); RSAES_OAEP_SHA_Decryptor priv(privFile); string strDecryptString; StringSource(pszEncryptString, TRUE, new HexDecoder(new PK_DecryptorFilter(GlobalRNG(), priv, new StringSink(strDecryptString)))); return strDecryptString;}
    2> 公钥存储在程序中string RSA_Decrypt_ByMem(char *pszEncryptString, char *pszMemPrivateKey){ StringSource privStr(pszMemPrivateKey, TRUE, new HexDecoder); RSAES_OAEP_SHA_Decryptor priv(privStr); string strDecryptString; StringSource(pszEncryptString, TRUE, new HexDecoder(new PK_DecryptorFilter(GlobalRNG(), priv, new StringSink(strDecryptString)))); return strDecryptString;}
    程序测试在main函数中调用上面封装好的函数进行测试,main函数为:
    char g_szPubKey[] = "30819D300D06092A864886F70D010101050003818B0030818702818100F0CE882D7CCB990323A6DB1B775EBE8F2910BFE75B4B580EF8C5089BB25FEDEEABCE2BBD2AC64A138E47F96A6C39152FE98067C0B4F5DC28F8D9394325ADB12A90A9598FF7A2A7211DEF974FC8A005D0CBCDE059FB8F7F9D214C5BAC2532CEB8EC4041AEAB19E80B8C4020F4A50102F9E738647E2384EA2FCD30C3681559CF6F020111";char g_szPrivKey[] = "30820275020100300D06092A864886F70D01010105000482025F3082025B02010002818100F0CE882D7CCB990323A6DB1B775EBE8F2910BFE75B4B580EF8C5089BB25FEDEEABCE2BBD2AC64A138E47F96A6C39152FE98067C0B4F5DC28F8D9394325ADB12A90A9598FF7A2A7211DEF974FC8A005D0CBCDE059FB8F7F9D214C5BAC2532CEB8EC4041AEAB19E80B8C4020F4A50102F9E738647E2384EA2FCD30C3681559CF6F020111028180210D49E8203005F15F3F0F03C5170B18AB4892CF70EC39434F52426FB91C39C162E0100AE7C0DCFDAA1DF50E9B67351AA7942251AA68051EB8BE7145739A599220030CF5E35ED4DEA41DD6E955722AE46153339FE7417BD00ADF53B368EAB6E71FAE0F7F394A34C91612B0F11AEC5525DB84DD982E6BF10CE74F177FA51ADC51024100F80296900AF134CCC5AC12C58D741C735F5EE9CBDFB8C1B1EB039BF078E37B09322074193B7B0AE5A60B544DDDB9159294E91744404A2C7CDF96287F5483D691024100F8908925066C3ED9AC8EAFE63A59D56FCBEC354A3DD513489DEDA70E42338CD2AEBDEEF685148123B31A55CA27B2A59CA53E2352DA284F30585A5D6B571245FF02410091E367A0066FC4B4B083565616F901AD4728C5C3384E900E4E021F7E653A849BFF5E6269320C24871661046A09F4670AEE2EC264620D8394BFC1BD781398D891024057BA8AC1C608162EB55F896050D46972C0717C38520EF7BF46CC5914175D7CFF107F4547F2BBF157E4DC1E47594E1C55677F57C2E395C19897A76C44009D09A5024100BBB92D3E8776B52FA20303E39FE8AE862637BB75880D82C6580C3217445C4A95BFB6E94120AD62AADC313418A350FF21B0ED861848626CC0F55936F750B44FC4";int _tmain(int argc, _TCHAR* argv[]){ char szPrivateFile[] = "privatefile"; char szPublicFile[] = "publicfile"; char szSeed[] = "WriteBugWriteBug"; char szOriginalString[] = "WRITE-BUG技术共享平台 - 一个专注校园计算机技术交流共享的平台"; /* 密钥在文件方式 */ // 生成RSA公私密钥对 GenerateRSAKey(1024, szPrivateFile, szPublicFile, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA公钥加密字符串 string strEncryptString = RSA_Encrypt_ByFile(szOriginalString, szPublicFile, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA私钥解密字符串 string strDecryptString = RSA_Decrypt_ByFile((char *)strEncryptString.c_str(), szPrivateFile); // 显示 printf("原文字符串:\n[%d]%s\n", ::lstrlen(szOriginalString), szOriginalString); printf("密文字符串:\n[%d]%s\n", strEncryptString.length(), strEncryptString.c_str()); printf("解密明文字符串:\n[%d]%s\n", strDecryptString.length(), strDecryptString.c_str()); printf("\n\n"); /* 密钥在内存方式 */ // RSA公钥加密字符串 string strEncryptString_Mem = RSA_Encrypt_ByMem(szOriginalString, g_szPubKey, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA私钥解密字符串 string strDecryptString_Mem = RSA_Decrypt_ByMem((char *)strEncryptString_Mem.c_str(), g_szPrivKey); // 显示 printf("原文字符串:\n[%d]%s\n", ::lstrlen(szOriginalString), szOriginalString); printf("密文字符串:\n[%d]%s\n", strEncryptString_Mem.length(), strEncryptString_Mem.c_str()); printf("解密明文字符串:\n[%d]%s\n", strDecryptString_Mem.length(), strDecryptString_Mem.c_str()); system("pause"); return 0;}
    测试结果为:

    根据图片显示,数据成功被加密和解密,两种公钥存储形式测试均成功。
    总结使用Crypto++库的好处之一,就是方便易用,即使你不了解具体的加密算法,但你只要知道要使用的加密算法这个名称,依然可以做出使用相应的加密算法进行数据加解密。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-11-15 08:20:03

发送私信

我在一个迷惘的故事里,看着自己的人生演绎着百态

11
文章数
14
评论数
eject