SandBoxie循序渐进耳监控篇上
Sandboxie监控篇笔记太长,引擎打算重写(利于二次开发),业余时间研究会久一些,分为上下篇分享笔记。
上篇主要自编译沙盘部署(合规过程),文件举例回调/r3dll注入分析,沙盘内部构建MSG通信分析(自行添加消息),基于MFC二次开发(抛砖引玉)。
下篇主要接管资源监控数据和重写部分监控引擎,添加hook第三方接口。
本篇是进程监控笔记,以功能为出发点,进一步探讨实现原理与数据应用,参杂排错思路和开发相关知识。
David.SandboxiePlush增添了许多新功能,新鲜感的界面和多交互,确实是不错的一面。
SandBoxiePlush界面
本文基于原生的Sandboxie添加API监视模块,准备改造如下(后续会改进,危险操作将会标红):
沙盘编译部署
进程篇基于调试学习和理解,本篇需要自己实现编译且正常执行,不推荐使用脚本集成化,最好能够理解和学习每一个环节,过程无疑是比较麻烦的,这里补贴一张r3进程篇流程图。
r3-process-run
Release编译全部工程,编译期间如果遇到lib异常,请参考第一篇文章引荐的解决方案,根据错误提示解决问题,目标是能够正常运行自编译的Sandboxie即可:
编译完成后直接运行SbieCtrl.exe会发生错误,SbieSvc.exe需要以服务的方式启动,SbieDrv这时候也没被加载。
后来查看了安装脚本和David-SandMain代码启动流程(保证每个步骤都合规),依赖于KmdUtil进行加载,sandboxie作者已写好模块,只需要调用即可。
SbieControl:
1) 如果本机已安装Sandboxie,调试可能会出现版本不匹配,这时候SbieSvc服务和驱动已启动,可以进行LPC查询版本号,需要卸载已安装的Sandboxie,如下所示:
Clinet:
版本检测CMyApp::InitInstance().CInitWait initwait(this);将会通过发送请求MSGID_SBIE_INI_GET_VERSION判断组件之间版本是否一致.
Server:
PipServer.*SbieIniServer::Handler2处理MSGID_SBIE_INI_GET_VERSION if (msg->msgid == MSGID_SBIE_INI_GET_VERSION) { return GetVersion(msg); }
GetVersion查询当前版本,MY_VERSION_STRING宏声明了版本号
MSG_HEADER *SbieIniServer::GetVersion(MSG_HEADER *msg) { WCHAR ver_str[16]; wsprintf(ver_str, L"%S", MY_VERSION_STRING); // MY_VERSION_STRING = "5.40" ULONG ver_len = wcslen(ver_str); ULONG rpl_len = sizeof(SBIE_INI_GET_USER_RPL) + (ver_len + 1) * sizeof(WCHAR); SBIE_INI_GET_VERSION_RPL *rpl = (SBIE_INI_GET_VERSION_RPL *)LONG_REPLY(rpl_len); if (! rpl) return SHORT_REPLY(STATUS_INSUFFICIENT_RESOURCES); wcscpy(rpl->version, ver_str); rpl->version_len = ver_len; return &rpl->h; }
检测版本号不匹配,弹窗提示,因为本机安装发行版是v5.33,源码版本是v5.4,自编译的程序应该都是v5.4版本,不会出现上述问题。
版本号校验实际开发中是必要的,多版本迭代,多个组件功能增添和优化,当新版发布后,客户只更新部分组件或某些原因更新失败,新老版参杂运行可能因接口已改动等问题造成很大的风险.
2) 指定的服务未安装,如下所示:
Client:
SbieCtrl.CMyApp::InitInstance().CInitWait.initwait(this).SbieDll_StartSbieSvc(BOOLEAN retry).SbieDll_ConnectPort().当SbieSvc未启动的时候,data->ProtHandle没有句柄.
if (! data->PortHandle) { BOOLEAN Silent = (req->msgid == MSGID_SBIE_INI_GET_VERSION || req->msgid == MSGID_SBIE_INI_GET_USER || req->msgid == MSGID_PROCESS_CHECK_INIT_COMPLETE); if (! SbieDll_ConnectPort(Silent)) return NULL;
调用SbieDll_ConnectPort()连接服务端,如下所示:
RtlInitUnicodeString(&PortName, SbieDll_PortName()); // 连接服务端 status = NtConnectPort( &data->PortHandle, &PortName, &QoS, NULL, NULL, &data->MaxDataLen, NULL, NULL); if (! NT_SUCCESS(status)) { if (! ErrorReported) { if (! Silent) SbieApi_Log(2203, L"connect %08X", status); ErrorReported = TRUE; } // 连接失败返回 return FALSE; }
如上图所示,指定服务未安装,SbieControl.exe启动过程中,默认SbieSvc服务是已加载,Server开启监听等待客户端连接,如果Server未启动,SbieControl不会做加载操作.
基于原生Sandboxie添加代码,连接服务失败则重新服务加载,为了有交互性,简单弹窗或Dlg告知用户是否需要加载SbieSvc和Drv(只提示一次),当然也可以用脚本完成服务安装和驱动安装,这里通过添加代码熟悉每一个环节,如下所示:
status = NtConnectPort( &data->PortHandle, &PortName, &QoS, NULL, NULL, &data->MaxDataLen, NULL, NULL); if (! NT_SUCCESS(status)) { // 如果失败,提示是否加载服务和驱动 只提示一次安装服务比较合适,nServiceloadflag标志是否第一次提示 if (!nServiceloadflag && (IDYES == MessageBoxW(NULL, L"是否重新加载服务和驱动", L"RPC-Server Connect失败", MB_OK | MB_YESNOCANCEL))) { // 加载...... MessageBoxW(NULL, L"加载成功", L"Waring", NULL); } else { nServiceloadflag = TRUE; if (!ErrorReported) { if (!Silent) SbieApi_Log(2203, L"connect %08X", status); ErrorReported = TRUE; } return FALSE; } }
这只是一个最简单的示例,如果点击是(Y)则加载驱动和服务,如下所示:
KimUtil:
编写服务管理模块,阅读KimUtil源码,它负责SbieDrv和SbieSvc生命周期管理,ReadMe.txt介绍如下:
KmdUtil (\install\kmdutil). Builds KmdUtil.exe which is used during the installtion process. E.g. to start/stop the Sbie driver (SbieDrv.sys).
// Cmdline获取控制码 if (! Parse_Command_Line( &Command, &Driver_Name, &Driver_Path, &Driver_Display, &Driver_MsgFile, &Driver_Altitude, &Driver_Group, &Options)) return EXIT_FAILURE; // 卸载 if (Command == CMD_DELETE) { ok = Kmd_Delete_Service(Driver_Name); if (ok) ok = Kmd_Unregister_Event_Source(Driver_Name); ok = TRUE; // don't let the calling installer fail } // 安装 if (Command == CMD_INSTALL) { ok = Kmd_Install_Service( Driver_Name, Driver_Path, Driver_Display, Driver_Group, Options); if (ok) { if (! Driver_MsgFile) Driver_MsgFile = Driver_Path; ok = Kmd_Register_Event_Source(Driver_Name, Driver_MsgFile); if (ok && Driver_Altitude) ok = Kmd_Register_MiniFilter(Driver_Name, Driver_Altitude); if (! ok) { Kmd_Unregister_Event_Source(Driver_Name); Kmd_Delete_Service(Driver_Name); } } } // 启动 if (Command == CMD_START) ok = Kmd_Start_Service(Driver_Name); // 停止 if (Command == CMD_STOP) ok = Kmd_Stop_Service(Driver_Name);
参考David.SandboxiePlush管理模块用法,初始化源码如下:
void CSbieUtils::Install(EComponent Component, QStringList& Ops) { QString HomePath = QCoreApplication::applicationDirPath().replace("/", "\\"); // "C:\\Program Files\\Sandboxie " if ((Component & eDriver) != 0 && GetServiceStatus(SBIEDRV) == 0) Ops.append(QString::fromWCharArray(L"kmdutil.exe|install|" SBIEDRV L"|") + "\"" + HomePath + "\\" + QString::fromWCharArray(SBIEDRV_SYS) + "\"" + "|type=kernel|start=demand|altitude=86900"); if ((Component & eService) != 0 && GetServiceStatus(SBIESVC) == 0) { Ops.append(QString::fromWCharArray(L"kmdutil.exe|install|" SBIESVC L"|") + "\"" + HomePath + "\\" + QString::fromWCharArray(SBIESVC_EXE) + "\"" + "|type=own|start=auto|display=\"Sandboxie Service\"|group=UIGroup"); Ops.append("reg.exe|ADD|HKLM\\SYSTEM\\ControlSet001\\Services\\SbieSvc|/v|PreferExternalManifest|/t|REG_DWORD|/d|1"); } }
为了方便调试,可以使用cmd就可以完成安装/启动/停止等工作,循序不能乱,先加载驱动,梳理如下所示:
Cmd runing: 1. kmdutil.exe install SbieDrv "{path}\SbieDrv.sys" type=kernel start=demand altitude=86900 2. kmdutil.exe install SbieSvc "{path}\SbieSvc.exe" type=own start=auto display="Sandboxie Service" group=UIGroup 3. reg.exe ADD HKLM\SYSTEM\ControlSet001\Services\SbieSvc.exe /v PreferExternalManifest /t REG_DWORD /d 1 4. Kmdutil start SbieDrv 5. Kmdutil start SbieSvc
如果没有签名就会出现无法验证数字,解决方案,简单粗暴开机F8,如下:
驱动加载成功之后,fltmc可以查看已加载的实列和高度
服务加载成功后如下:
如果开启SbieSvc服务遇到了拒绝访问等权限问题,先核查注册表可执行路径,确认是否因SbieSvc.exe绝对路径导致问题。在排查目录权限,百度自行解决即可(注意先启动驱动/后启动服务)。
给原生Sandboxie添加服务/驱动管理代码,至此SbieCtrl.exe界面运行成功。
Error1: 运行后发现无法正常执行,会出现配置文件错误,如下所示:
解决方案:拷贝install\Templates.ini文件至沙箱编译运行目录下即可。
Error2: 执行进程再次遇到错误,如下所示:
找不到对象名称?ALPC连接失败?如何出现的这个错误?当前项目共找到三处MSG_2101调用处,但是根据上下文只有一处命中,错误源于Syscall_CheckObject函数的检测,源码如下:
if ((status != STATUS_SUCCESS) && (status != STATUS_BAD_INITIAL_PC)) { WCHAR msg[256]; swprintf(msg, L"%S (%08X) access=%08X initialized=%d", syscall_entry->name, status, HandleInfo->GrantedAccess, proc->initialized); Log_Msg(MSG_2101, msg, Name != NULL ? Name->Name.Buffer : L"Unnamed object"); }
解决方案:
SbieDrv添加断点,编译安装,Name值为空,ALPC未连接成功,alpc意味着可能未初始化,该问题上两周没有找到解决方案,因为时间花在了引擎二次开发和界面改进,有时间的朋友可以研究。
控制权限
抛砖引玉
本篇以文件操作举例,使用Hook API, fsd hook,disk hook,Ntfs(MFT)篡改都可以实现文件权限控制(可读,可写,可访问)重定向。
沙盘已实现限制功能,可控制多进程之间的权限管理,Sandboxie自带资源访问监控,如下所示:
本篇以文件限制/资源访问两项功能为主线,进一步学习源码,分析实现原理,添加更直观的数据展示。
测试demo:
CreateFile(OPEN_ALWAYS)-->Userinput-->WriteFile-->CloseHand-->CreateFile(OPEN_EXISTING)-->ReadFile做为API序列。
文件限制
1) 为了测试沙盘隔离效果,本机C盘创建1.txt文件,并且写入字符,然后Sandboxie执行Demo,创建1.txt并且写入输入的字符,如下图所示,Demo执行之后,本地1.txt没有发生任何改变:
2) 沙盘目录下已创建了C\1.txt,且写入了Demo中输入的字符串,这就是重定向。
文件权限:
如何去构造进程监控?r3层跨进程hook依赖远程线程注入Dll或shellcode,对目标进程API挂钩实现过滤。Sandboxie如何实现重定向和监控呢?一起寻找答案如果windbg过程中寻找答案,那将是非常耗费精力的事情,善于搜索源码,Sandboxie以各类消息进行通信,注入也会有消息定义,可以搜索关键字。
enum { SVC_FIRST = 0x23450000L, SVC_LOOKUP_SID, SVC_INJECT_PROCESS, SVC_CANCEL_PROCESS, SVC_UNMOUNT_HIVE, SVC_LOG_MESSAGE, SVC_RESTART_HOST_INJECTED_SVCS, SVC_LAST };
其中SVC_INJECT_PROCESS/SVC_RESTART_HOST_INJECTED_SVCS字面意思很明显,服务注入进程,通过代码搜索调用函数及回溯。进程篇发现执行过程中r3没有做hook代码?如何实现进程监控呢?这里阐述驱动初始化过程(文件),一开始是想放到原理篇(SbieDrv完整性分析)。
1. _FX NTSTATUS DriverEntry()SbieDrv驱动入口点,驱动加载时候将会执行方法,Process_Init()
// // initialize modules. these place hooks into the system. hooks // become active as soon as installed. the Process module must be // initialized first, because it initializes the process list // if (ok) ok = Process_Init();
2. Process_Init()函数函数负责注册回调,PsSetCreateProcessNotifyRoutine ,进程创建/结束将会调用回调函数Process_NotifyProcess()
// // install process notify routines // status = PsSetCreateProcessNotifyRoutine(Process_NotifyProcess, FALSE);
3. Process_NotifyProcess()函数进程创建将会处理Process_NotifyProcess_Create()
if (ProcessId) { if (Create) { if (ParentId) { Process_NotifyProcess_Create(ProcessId, ParentId, NULL); } } else { Process_NotifyProcess_Delete(ProcessId); } }
4. 负责进程相关的结构填充,通过Process_Low_Inject发送SVC_INJECT_PROCESS至SbieSvc告知注入,等待SbieSvc的挂钩处理。
Process_Low_Inject( pid, session_id, create_time, nptr1, add_process_to_job, bHostInject); if (! Api_SendServiceMessage(SVC_INJECT_PROCESS, sizeof(msg), &msg)) status = STATUS_SERVER_DISABLED;
5. SbieSvc接收到消息之后,DriverAssist.cpp.MsgWorkerThread开始工作,如下:
else if (msgid == SVC_INJECT_PROCESS) { InjectLow(data_ptr); } else if (msgid == SVC_RESTART_HOST_INJECTED_SVCS) { RestartHostInjectedSvcs(); }
6. 响应SVC_INJECT_PROCESS,call InjectLow负责处理和目标进程注入,最后通知注入完成。
if (SbieApi_CallOne(API_INJECT_COMPLETE, msg->process_id) == 0) errlvl = 0; else errlvl = 0x99;
7. svc注入进程其实与平常注入无二样,通过VirtualProtectEx/WriteProcessMemory来写入目标进程,它选择覆盖了LdrInitializeThunk函数来挂钩执行。
sandboxie-master\core\svc\DriverAssistInject.cpp有具体hook过程,包括判断了是否已经被hook等处理,这里将不全部刨析。dll注入后如何监控呢,dll源码寻找初始化hook的痕迹,如下所示:
Sboxdll.file_init
#define SBIEDLL_HOOK(pfx,proc) \ *(ULONG_PTR *)&__sys_##proc = (ULONG_PTR) \ SbieDll_Hook(#proc, proc, pfx##proc); \ if (! __sys_##proc) return FALSE;
SbieDll_Hook函数是实现r3-inlinehook的主要模块,如下所示:
函数原型可知,参数一标识函数名,参数二/三分别是原函数地址和过滤函数地址。
Sandboxie实现inlinehook,经过了以下几个步骤(x32举例):
1) 函数源地址参数校验 if (! SourceFunc) { SbieApi_Log(2303, _fmt1, SourceFuncName, 1); return NULL; }
2) 检测沙箱hook支持,x32/x64不同,找到函数hook的点。
UCHAR *func = (UCHAR *)SourceFunc; if (func[0] == 0xB8 && // mov eax,? func[5] == 0xBA && // mov edx,? *(USHORT *)&func[10] == 0xE2FF) // jmp edx { ULONG i = 0; ULONG *longs = *(ULONG **)&func[6]; for (i = 0; i < 20; i++, longs++) { if (longs[0] == 0x5208EC83 && longs[1] == 0x0C24548B && longs[2] == 0x08245489 && longs[3] == 0x0C2444C7 && longs[5] == 0x042444C7) { SourceFunc = (void *)longs[4]; break; } } }
3) 检测是否已被挂钩,如果是0xEB(short jmp)
if (*(UCHAR *)SourceFunc == 0xEB) { signed char offset = *((signed char *)SourceFunc + 1); SourceFunc = (UCHAR *)SourceFunc + offset + 2; }
4) 0xE9(near jump)
while (*(UCHAR *)SourceFunc == 0xE9) { diff = *(LONG *)((ULONG_PTR)SourceFunc + 1); target = (ULONG_PTR)SourceFunc + diff + 5; if (target == (ULONG_PTR)DetourFunc) { SbieApi_Log(2303, _fmt1, SourceFuncName, 4); return NULL; }
5) 调用驱动来创建跳板
tramp = Dll_AllocCode128(); if (SbieApi_HookTramp(SourceFunc, tramp) != 0) { SbieApi_Log(2303, _fmt1, SourceFuncName, 2); return NULL;
}
6) inlinehook指针替换
func = (UCHAR *)SourceFunc; if (!VirtualProtect(&func[-8], 20, PAGE_EXECUTE_READWRITE, &prot)) { ULONG err = GetLastError(); SbieApi_Log(2303, _fmt2, SourceFuncName, 33, err); return NULL; } diff = (UCHAR *)DetourFunc - (func + 5); func[0] = 0xE9; // JMP DetourFunc *(ULONG *)(&func[1]) = (ULONG)diff; VirtualProtect(&func[-8], 20, prot, &dummy_prot); func = (UCHAR *)(ULONG_PTR)(tramp + 16);
整个过程与平常编写的inlinehook如出一辙,Sandboxie已商用化10几年,inlinehook稳定性/兼容性比较靠谱,按照固定格式添加hook的函数即可,或者修改已定义的过滤函数,实现格外的过滤功能。DLL几乎接管了常见的api,但WMI等不在这个范围内,需要格外的编写监控模块,添加到沙盘机制中。
CreateFile过滤函数
hook过程我没有调试,所以不一定严谨,想自己接管引擎在开发中,沙盘中的进程调用NtCreateFile,将会执行已被挂钩函数File_NtCreateFileImpl,如上图可以看到函数源码近1200行,Snadboxie作者考虑周全细致,学到了很多知识,详细请自行“食用”源码。
界面开发:
界面初始化流程
定位代码随便加入一行尝试是否生效,界面源码apps\control目录,添加测试属性如下,ProcListCtrl.Create()为例:
// 设置List表 CListCtrl::InsertColumn(0, CMyMsg(MSG_3517), LVCFMT_LEFT, width0, 0); CListCtrl::InsertColumn(1, CMyMsg(MSG_3518), LVCFMT_LEFT, width1, 0); CListCtrl::InsertColumn(2, CMyMsg(MSG_3519), LVCFMT_LEFT, width2, 0); CListCtrl::InsertColumn(3, L"测试", LVCFMT_LEFT, width2, 0);
这样添加不合理,会出问题,暂时先这样处理。资源访问控制功能设计模态,当监视窗口弹出无法对主界面做任何操作。apps\control\MonitorDialog.cpp,后面会详细介绍MSG自定义添加消息处理,如下所示.
注意,Dlg应该没有构建类,无法通过向导绑定资源变量(目前本机测试是这样),需要自己用CListCtrl类获取组件ID进行操作。MFC作者重写了控件类,CMyxxx标识为重构的类。上述ListCtrl添加完成后,属性ID=ID_MESSAGE_LIST_MONITORL,使用CListCtrl代码如下:
CRect rc; this->GetClientRect(rc); int nWidth = rc.Width(); CListCtrl *listctrl = (CListCtrl *)GetDlgItem(ID_MESSAGE_LIST_MONITORL); DWORD dwOldStyle; dwOldStyle = listctrl->GetExtendedStyle(); listctrl->SetExtendedStyle(dwOldStyle | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); listctrl->InsertColumn(0, L"监控类型", LVCFMT_CENTER, nWidth / 3); listctrl->InsertColumn(1, L"监控api", LVCFMT_CENTER, nWidth / 3); listctrl->InsertColumn(2, L"捕获数据", LVCFMT_CENTER, nWidth / 3);
如何构建非模态对话框呢,这是一个麻烦的过程,尝试后果断放弃,耗时费力不讨好,有更好的方案来代替。弃用Sandboxie作者写的监控窗口,移植到Master主页面,如何设计呢?具备可扩展性?考虑BaseDlg可伸缩,随意调整大小,不改动原有的界面,没有原型图,直接手绘就好了,按钮有点多余,可以添加到菜单栏,否则显得太突兀,点击即可显示,如开篇所示。
1. 第一步,添加菜单按钮,Api监视模块,一般都是绑定菜单处理,处理输入名称即可,该工程需要全部手工完成。
2. 上述编号3411~3451对应着主界面菜单,0x3461其实已被占用,5160~7000之间ID标识未被使用,添加的ID做好维护,菜单处理代码:
void CMyFrame::InitMenus(void) { // // create and customize main menu // CMenu *pMenu = CMyApp::MyLoadMenu(L"TOP_MENU");
遵循作者消息规范操作,通过查询Dll来获取对应的字符串。菜单初始化通过循环完成,TOP_MENU主菜单,MSG_3411消息号递增10(10个以内子菜单)。一开始以为基于msgs.h生成,几经折腾,Parse工程负责将本地txt生成mc,转换msgs.h文件,MSG工程编译dll,共享内存使用。
创建msgs.mc文件:
自定义MSG-ID只有写入Sbie-English-1033.txt文件才会生效,编译后的msgs.h才会包含,自定义格式如下:
#---------------------------------------------------------------------------- # New TOP_MENUS MSG #---------------------------------------------------------------------------- 5161;txt;01 &Test . 5162;txt;01 &ProcessAPiMonitor .
3. MyFarMe.Cpp添加菜单响应消息手动添加,如下所示:
1. Class类声明 afx_msg void OnCmdPackTools(); 2. Map映射,OnCmdPackTools响应菜单点击函数 BEGIN_MESSAGE_MAP(CMyFrame, CFrameWnd) { ...... ON_COMMAND(ID_MEUN_APIMONITOR, OnCmdPackTools) ...... } 3. 方法实现,如何操作: void CMyFrame::OnCmdPackTools() { AfxMessageBox(L"1"); }
4. 第二步,List宽度对于当前窗口减半,初始化新的监控List窗口(占据另一半)。菜单检测没有使用Checks属性,自己实现,同样List也被重构,ProcessList/FileList都对应的初始化如下:
// // create viewers // // 初始化List-tree控件 进程/文件 m_proclist.Create(this); m_filelist.Create(this);
5. m_procList/m_filelist组件对象,可以通过变量来控制组件属性。梳理List继承,MyListCtr继承基类CListCtrl,封装使用单独继承MyListCtr,每个功能模块都可以继承封装。
创建进程API监控类.cpp/.h,继承CMyListCtrl或者直接用ProcListCtrl类,编写控制List大小函数。
总结
虽然没有介绍初始化过程和原理,进程篇到监控篇学习至少可以掌握代码增删改查,能够执行/测试,编写部分功能和模块,对各组件之间联系更深一步,函数调用关系愈发清晰,目前来说这些是足够的。可以按照个人的意愿修改源码,这不同于进程篇的调试和理解,从这里开始你将可以基于Sandboxie做一些认为好的事情,这将是一个新的里程碑。原理篇放在最后是因为Sandboxie工程量偏大,设计与代码质量高,二次开发中不断学习,久而久之才能更好理解每一个过程,才能严谨的对Sandboxie原理完善总结。
最后很多人搞混轻量级沙盘和恶意软件分析沙箱的概念,准确来说二者完全不是一回事,除了虚拟化安全这条理念,设计出发点不同。个人使用Sandboxie一直以来用于游戏多开和安装未知软件,你会发现Sandboxie社区散户大多是用来安装软件(包括移植)和游戏,它并不是为了恶意软件分析而设计,但是开源后的Sandboxie,如果你愿意它完全可以成为Windows下优秀的恶意软件分析沙箱之一。
最近见不少群里都在讨论相关技术,大家都不知道如何学习,找材料理解代码。如果你喜欢rootkit防御检测/vt_level恶意行为检测感兴趣的朋友可以加群一起沟通,希望更多对底层安全开发的人一起学习研究。引入rootkit监控(动态拦截),成熟后引入vt框架ept_apimonitor,这是比较有意思的攻防对抗。
QQ群:493501344
Sandboxie监控篇下笔记分享之后源码上传至github(方便维护),vs2017(141)编译+wdk7600,后续同步Git(目前因有问题,包括界面无数据,暂不上传)
Github:https://github.com/TimelifeCzy
[公告]看雪论坛2020激励机制上线了!多多参与讨论可以获得积分快速升级?
最后于 1天前 被一半人生编辑 ,原因: