探索黑客技术攻防,实战研究与安全创新

导航菜单

修改NTLDR添加Boot级开机密码

早在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所示。

C8.png

C9.png

我们发现入口处即是一个跳转指令,跳到后面开始执行加载代码,因此劫持NTLDR就变得相当简单,我们直接修改入口处的跳转指令,让它跳到我们自己的代码上实现功能,完毕后再跳回原始地址。那么我们的代码放在哪里呢?用16进制编辑工具打开NTLDR,发现在0x00003000附近有很大的空白区域,如图3所示,我们写好代码后将它们放在这里即可,经测试不会对系统加载产生影响。

C10.png

编写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所示。

C11.png

覆写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所示。

C12.png

结语

本文仅仅是一个修改NTLDR的示例,并不是一个成型的Bootkit。实际上,因为兼容性的问题,Bootkit有太多的局限性,而且要学习Bootkit,我们必须深入了解实模式以及16位代码的编写。但随着技术的发展,很快我们可能就不再需要实模式引导了,甚至不再需要BIOS。现在已经有一种新的启动方式:UEFI(UnifiedExtensibleFirmwareInterface),这种模式更加灵活,且完全替代了BIOS。再过几年这项技术就会大面积普及,这也将标志着BIOSBootkit时代的终结。但如黑防所说:在攻与防的对立统一中寻求突破。新的时代新的技术,安全行业的发展也将迎来新的挑战,相信以后会有更多精彩的技术奉献给大家。