早在2005年,Bootkit这个概念就已被提出。随着红极一时的病毒“鬼影”的出现,Bootkit才渐渐被国人熟知。Bootkit可以说是Rootkit的加强版,强大之处在于它的隐蔽性,一般的查杀很难将其彻底清除干净。例如鬼影通过修改MBR(MainBootRecord,主引导记录)实现了开机自动执行恶意代码,且重装系统甚至全盘格式化都无法删除。因为它存在于硬盘的1扇区里,并非文件系统中的文件,而且MBR的代码在系统加载前就已经被执行,所以更是占有极高的主动性。
以上是对Bootkit的一个简介,用它作为开场白,是因为本文将实现一个Boot级别的程序,可以说是一个“善意的Bootkit”。
概述
首先要简单概述一下计算机的引导过程。主板加电后,CPU将执行权交给BIOS,BIOS将检查并初始化各个设备,执行计算机最基本的初始化工作,这个流程是固定的。当固化的BIOS程序执行完毕后,BIOS把控制权交给启动设备,操作系统通常从硬盘启动,系统BIOS将读取并执行硬盘上的主引导记录MBR(MainBootRecord),MBR接着从分区表中找到第一个活动分区,然后读取并执行这个活动分区的分区引导记录DBR(DosBootRecord),执行权交到DBR手里,之后执行的便是系统初始化的代码。DBR在系统分区中找到加载系统的程序NTLDR(Win7/VISTA下变成了bootmgr,后文写到的NTLDR也同时代表bootmgr,因为在本文讨论的范畴内它们的差别并不大),最终把执行权交给NTLDR,最后由NTLDR来加载并初始化操作系统。
综上,我们可以概括一下引导流程:BIOS-MBR-DBR-NTLDR。这四个流程我们都可以进行劫持,插入自己的代码,但是兼容性不一样,比如BIOSBootkit就有很大的局限性,要判断主板上的BIOS型号再做针对性处理。本文将演示一种比较简便的方法:劫持NTLDR,实现一个开机要求输入密码才能进入系统的小程序。
劫持NTLDR
NTLDR文件保存在系统盘的根目录下,它不是一个典型的PE文件,但包含着可执行代码。实际上,NTLDR是由一个16位程序和一个32位程序合并而成的,因为引导阶段系统仍运行在实模式下,所以控制权交给NTLDR时执行的仍是16位程序,当系统环境加载完毕后,才会跳到后面的32位NTLDR执行进一步的操作。这里我们只关心16位的NTLDR代码。用IDA打开NTLDR,以16位模式进行反汇编来分析NTLDR,如图1和图2所示。
我们发现入口处即是一个跳转指令,跳到后面开始执行加载代码,因此劫持NTLDR就变得相当简单,我们直接修改入口处的跳转指令,让它跳到我们自己的代码上实现功能,完毕后再跳回原始地址。那么我们的代码放在哪里呢?用16进制编辑工具打开NTLDR,发现在0x00003000附近有很大的空白区域,如图3所示,我们写好代码后将它们放在这里即可,经测试不会对系统加载产生影响。
编写16位程序实现验证密码功能
前面的铺垫都做好了,下面我们就要编程实现主要功能了。因为运行我们的代码时CPU处于实模式,可用的资源很有限,只能使用BIOS中断来实现各种显示功能,并且这部分代码要用16位汇编语言来编写。我实现的代码如下(主要参考了韦轁的代码并加以完善)。
;#Mode=COM .modeltiny .CODE start: ;保存寄存器内容,避免影响系统加载 pushax pushbx pushcx pushdx pushsi pushdi entry: ;设置ds=cs,以便后面定位字符串 movax,cs movds,ax callsc ;显示输入密码的提示字符串 movax,1234h callprint ;正确密码的字符串地址放入si movsi,2341h xordi,di lp: ;取得用户输入 callgetkey cmpal,ds:[si] jeshortbyte_right ;本字符正确则增加计数,存于di incdi byte_right: ;显示一个* moval,* callputchar incsi moval,ds:[si] cmpal,0 jeshortcheck ;输入未完成,继续获取用户输入 jmpshortlp check: ;输入完成,检查密码是否完全正确 cmpdi,0 ;正确则程序退出,代码权交给系统,继续引导 jeshortsucc ;不正确则显示密码错误提示,并重新要求输入 movah,2 movbh,0 movdh,2 movdl,0 int10h movax,4321h callprint callgetkey jmpshortentry succ: ;恢复寄存器内容 popdi popsi popdx popcx popbx popax ;跳回原始地址 movax,4231h jmpax ret scproc;清屏 movah,0h moval,2h int10h;设置显示模式 ret scendp putcharproc;就是C里面的putchar(),入口参数al=theconstchar movah,10 movcx,1 movbh,0h int10h movah,3 movbh,0 int10h movah,2 cmpdl,79 jeshortline_over incdl int10h ret line_over: movdl,0 incdh int10h ret putcharendp getkeyproc;相当于inkey$,出口al-按键的ASCII movah,0h int16h ret getkeyendp printproc;显示一个字符串,以\0结尾,入口参数,ax=constchar* movsi,ax ff: moval,ds:[si] cmpal,0 jestring_over callputchar incsi jmpff string_over: ret printendp passdbPASSWORD,0,0,0,0 AskdbEnterpassword:,0 faildbWrong.,0 END
看到这里,读者可能对代码中的几个绝对地址有疑惑,这个稍后再作解释。此段代码可以用MASM来编译,安装最新的MASM,将上述代码保存为chkpwd.asm,然后复制到MASM目录\Bin里,我写了个批处理来自动编译并链接,放到相同目录下运行即可。
@echooff @color0a setfname=chkpwd ifexist%fname%.objdel%fname%.obj ifexist%fname%.comdel%fname%.com ifexist%fname%.exedel%fname%.exe ml%fname%.asm link16/tiny%fname%.obj echo. pause
因为是16位的程序,故要用link16.exe来链接。注意,编译时指定了tiny开关,则编译出来会是一个纯净的COM文件,非PE格式,相当于直接把汇编码翻译成机器码后写入文件,生成的COM程序如图4所示。
覆写NTLDR并修正重定位
接下来是不是直接将chkpwd.com内容写入空白区域,再更改NTLDR的入口跳转就完成了呢?不,还有一个重要的问题要考虑,就是字符串的重定位。阅读我们的asm代码,发现输出字符串时,都是先将字符串地址放到ax里再调用print,但再仔细观察,可以发现代码中凡是涉及到字符串输出的地方,我们都用了movax,1234h这样的地址,可1234h并不是字符串的地址,为什么要用1234h呢?因为我们这里不管用什么地址,将chkpwd.com插入到NTLDR后,字符串的绝对地址都会改变,所以必须要后期写代码进程重定位,也就是修正movax,1234h这样的指令,动态地把1234h改成正确的字符串地址。写成1234h的形式,是为了后面我们写代码时能方便地通过0x120x34这样的特征码来找到movax,addr指令的位置。完整的修改NTLDR代码如下。
#includestdio.h #includeWindows.h #defineMAX_SHELLCODE_SIZE200 intOFFSET_TIPS_MOV; intOFFSET_PWD_MOV; intOFFSET_WARN_MOV; intOFFSET_PWD; intOFFSET_STR_TIPS; intOFFSET_STR_WARN; intOFFSET_JMP_MOV; WORDORG_JMP_CODE; BOOLEnablePrivilege() { TOKEN_PRIVILEGEStkp; HANDLEhToken; if(!OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES, hToken))returnFALSE; LookupPrivilegeValue(NULL,SE_DEBUG_NAME,tkp.Privileges[0].Luid); //修改进程权限 tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken,FALSE,tkp,sizeof(tkp),NULL,NULL); //通知系统修改进程权限 return(GetLastError()==ERROR_SUCCESS); } HANDLEOpenCom() { HANDLEhCom; wchar_tszCom[MAX_PATH]; //打开COM程序 ZeroMemory(szCom,MAX_PATH*sizeof(wchar_t)); GetCurrentDirectory(MAX_PATH,szCom); wcscat(szCom,L\\chkpwd.com); hCom=CreateFile(szCom,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ| FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL); returnhCom; } HANDLEOpenNTLDR() { HANDLEhNTLDR; wchar_tszNTLDR[MAX_PATH]; //打开NTLDR ZeroMemory(szNTLDR,MAX_PATH*sizeof(wchar_t)); GetSystemDirectory(szNTLDR,MAX_PATH); ZeroMemory(szNTLDR+2,(MAX_PATH-2)*sizeof(wchar_t)); //只保留盘符 wcscat(szNTLDR,L\\NTLDR); SetFileAttributes(szNTLDR,FILE_ATTRIBUTE_NORMAL);//去除只读属性 hNTLDR=CreateFile(szNTLDR,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ |FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL); returnhNTLDR; } HANDLEOpenBootMgr() {//Windows7系统没有NTLDR,取而代之的是bootmgr HANDLEhBootMgr; wchar_tszBootMgr[MAX_PATH]; //提升SE_DEBUG权限 if(!EnablePrivilege())returnNULL; //打开NTLDR ZeroMemory(szBootMgr,MAX_PATH*sizeof(wchar_t)); GetSystemDirectory(szBootMgr,MAX_PATH); ZeroMemory(szBootMgr+2,(MAX_PATH-2)*sizeof(wchar_t)); //只保留盘符 wcscat(szBootMgr,L\\bootmgr); SetFileAttributes(szBootMgr,FILE_ATTRIBUTE_NORMAL);//去除只读属性 hBootMgr=CreateFile(szBootMgr,GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL); returnhBootMgr; } /*=========================================================== 找到一段空白区域用于插入代码 ===========================================================*/ char*FindEmptyArea(HANDLEhFile,intnSize,char*buffer) { inti,j; if(nSize=0xFFFF) returnNULL; for(i=0;i0xFFFF-nSize;i++){ for(j=0;jnSize;j++){ if(buffer[i+j]!=\0) break; } if(j==nSize){ return(char*)i; } } returnNULL; } /*=========================================================== 修正重定位地址 ===========================================================*/ VOIDFixOffsetHardCode(char*ComData,intComSize) { charHardCode_Tips_Mov[3]={0xB8,0x34,0x12};//movax,1234h charHardCode_Pwd_Mov[3]={0xBE,0x41,0x23}; //movsi,2341h charHardCode_Warn_Mov[3]={0xB8,0x21,0x43};//movax,4321h charHardCode_Jmp_Mov[3]={0xB8,0x31,0x42};//movax,4231h;原始跳转的地址 inti; for(i=0;iComSize-3;i++){ if(strncmp(ComData+i,HardCode_Tips_Mov,3)==0)OFFSET_TIPS_MOV =i+1;//+1是为了略过指令的1个字节 elseif(strncmp(ComData+i,HardCode_Pwd_Mov,3)==0)OFFSET_PWD_MOV =i+1; elseif(strncmp(ComData+i,HardCode_Warn_Mov,3)==0) OFFSET_WARN_MOV=i+1; elseif(strncmp(ComData+i,HardCode_Jmp_Mov,3)==0)OFFSET_JMP_MOV =i+1; } for(i=0;iComSize-5;i++){ if(strncmp(ComData+i,Enter,5)==0)OFFSET_STR_TIPS=i; elseif(strncmp(ComData+i,PASSW,5)==0)OFFSET_PWD=i; elseif(strncmp(ComData+i,Wrong,5)==0)OFFSET_STR_WARN=i; } } /*=========================================================== 主函数,参数为要设置的密码,不超过11位 ===========================================================*/ BOOLSetBootPassword(char*szPwd) { HANDLEhNTLDR,hBootMgr,hLoader,hCom; charbuffer[0xFFFF]; intComSize; char*ComData; char*Base; DWORDdwReads,dwWrites; //打开COM文件 hCom=OpenCom(); if(hCom==INVALID_HANDLE_VALUE){ CloseHandle(hNTLDR); returnFALSE; } //获取COM文件大小 ComSize=GetFileSize(hCom,NULL); if(ComSizeMAX_SHELLCODE_SIZE){ CloseHandle(hCom); returnFALSE; } //打开NTLDR或BootMgr hNTLDR=OpenNTLDR(); if(hNTLDR==INVALID_HANDLE_VALUE){ hBootMgr=OpenBootMgr(); if(hBootMgr==INVALID_HANDLE_VALUE){ CloseHandle(hCom); returnFALSE; }else{ hLoader=hBootMgr; } }else{ hLoader=hNTLDR; } //检查NTLDR是否有空白区域插入代码 ReadFile(hLoader,buffer,0xFFFF,dwReads,NULL); Base=FindEmptyArea(hLoader,ComSize+0x10,buffer)+0x10; //多保留0x10个字节的位置避免衔接过紧出错 if(Base==NULL){ CloseHandle(hCom); CloseHandle(hLoader); returnFALSE; } //读入COM内容 ComData=(char*)malloc(ComSize); ReadFile(hCom,ComData,ComSize,dwReads,NULL); //修正偏移量 ORG_JMP_CODE=*(WORD*)(buffer+1); FixOffsetHardCode(ComData,ComSize); //处理重定向 *(WORD*)(ComData+OFFSET_TIPS_MOV)=(WORD)(Base+OFFSET_STR_TIPS); *(WORD*)(ComData+OFFSET_PWD_MOV)=(WORD)(Base+OFFSET_PWD); *(WORD*)(ComData+OFFSET_WARN_MOV)=(WORD)(Base+OFFSET_STR_WARN); *(WORD*)(ComData+OFFSET_JMP_MOV)=ORG_JMP_CODE+3; //目标地址(?)-当前地址(0x0000)-指令长度(3)=ORG_JMP_CODE //修正密码 memset(ComData+OFFSET_PWD,0,12); CopyMemory(ComData+OFFSET_PWD,szPwd,strlen(szPwd)); //插入COM内容到NTLDR+Base中 CopyMemory(buffer[(WORD)Base],ComData,ComSize); //改写NTLDR!Entry处的jmp *(WORD*)(buffer+1)=(WORD)Base-0x0000-0x03; //覆写NTLDR SetFilePointer(hLoader,0,NULL,FILE_BEGIN); WriteFile(hLoader,buffer,0xFFFF,dwWrites,NULL); //释放资源 CloseHandle(hCom); CloseHandle(hLoader); free(ComData); //MessageBoxW(0,LSucceed!,LBOOT,0); returnTRUE; } intmain(intargc,char**argv) { charszPwd[12]={0}; intStatus=1; printf(Enteranewpassword:); scanf(%s,szPwd); putchar(\n); Status=(SetBootPassword(szPwd)==TRUE); returnStatus; }
以上代码便实现了修改NTLDR添加开机密码,且代码分别处理了NTLDR和bootmgr,所以WindowsXP/7/8都支持。因为我们代码执行的这个阶段还没有完全进入操作系统层次,所以针对不同系统代码并不需要做很大修改,只要把引导处的劫持做好就基本没问题了。最终的运行效果如图5所示。
结语
本文仅仅是一个修改NTLDR的示例,并不是一个成型的Bootkit。实际上,因为兼容性的问题,Bootkit有太多的局限性,而且要学习Bootkit,我们必须深入了解实模式以及16位代码的编写。但随着技术的发展,很快我们可能就不再需要实模式引导了,甚至不再需要BIOS。现在已经有一种新的启动方式:UEFI(UnifiedExtensibleFirmwareInterface),这种模式更加灵活,且完全替代了BIOS。再过几年这项技术就会大面积普及,这也将标志着BIOSBootkit时代的终结。但如黑防所说:在攻与防的对立统一中寻求突破。新的时代新的技术,安全行业的发展也将迎来新的挑战,相信以后会有更多精彩的技术奉献给大家。