[From QQ空间]对 SDOJ 2.5 安全性的一点突发奇想

       SDOJ 2.5 eXtensible 的评测器会对编译后的程序进行 API 导入检查,以阻止调用可能危害系统安全的 API。但是呢,API 是可以动态加载的,而相关的 API在内存中的位置是固定的,这就产生了一个安全隐患,即可以利用 ShellCode 绕开 API 检查来攻击系统。
       于是乎,我们就必须另外想一个办法来解决此问题。由于每个进程的进程空间相互隔离,因此在一个进程空间里修改一个共用 DLL 的代码是不会影响到其他进程的。可能已经有人想到了,这就是 API 断点 + 调试跟踪技术!具体思路是:
        1、用调试方式运行被评测的程序
        2、在程序开始运行前,对需要被监视的 API 设置断点
        3、如果程序中包含 ShellCode,且 ShellCode 调用了被监视的 API,那么程序的运行就会被中断,此时评测器就可以处理此次操作,判断是继续执行还是立即中止运行。
        因此我写了一个小小的调试器(32位版的),用来监视程序对 API 的调用。如果技术成熟了,就可以把这个功能添加到评测器上。
        首先是要构造一个 API 列表,它包含了需要被监视的 API 的名字和位于的模块。我做的这个表比较复杂,它的每个 API 还带了参数,以便及时读取和观察。以下是我做的 API 列表的一部分:
N "kernel32.dll" WinExec(STR lpCmdLine, UINT uCmdShow);
AW "kernel32.dll" CreateProcess(STR lpApplicationName, STR lpCommandLine, PTR lpProcessAttributes, PTR lpThreadAttributes, BOOL bInheritHandles,UINT dwCreationFlags, PTR lpEnvironment, STR lpCurrentDirectory, PTR lpStartupInfo, PTR lpProcessInformation);
AW "advapi32.dll" CreateProcessAsUser(UINT hToken, STR lpApplicationName, STR lpCommandLine, PTR lpProcessAttributes, PTR lpThreadAttributes,BOOL bInheritHandles, UINT dwCreationFlags, PTR lpEnvironment, STR lpCurrentDirectory, PTR lpStartupInfo, PTR lpProcessInformation);
N "kernel32.dll" Sleep(UINT dwMilliseconds);
N "kernel32.dll" ExitProcess(UINT uExitCode);
AW "kernel32.dll" CreateFile(STR lpFileName, HEX dwDesiredAccess, UINT dwShareMode, PTR lpSecurityAttributes, UINT dwCreationDisposition, HEXdwFlagsAndAttributes, UINT hTemplateFile);
N "advapi32.dll" OpenProcessToken(UINT ProcessHandle, HEX DesiredAccess, PTR TokenHandle);
AW "advapi32.dll" LookupPrivilegeValue(STR lpSystemName, STR lpName, UINT64 lpLuid);
N "advapi32.dll" SetTokenInformation(UINT TokenHandle, UINT TokenInformationClass, UINT TokenInformation, UINT TokenInformationLength);
N "advapi32.dll" DuplicateTokenEx(UINT hExistingToken, HEX dwDesiredAccess, UINT lpTokenAttributes, UINT ImpersonationLevel, UINT TokenType,PTR phNewToken);
N "msvcrt.dll" system(STR _Command);
N "msvcrt.dll" exit(INT _Code);
N "msvcrt.dll" _onexit(PTR _Func);
N "msvcrt.dll" fwrite(STR _Str, UINT _Size, UINT _Count, PTR _File);
        某A:这怎么看着像是在声明函数?嗯。。这的确很像,因为我是按照 C 语言的风格写的。这其中 A、W、N代表着是否监视 API 函数的 ANSI 或 Unicode 版本,也就是是否监视“函数名+'A'” 或 “函数名+'W'”;紧跟着的红色的就是模块名;然后是 API 名,后面的是参数列表。这其中的 STR、PTR、UINT、BOOL 等不是参数类型,而是显示类型,最终这些参数的值会显示在输出窗口中。我专门编写了一个解析器来处理这个表。
        API 列表构造好了,就可以编写一个用于检验的小程序了。这个程序的代码如下(C++):
#include "windows.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
    cout<<"提升 Debug 权限"<<endl;
    HANDLE hObject;
    LARGE_INTEGER Luid;
    TOKEN_PRIVILEGES NewStatus, tmpTP;
    DWORD retLen;

    if(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hObject))
    {
        if(LookupPrivilegeValue(NULL, "SeDebugPrivilege", &Luid))
        {
            NewStatus.Privileges[0].Luid = Luid;
            NewStatus.PrivilegeCount = 1;
            NewStatus.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
            AdjustTokenPrivileges(hObject, FALSE, &NewStatus, 0, &tmpTP, &retLen);
        }
    }

    cout<<"Sleep 延时 1000ms"<<endl;
    Sleep(1000);

    cout<<"CreateProcessAsUser 打开命令提示符"<<endl;
    STARTUPINFO si={0};
    PROCESS_INFORMATION pi={0};
    si.cb=sizeof(si);
    si.dwFlags=STARTF_USESHOWWINDOW;
    si.wShowWindow=SW_NORMAL;

    HANDLE hToken, hDuplicatedToken;
    DWORD dwSessionId;

    if(OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken))
    {
        if(DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hDuplicatedToken))
        {
            CloseHandle(hToken);
            hToken = hDuplicatedToken;
            dwSessionId = 1;
            SetTokenInformation(hToken, TokenSessionId, &dwSessionId, sizeof(DWORD));
            CreateProcessAsUser(hToken, NULL, "cmd.exe", NULL, NULL, FALSE,
                                CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS,
                                NULL, NULL, &si, &pi);
        }
    }

    WinExec("notepad.exe", 1);
    system("pause");
    return 0;
}
        代码写好了,就可以编写调试器了。这里我就省略编写调试器的步骤。编写调试器不仅需要用到 Win32 调试 API,还要对 PE 结构进行操作,这些相关的资料都可以在网上找到。
        我们用调试器运行一下这个测试程序。由于有 system("pause"); 这个语句存在,程序的运行会中断一次,此时截图如下:
        我们可以看到程序已经正常地运行了。此时按下任意键,程序即结束运行。
        程序运行结束后,我们来看看调试器的输出窗口:
        通过和源代码进行对比,我们可以发现源代码中所有的 Win32 API 调用全部被监视了,且它们的参数都被列在了下方。这只是一个演示功能,只能监视,不能拦截。以后可以把拦截的功能加上,这样就又能提高 SDOJ 2.5 的安全性了。最后贴出输出窗口的内容(较长)。
已创建进程
已加载 ntdll.dll
已加载 C:\Windows\syswow64\kernel32.dll
已加载 C:\Windows\syswow64\KERNELBASE.dll
已加载 C:\Windows\syswow64\ADVAPI32.DLL
已加载 C:\Windows\syswow64\msvcrt.dll
已加载 C:\Windows\SysWOW64\sechost.dll
已加载 C:\Windows\syswow64\RPCRT4.dll
已加载 C:\Windows\syswow64\SspiCli.dll
已加载 C:\Windows\syswow64\CRYPTBASE.dll
系统 API 调用:_onexit 发生在模块 msvcrt.dll 位于地址 758B5BC4
_Func = 0x0040CEB0
系统 API 调用:fwrite 发生在模块 <主程序> 位于地址 004116FB
_Str = "提升 Debug 权限"
_Size = 1
_Count = 15
_File = 0x758F2920
系统 API 调用:OpenProcessToken 发生在模块 <主程序> 位于地址 00401406
ProcessHandle = 4294967295
DesiredAccess = 00000028
TokenHandle = 0x0028FF3C
系统 API 调用:LookupPrivilegeValueA 发生在模块 <主程序> 位于地址 00401428
lpSystemName = NULL
lpName = "SeDebugPrivilege"
lpLuid = 23170764446367952
系统 API 调用:LookupPrivilegeValueW 发生在模块 ADVAPI32.DLL 位于地址 75CEB60E
lpSystemName = ""
lpName = "SeDebugPrivilege"
lpLuid = 23170764446367952
已创建线程 00000298
系统 API 调用:AdjustTokenPrivileges 发生在模块 <主程序> 位于地址 00401479
TokenHandle = 72
DisableAllPrivileges = FALSE
NewState = 0x0028FF20
BufferLength = 0
PreviousState = 0x0028FF10
ReturnLength = 0x0028FF0C
系统 API 调用:fwrite 发生在模块 <主程序> 位于地址 004116FB
_Str = "Sleep 延时 1000ms"
_Size = 1
_Count = 17
_File = 0x758F2920
系统 API 调用:Sleep 发生在模块 <主程序> 位于地址 004014AC
dwMilliseconds = 1000
系统 API 调用:fwrite 发生在模块 <主程序> 位于地址 004116FB
_Str = "CreateProcessAsUser 打开命令提示符"
_Size = 1
_Count = 34
_File = 0x758F2920
系统 API 调用:OpenProcessToken 发生在模块 <主程序> 位于地址 00401541
ProcessHandle = 4294967295
DesiredAccess = 000F00FF
TokenHandle = 0x0028FE9C
系统 API 调用:DuplicateTokenEx 发生在模块 <主程序> 位于地址 00401584
hExistingToken = 176
dwDesiredAccess = 02000000
lpTokenAttributes = 0
ImpersonationLevel = 1
TokenType = 1
phNewToken = 0x0028FE98
系统 API 调用:SetTokenInformation 发生在模块 <主程序> 位于地址 004015DE
TokenHandle = 180
TokenInformationClass = 12
TokenInformation = 2686612
TokenInformationLength = 4
系统 API 调用:CreateProcessAsUserA 发生在模块 <主程序> 位于地址 00401643
hToken = 180
lpApplicationName = NULL
lpCommandLine = "cmd.exe"
lpProcessAttributes = NULL
lpThreadAttributes = NULL
bInheritHandles = FALSE
dwCreationFlags = 48
lpEnvironment = NULL
lpCurrentDirectory = NULL
lpStartupInfo = 0x0028FEB0
lpProcessInformation = 0x0028FEA0
已加载 C:\Windows\SysWOW64\apphelp.dll
已加载 C:\Windows\SysWOW64\apphelp.dll
系统 API 调用:WinExec 发生在模块 <主程序> 位于地址 0040165A
lpCmdLine = "notepad.exe"
uCmdShow = 1
系统 API 调用:CreateProcessA 发生在模块 kernel32.dll 位于地址 74ED2F1E
lpApplicationName = NULL
lpCommandLine = "notepad.exe"
lpProcessAttributes = NULL
lpThreadAttributes = NULL
bInheritHandles = FALSE
dwCreationFlags = 0
lpEnvironment = NULL
lpCurrentDirectory = NULL
lpStartupInfo = 0x0028FDB8
lpProcessInformation = 0x0028FE20
已加载 C:\Windows\SysWOW64\apphelp.dll
系统 API 调用:system 发生在模块 <主程序> 位于地址 00401669
_Command = "pause"
系统 API 调用:CreateProcessA 发生在模块 msvcrt.dll 位于地址 758AB423
lpApplicationName = "C:\Windows\system32\cmd.exe"
lpCommandLine = "C:\Windows\system32\cmd.exe /c pause"
lpProcessAttributes = NULL
lpThreadAttributes = NULL
bInheritHandles = TRUE
dwCreationFlags = 0
lpEnvironment = NULL
lpCurrentDirectory = NULL
lpStartupInfo = 0x0028FD3C
lpProcessInformation = 0x0028FD80
已加载 저팏
已创建线程 00000118
已创建线程 000003C8
已创建线程 00000200
系统 API 调用:ExitProcess 发生在模块 <主程序> 位于地址 004011F6
uExitCode = 0
进程已退出,退出代码: 0

发表评论

电子邮件地址不会被公开。 必填项已用*标注