恶意代码分析系列-几种常用技术(3)

简介: 恶意代码分析系列-几种常用技术(3)

介绍


病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等动作。通常植入和攻击是分开在不同模块之中,有些病毒木马具有模拟PE加载器的功能,它们把 DLLexe从内存中直接加载到病毒木马的内存中执行,不需要 LoadLibrary等现成的 API函数去操作,以此躲过杀毒软件的拦截检测。几乎所有的病毒木马程序都会用到自启动技术,当程序感染之后的第一件事往往不是破坏,而是隐藏自己和如何执行,即使马上启动注入模块发动攻击依然解决不了永久驻留的问题,在Windows系统下解决永久驻留的第一步就是伴随系统启动而启动,即开机自启动。设置为开机自启动之后即使关闭计算机病毒程序仍然可以在下次开机的时候随着系统启动,由系统加载到内存中运行,从而窃取用户数据和隐私。开机自启动技术是病毒木马至关重要的技术。


直接加载内存运行


很多病毒木马具有PE加载器的功能,他们把 DLL或者 EXE等文件直接从内存加载到病毒木马的内存中执行,不需要通过 LoadLibrary等现成的 API中去操作,以此躲过杀毒软件的拦截检测。
在程序需要动态的调用 DLL文件,内存加载运行技术可以把这些 DLL作为资源直接插入到自己的程序中,直接在内存中加载运行即可,不需要将 DLL释放到本地。内存直接加载运行技术需要对PE文件结构有较深的认识,明白PE格式中的导入、导出和重定位表的具体操作过程。PE加载器模拟PE文件的加载过程的核心就是对导入、导出和重定位表的操作过程。

  • 实现流程
  1. DLL文件中,根据PE结构获取其加载映像的大小 SizeofImage,并根据 SizeOfImage在自己的程序中申请可读、可写、可执行的内存,那么这块内存的首地址就是 DLL的加载基址
  2. 根据 DLL中的 PE结构获取其映像对齐大小 SectionAlignment,然后把 DLL文件数据按照 SectionAlignment复制到上述申请的可读、可写、可执行的内存中
  3. 根据PE结构的重定位表,重新对重定位进行修正
  4. 根据PE结构的导入表,加载所需的 DLL,并获取导入函数的地址并写入导入表中,修改 DLL加载基址 ImageBase
  5. 根据PE结构获取 DLL的入口地址,然后构造并调用 DllMain函数,实现 DLL加载。

在直接加载 EXE文件时,不需要构造 DllMain函数,而是根据PE结构获取 EXE的入口地址偏移 AddressOfEntryPoint并计算出入口地址,然后直接跳转到入口地址执行即可,而且对于 EXE文件来说重定位表并不是必须的,即使没有重定位表, EXE也可以正常运行。因为对于 EXE进程来说,进程最早加载的模块是 EXE模块,所以它可以按照默认的加载基址加载到内存。对于没有重定位表的程序,只能把它加载到默认的加载基址上,如果默认加载基址已被占用,则直接内存加载运行会失败。

  • 关键实现
// 模拟GetProcAddress获取内存DLL的导出函数
LPVOID MyGetProcAddress(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;
}
// 修改PE文件重定位表信息
BOOL DoRelocationTable(LPVOID lpBaseAddress)
{
  // 重定位表的结构:
  // DWORD sectionAddress, DWORD size (包括本节需要重定位的数据)
  //注意重定位表的位置可能和硬盘文件中的偏移地址不同,应该使用加载后的地址
  PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
  PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
  PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
  // 判断是否有 重定位表
  if ((PVOID)pLoc == (PVOID)pDosHeader)
  {
  // 重定位表 为空
    return TRUE;
  }
  while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //开始扫描重定位表
  {
    WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION));
    //计算本节需要修正的重定位地址的数目
    int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
    for (int i = 0; i < nNumberOfReloc; i++)
    {
      if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //这是一个需要修正的地址
      {
        // 32位dll重定位,IMAGE_REL_BASED_HIGHLOW
        // 对于x86的可执行文件,所有的基址重定位都是IMAGE_REL_BASED_HIGHLOW类型的。
        DWORD* pAddress = (DWORD *)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));
        DWORD dwDelta = (DWORD)pDosHeader - pNtHeaders->OptionalHeader.ImageBase;
        *pAddress += dwDelta;
      }
    }
    //转移到下一个节进行处理
    pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);
  }
  return TRUE;
}


设置自启动

 

 注册表

注册表相当于操作系统的数据库,记录着系统中方方面面的数据,其中也不   乏直接或者间接导致开机自启动的数据。下面是Run注册表中添加程序路径   的方式,实现开机自启。

  • 关键函数
WINADVAPI LSTATUS
APIENTRY
RegOpenKeyExW(
    _In_ HKEY hKey,  // 当前打开或者预定义的键
    _In_opt_ LPCWSTR lpSubKey, // 指向一个非中断字符串将要打开键的名称
    _In_opt_ DWORD ulOptions,  // 保留,必须设置为 0
    _In_ REGSAM samDesired,    // 对指定键希望得到的访问权限进行的访问标记
    _Out_ PHKEY phkResult      // 指向一个变量的指针,该变量保存打开注册表的句柄
    );
WINADVAPI LSTATUS
APIENTRY
RegSetValueExW(
    _In_ HKEY hKey,  // 指定一个已打开项的句柄,或者一个标准项名
    _In_opt_ LPCWSTR lpValueName, // 指向一个字符串的指针,该字符串包含了欲设置值的名称
    _Reserved_ DWORD Reserved,    // 保留值,必须强制为 0
    _In_ DWORD dwType,            // 指定将存储的数据类型
    _In_reads_bytes_opt_(cbData) CONST BYTE* lpData, 
    // 指向一个缓冲区,该缓冲区包含了为指定名称存储的数据
    _In_ DWORD cbData // 指定由lpData参数所指向的数据大小,单位是字节
    );
  • 实现思路

在Windows中提供了专门的开机自启动注册表,在每次开机完成后,他都会在这个注册表键下遍历键值,以获取键值中的程序路径,并创建进程自启动程序,所以,要想修改注册表实现自启动,只需要在这个注册表键下添加想要设置自启动程序的程序路径就可以了,这两个路径分别为:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run

另外在修改注册表的权限问题上,在编程实现上,

要修改 HKEY_LOCAL_MACHINE主键的注册表要求程序具有管理员的权限。

而修改 HKEY_CURRENT_USER主键的注册表,只需要用户默认权限就可以实现。

BOOL RegCurrentUser(char *lpFileName, char *lpValueName)
{
  // 默认权限
  HKEY hKey;
  // 打开注册表键
  RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey)
  // 修改注册表值,实现开机自启
  RegSetValueEx(hKey, lpszValueName, 0, REG_SZ, (BYTE *)lpszFileName, (1 + lstrlen(lpszFileName)))
  // 关闭注册表键
  RegCloseKey(hKey);
  return TRUE;
}
// 需管理员权限的路径
BOOL RegLocalMachine(char *lpFileName, char *lpValueName)
{
  // 管理员权限
  HKEY hKey;
  // 打开注册表键
  RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey)
  // 修改注册表值,实现开机自启
  RegSetValueEx(hKey, lpszValueName, 0, REG_SZ, (BYTE *)lpszFileName, (1 + lstrlen(lpszFileName)))
  // 关闭注册表键
  RegCloseKey(hKey);
  return TRUE;
}


快速启动目录


快速启动目录是一种实现起来最为简单的自启动方法,不用修改任何系统数据。

  • 函数介绍
BOOL SHGetSpecialFolderPathA(
  HWND  hwnd,    // 窗口所有者的句柄
  LPSTR pszPath, // 返回路径的缓冲区
  int   csidl,   // 系统路径的CSIDL标识
  BOOL  fCreate  // 指示文件夹是否要创建,false不创建 true创建
);
  • 实现原理

 Windows系统有自带的快速启动的文件夹,他是最为简单的自启动方式,只要把程序放入到这个快速启动文件夹中,系统在启动时就会自动地加载并运行相应的程序,实现开机自启动功能。

快速启动目录并不是一个固定地目录,每台计算机地目录都不相同,但是可以使用 SHGetSpecialFolderPath函数获取Windows系统中快速启动目录地路径,快速启动目录的 CSIDL标识值为 CSIDL_STARTUP


BOOL AutoRun_Startup(char *lpszSrcFilePath, char *lpszDestFileName){  BOOL bRet = FALSE;  char szStartupPath[MAX_PATH] = {0};  char szDestFilePath[MAX_PATH] = {0};  // 获取 快速启动目录 路径  bRet = SHGetSpecialFolderPath(NULL, szStartupPath, CSIDL_STARTUP, TRUE);  printf("szStartupPath=%s\n", szStartupPath);  if (FALSE == bRet)  {    return FALSE;  }  // 构造拷贝的 目的文件路径  wsprintf(szDestFilePath, "%s\\%s", szStartupPath, lpszDestFileName);  // 拷贝文件到快速启动目录下  bRet = CopyFile(lpszSrcFilePath, szDestFilePath, FALSE);  if (FALSE == bRet)  {    return FALSE;  }  return TRUE;}


系统服务


大多数在后台运行的系统服务是随着系统启动而启动的,系统进程自启动是通过创建系统服务并设置服务启动类型为自启动来实现的,下面来看一下创建系统服务进程的原理和实现。

  • 函数介绍

WINADVAPI SC_HANDLE
    // 建立一个到服务控制管理器的连接,并打开指定的数据库
WINAPI OpenSCManagerW(
    _In_opt_     LPCWSTR        lpMachineName,    
    //指向零终止字符串,指定目标计算机的名称
    _In_opt_     LPCWSTR        lpDatabaseName,   
    //指向零终止字符串,指定将要打开的服务控制管理数据库的名称  SERVICES_ACTIVE_DATABASE
    _In_         DWORD          dwDesiredAccess
    // 指向服务访问控制管理器的权限
    );
WINADVAPI SC_HANDLE
    // 创建一个服务对象,并将其添加到指定的服务控制管理器数据库中
WINAPI CreateServiceW(
    _In_        SC_HANDLE    hSCManager,    // 指向服务控制管理器数据库的句柄
    _In_        LPCWSTR     lpServiceName,  // 要安装服务的名称
    _In_opt_    LPCWSTR     lpDisplayName,  // 用户界面用来识别服务的显示名称
    _In_        DWORD        dwDesiredAccess, // 对服务的访问
    _In_        DWORD        dwServiceType,   // 服务类型
    _In_        DWORD        dwStartType,    // 服务启动项
    _In_        DWORD        dwErrorControl, 
    // 当该服务启动失败时,指定产生错误严重程度以及应采取的保护措施
    _In_opt_    LPCWSTR     lpBinaryPathName,
    // 服务程序的二进制文件,它完全限定路径。如果路径中包含空格,则必须引用它,以便能正确地解析
    _In_opt_    LPCWSTR     lpLoadOrderGroup, // 指向加载排序组的名称
    _Out_opt_   LPDWORD      lpdwTagId,  // 指定的组中唯一的标记值变量
    _In_opt_    LPCWSTR     lpDependencies, 
    // 空分隔名称的服务或加载顺序组系统在这个服务开始之前的双空终止数组的指针 
    _In_opt_    LPCWSTR     lpServiceStartName, // 该服务应运行的账户名称
    _In_opt_    LPCWSTR     lpPassword // 由lpServiceStartName参数指定的账户名的密码
    );
WINADVAPI SC_HANDLE
    // 打开一个已经存在的服务
WINAPI OpenServiceW(
    _In_  SC_HANDLE  hSCManager,     // 指向SCM数据库句柄
    _In_  LPCWSTR    lpServiceName,  // 要打开服务的名字
    _In_  DWORD      dwDesiredAccess // 指定服务权限
    );
WINADVAPI BOOL
    // 启动服务
WINAPI StartServiceW(
    _In_            SC_HANDLE            hService,
    // OpenService或者CreateService函数返回的服务句柄,需要有SERVICE_START
    _In_            DWORD                dwNumServiceArgs,
    // 下一个形参的字符串个数
    _In_reads_opt_(dwNumServiceArgs)
                    LPCWSTR             *lpServiceArgVectors
    // 传递给服务ServiceMain的参数,如果没有可以为NULL
    );
WINADVAPI BOOL
    // 将服务进程的主线程连接到服务控制管理器,该线程将作为调用过程的服务控制分派器线程
WINAPI StartServiceCtrlDispatcherW(
    _In_ CONST  SERVICE_TABLE_ENTRYW    *lpServiceStartTable
    // 指向SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的条目
    );
  • 实现思路

    通过 OpenSCManager函数打开服务控制管理器数据库并获取数据库的句       柄

WINADVAPI SC_HANDLE
    // 建立一个到服务控制管理器的连接,并打开指定的数据库
WINAPI OpenSCManagerW(
    _In_opt_     LPCWSTR        lpMachineName,    
    //指向零终止字符串,指定目标计算机的名称
    _In_opt_     LPCWSTR        lpDatabaseName,   
    //指向零终止字符串,指定将要打开的服务控制管理数据库的名称  SERVICES_ACTIVE_DATABASE
    _In_         DWORD          dwDesiredAccess
    // 指向服务访问控制管理器的权限
    );
WINADVAPI SC_HANDLE
    // 创建一个服务对象,并将其添加到指定的服务控制管理器数据库中
WINAPI CreateServiceW(
    _In_        SC_HANDLE    hSCManager,    // 指向服务控制管理器数据库的句柄
    _In_        LPCWSTR     lpServiceName,  // 要安装服务的名称
    _In_opt_    LPCWSTR     lpDisplayName,  // 用户界面用来识别服务的显示名称
    _In_        DWORD        dwDesiredAccess, // 对服务的访问
    _In_        DWORD        dwServiceType,   // 服务类型
    _In_        DWORD        dwStartType,    // 服务启动项
    _In_        DWORD        dwErrorControl, 
    // 当该服务启动失败时,指定产生错误严重程度以及应采取的保护措施
    _In_opt_    LPCWSTR     lpBinaryPathName,
    // 服务程序的二进制文件,它完全限定路径。如果路径中包含空格,则必须引用它,以便能正确地解析
    _In_opt_    LPCWSTR     lpLoadOrderGroup, // 指向加载排序组的名称
    _Out_opt_   LPDWORD      lpdwTagId,  // 指定的组中唯一的标记值变量
    _In_opt_    LPCWSTR     lpDependencies, 
    // 空分隔名称的服务或加载顺序组系统在这个服务开始之前的双空终止数组的指针 
    _In_opt_    LPCWSTR     lpServiceStartName, // 该服务应运行的账户名称
    _In_opt_    LPCWSTR     lpPassword // 由lpServiceStartName参数指定的账户名的密码
    );
WINADVAPI SC_HANDLE
    // 打开一个已经存在的服务
WINAPI OpenServiceW(
    _In_  SC_HANDLE  hSCManager,     // 指向SCM数据库句柄
    _In_  LPCWSTR    lpServiceName,  // 要打开服务的名字
    _In_  DWORD      dwDesiredAccess // 指定服务权限
    );
WINADVAPI BOOL
    // 启动服务
WINAPI StartServiceW(
    _In_            SC_HANDLE            hService,
    // OpenService或者CreateService函数返回的服务句柄,需要有SERVICE_START
    _In_            DWORD                dwNumServiceArgs,
    // 下一个形参的字符串个数
    _In_reads_opt_(dwNumServiceArgs)
                    LPCWSTR             *lpServiceArgVectors
    // 传递给服务ServiceMain的参数,如果没有可以为NULL
    );
WINADVAPI BOOL
    // 将服务进程的主线程连接到服务控制管理器,该线程将作为调用过程的服务控制分派器线程
WINAPI StartServiceCtrlDispatcherW(
    _In_ CONST  SERVICE_TABLE_ENTRYW    *lpServiceStartTable
    // 指向SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的条目
    );

系统服务程序的编写

自启动服务程序并不是普通的程序,而是要求程序创建服务入口点函数,否则,不能创建系统服务。

调用系统函数 StartServiceCtrlDispatcher将程序的主线程连接到服务控制管理程序,服务控制管理程序启动服务后,等待服务控制主函数调用 StartServiceCtrlDispatcher函数,如果没有调用该函数时设置服务入口点,则会报错。

服务程序 ServiceMain入口函数的代码

void __stdcall ServiceMain(DWORD dwArgc, char *lpszArgv)
{
  g_ServiceStatusHandle = RegisterServiceCtrlHandler(g_szServiceName, ServiceCtrlHandle);
  TellSCM(SERVICE_START_PENDING, 0, 1);
  TellSCM(SERVICE_RUNNING, 0, 0);
  while (TRUE)
  {
    Sleep(5000);
    DoTask();
  }
}


总结


关于自启动技术最常见的是第一种注册表,下面的相比于第一种有的适用行不是很强,或者编写比较麻烦。这种技术也是杀软重点监测的技术,对于杀软来说,只要守住“入口”,就可以将病毒木马挡在门外了,下一篇会继续探索一下隐藏技术,学习一点简单的免杀。


相关文章
|
4月前
|
SQL API Python
`bandit`是一个Python静态代码分析工具,专注于查找常见的安全漏洞,如SQL注入、跨站脚本(XSS)等。
`bandit`是一个Python静态代码分析工具,专注于查找常见的安全漏洞,如SQL注入、跨站脚本(XSS)等。
|
安全 API 开发工具
恶意代码分析系列-几种常用技术(1)
恶意代码分析系列-几种常用技术(1)
|
安全 API Windows
恶意代码分析系列-几种常用技术(2)
恶意代码分析系列-几种常用技术(2)
|
存储 算法 安全
​恶意代码之静态分析
​恶意代码之静态分析
|
SQL 安全 测试技术
[网络安全]SQL注入原理及常见攻击方法简析
一般而言,登录验证逻辑语句为: select * from 表名 where name(用户名)='$输入' and pass(密码)='$输入' 当数据表中同时存在输入的name和pass字段时,页面将回显登录成功。
760 0
|
安全 算法 编译器
逆向分析 工具、加壳、安全防护篇
作者主页:https://www.couragesteak.com/
逆向分析 工具、加壳、安全防护篇
|
JavaScript 安全 前端开发
漏洞发现:代码分析引擎 CodeQL
codeql 是一门类似 SQL 的查询语言,通过对源码(C/C++、C#、golang、java、JavaScript、typescript、python)进行完整编译,并在此过程中把源码文件的所有相关信息(调用关系、语法语义、语法树)存在数据库中,然后编写代码查询该数据库来发现安全漏洞(硬编码 / XSS 等)。
656 1
漏洞发现:代码分析引擎 CodeQL
|
Java
[恶意代码分析]恶意代码种类以及分析环境介绍
[恶意代码分析]恶意代码种类以及分析环境介绍
408 1
[恶意代码分析]恶意代码种类以及分析环境介绍
|
安全 前端开发 网络安全
渗透测试服务 针对CSRF漏洞检测与代码防御办法
XSS跨站以及CSRF攻击,在目前的渗透测试,以及网站漏洞检测中 ,经常的被爆出有高危漏洞,我们SINE安全公司在对客户网站进行渗透测试时,也常有的发现客户网站以及APP存在以上的漏洞,其实CSRF以及XSS跨站很容易被发现以及利用,在收集客户网站域名,以及其他信息的时候,大体的注意一些请求操作,前端输入,get,post请求中,可否插入csrf代码,以及XSS代码。
184 0
渗透测试服务 针对CSRF漏洞检测与代码防御办法
|
安全
代码执行漏洞原理/防御
原理 代码执行漏洞是指 攻击者利用 将字符串转化成代码的函数 , 进行代码注入
560 0
下一篇
无影云桌面