⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 玩转windows -dev-mem.txt

📁 可以对黑客编程有一定的了解
💻 TXT
📖 第 1 页 / 共 5 页
字号:
->Dacl    : ->Ace[2]: ->AceSize: 0x18
->Dacl    : ->Ace[2]: ->Mask : 0x0002000d
->Dacl    : ->Ace[2]: ->SID: S-1-5-32-544

->Sacl    :  is NULL

    新的ACE(access-control entry)是Ace[0],拥有0x00000002权限(SECTION_MAP_WRITE)。需要更多
信息,可以查看MSDN中的Security win32 API [9]


--[ 4 - 玩转\Device\PhysicalMemory

    为什么要来处理\Device\PhysicalMemory?我可以说用来读、写、修补内存。这已经足够了。 :)


----[ 4.1 读写内存

    我们开始吧……

    为了读写\Device\PhysicalMemory,必须:

    1、打开对象句柄 (NtOpenSection)
    2、转化虚拟内存地址为物理地址
    3、映射section到物理空间 (NtMapViewOfSection)
    4、在被映射的内存中读写数据
    5、关闭section的映射 (NtUnmapViewOfSection)
    6、关闭对象句柄 (NtClose)

    现在我们的主要目的是怎么转化虚拟内存地址为物理地址。我们知道在核心模式(ring0),有一个函
数 MmGetPhysicalAddress (ntoskrnl.exe)可以做到。但是我们现在在ring3,因此必须来“模拟”这个
函数。

---
from ntddk.h
PHYSICAL_ADDRESS MmGetPhysicalAddress(void *BaseAddress);
---

    PHYSICAL_ADDRESS是quad-word (64 bits)的。原本打算在文章开头分析一下汇编代码,但是它太长
了。地址转化也很普通,我只想快点进行这个题目。

    quad-word的低位被传递给eax,高位传递给edx。要转化虚拟地址到物理地址,可以有两种办法:

 * case 0x80000000 <= BaseAddress < 0xA0000000:
    我们唯一要做的只是提供一个0x1FFFF000掩码虚拟地址

 * case BaseAddress < 0x80000000 && BaseAddress >= 0xA0000000
    这种办法对于我们来说有点问题,因为我们并没有办法在这个范围转化地址,因为我们需要读cr3记
录或者运行非ring3可调用的汇编指令。需要更多信息,可参考Intel Software Developer's Manual 
Volume 3 (see [5]).

    EliCZ告诉我,以他的经验可以猜测一个物理地址偏移掩码,保留部分的索引。掩码:0xFFFF000

    这是一个轻量级版本的MmGetPhysicalAddress()

PHYSICAL_MEMORY MyGetPhysicalAddress(void *BaseAddress) {
   if (BaseAddress < 0x80000000 || BaseAddress >= 0xA0000000) {
      return(BaseAddress & 0xFFFF000);
   }
   return(BaseAddress & 0x1FFFF000);
}

    对于限定地址边界为[0x80000000, 0xA0000000]主要是这情况不能更成功地猜测正确。这就是为什么
如果你想更准确最好还是调用实际上的MmGetPhysicalAddress()。我们可以在一些章节中看到怎么做的。

请参考程序:See winkdump.c

    使用winkdump之后,我意识到实际上还存在另外的问题。当转化0x877ef000以上的虚拟地址,物理地
址得到的结果是0x00000000077e0000以上,但在我的系统上根本不可能!

kd> dd MmHighestPhysicalPage l1
dd MmHighestPhysicalPage l1
8046a04c  000077ef

    从上看出最后的物理页面定位在0x0000000077ef0000。这意味着我们只能dump一小片内存,但总之本
文的目的是为了得到更好的了解关于怎么用\Device\PhysicalMemory,而不只是做一个好的memory dumper。
虽然可dump的范围是ntoskrnl.exe 和 HAL.dll (Hardware Abstraction Layer)映射的区域,你仍然可以
做一些工具来dump系统调用表:

kd> ? KeServiceDescriptorTable
? KeServiceDescriptorTable
Evaluate expression: -2142852224 = 8046ab80

    0x8046ab80是系统服务表结构,象这样的:

typedef struct _SST {
   PDWORD  ServiceTable;           // array of entry points
   PDWORD  CounterTable;           // array of usage counters
   DWORD   ServiceLimit;           // number of table entries
   PBYTE   ArgumentTable;          // array of byte counts
} SST, *PSST;


C:\coding\phrack\winkdump\Release>winkdump.exe 0x8046ab80 16
 *** win2k memory dumper using \Device\PhysicalMemory ***

 Virtual Address       : 0x8046ab80
 Allocation granularity: 65536 bytes
 Offset                : 0xab80
 Physical Address      : 0x0000000000460000
 Mapped size           : 45056 bytes
 View size             : 16 bytes

d8 04 47 80 00 00 00 00  f8 00 00 00 bc 08 47 80  | ..G...........G.

Array of pointers to syscalls: 0x804704d8 (symbol KiServiceTable)
Counter table                : NULL
ServiceLimit                 : 248 (0xf8) syscalls
Argument table               : 0x804708bc (symbol KiArgumentTable)

    我们还没有dump248个系统调用地址,只是看了看类似这样的:

C:\coding\phrack\winkdump\Release>winkdump.exe 0x804704d8 12
 *** win2k memory dumper using \Device\PhysicalMemory ***

 Virtual Address       : 0x804704d8
 Allocation granularity: 65536 bytes
 Offset                : 0x4d8
 Physical Address      : 0x0000000000470000
 Mapped size           : 4096 bytes
 View size             : 12 bytes

bf b3 4a 80 6b e8 4a 80  f3 de 4b 80              | ..J.k.J...K.

 * 0x804ab3bf (NtAcceptConnectPort)
 * 0x804ae86b (NtAccessCheck)
 * 0x804bdef3 (NtAccessCheckAndAuditAlarm)

    在下面一节,我们会理解什么是callgate,以及我们同\Device\PhysicalMemory怎么用它们去解决
刚才地址转化的问题。


----[ 4.2 什么是 callgate

    callgate是一种能让程序运行在比它实际权限更高权限下的机制。比如,ring3的程序可以去执行
ring0代码。

    要创建一个callgate,必须指定:
    1) 需要代码执行在什么ring等级
    2) 当跳转到ring0时会被执行的函数地址
    3) 传递给函数的参数

    当callgate被访问的时候,处理器首先进行权限检查,保存当前的SS,ESP,CS,EIP寄存器,然后
加载segment selector和新的堆栈指针(ring0堆栈),从TSS到SS,EIP寄存器。这个指针就可以指到新
的ring0堆栈。SS和ESP寄存器被PUSH到堆栈中,参数被拷贝。CS和EIP(保存的)PUSH到堆栈中去调用程
序到新的堆栈。新的segment selector被加载用来处理从callgate被加载到CS和EIP中的新的代码片段和
指令指针。最后,它跳转到在创建callgate时候指定的函数地址。

    一旦完成后,在ring0执行的函数必须清除自己的堆栈,这就是为什么我们在代码中定义函数的时候
要用_declspec(naked)(MS VC++ 6) (类似GCC中的__attribute__(stdcall))

---
from MSDN:
__declspec( naked ) declarator

    对于用naked申明的函数,编译器不会产生prolog和epilog代码。你可以用这些特性通过inline汇编
码来写自己的prolog和epilog代码。
---

    要了解关于更多关于callgate的信息,请参考Intel Software Developer's Manual Volume 1 ([5]).

    为了安装一个Callgate,可以有两种选择:手工在GDT寻找新的空余入口,用来放置我们的Callgate;
或者用ntoskrnl.exe中的未公开函数,但是这些函数只能在ring0访问。由于我们并不在ring0,所以没有
太多的用处,但我还是简要地说明一下:

NTSTATUS KeI386AllocateGdtSelectors(USHORT *SelectorArray, 
                                    USHORT nSelectors);
NTSTATUS KeI386ReleaseGdtSelectors(USHORT *SelectorArray, 
                                   USHORT nSelectors);
NTSTATUS KeI386SetGdtSelector(USHORT Selector,
                              PVOID Descriptor);

    从它们的名字就可以知道其作用了。:) 因此,如果你打算安装一个callgate,首先使用
KeI386AllocateGdtSelectors() 分配一个GDT Selector, 然后通过KeI386SetGdtSelector 来设置。完成后,
通过KeI386ReleaseGdtSelectors来释放。

    这还是很有意思,但是不符合我们的需要。因此在ring3执行代码的时候需要设置一个GDT Selector。
这接近\Device\PhysicalMemory了。在下一节,我会解释怎么用\Device\PhysicalMemory来安装callgate.


----[ 4.3 不用驱动运行ring0代码

    第一个问题,“为什么运行ring0代码不需要用设备驱动?”

优点:
    * 不需要向SCM注册服务
    * 秘密代码 :)

缺点:
    * 代码不能跟设备驱动那样稳定
    * 需要添加写权限到\Device\PhysicalMemory

    因此要紧记,当通过\Device\PhysicalMemory运行ring0代码的时候,你会遇到很多困难的。

    现在我们可以写内存并且我们知道我们可以用callgate来运行ring0,那么还等什么呢?

    首先,我们需要知道section的什么部分映射去读GDT表。这并不是问题,因为我们能访问全局的描述
符表记录通过sgdt编译指令。

typedef struct _KGDTENTRY {
   WORD LimitLow; // size in bytes of the GDT
   WORD BaseLow;  // address of GDT (low part)
   WORD BaseHigh; // address of GDT (high part)
} KGDTENTRY, *PKGDTENTRY;

KGDT_ENTRY gGdt;
_asm sgdt gGdt; // load Global Descriptor Table register into gGdt

    我们转化虚拟地址从BaseLow/BaseHigh到物理地址,然后我们隐射GDT表的基地址。我们很幸运,即
便GDT表地址并不在我们“想象”的范围内,它也能正确被转化 (99%的可能)

PhysicalAddress = GetPhysicalAddress(gGdt.BaseHigh << 16 | gGdt.BaseLow);

NtMapViewOfSection(SectionHandle,
                   ProcessHandle,
                   BaseAddress,       // pointer to mapped memory
                   0L,
                   gGdt.LimitLow,     // size to map
                   &PhysicalAddress,
                   &ViewSize,         // pointer to mapped size
                   ViewShare,
                   0,                 // allocation type
                   PAGE_READWRITE);   // protection

    最后我们循环映射的地址去找到一个空闲的selector, 通过查看Callgate描述符结构中的"Present"
标记。

typedef struct _CALLGATE_DESCRIPTOR {
   USHORT offset_0_15;    // low part of the function address
   USHORT selector;
   UCHAR  param_count :4;
   UCHAR  some_bits   :4;
   UCHAR  type        :4; // segment or gate type
   UCHAR  app_system  :1; // segment descriptor (0) or system segment (1)
   UCHAR  dpl         :2; // specify which privilege level can call it
   UCHAR  present     :1;
   USHORT offset_16_31;   // high part of the function address
} CALLGATE_DESCRIPTOR, *PCALLGATE_DESCRIPTOR;

    offset_0_15 和 offset_16_31正好是函数地址的低位 和 高位。selector可以是下面所列的一个:

--- from ntddk.h
#define KGDT_NULL       0
#define KGDT_R0_CODE    8   // <-- what we need (ring0 code)
#define KGDT_R0_DATA    16
#define KGDT_R3_CODE    24
#define KGDT_R3_DATA    32
#define KGDT_TSS        40
#define KGDT_R0_PCR     48
#define KGDT_R3_TEB     56
#define KGDT_VDM_TILE   64
#define KGDT_LDT        72
#define KGDT_DF_TSS     80
#define KGDT_NMI_TSS    88
---

    一旦callgate被安装,就还有两步去得到最高的ring0权力:编写我们callgate调用的程序和调用
callgate。

    正如在4.2中所介绍,我们需要编写一个函数,并且有ring0的prolog / epilog,而且我们需要自己
清除堆栈。看看下面的示例代码:

void __declspec(naked) Ring0Func() { // our nude function :]
   // ring0 prolog
   _asm {
      pushad // push eax,ecx,edx,ebx,ebp,esp,esi,edi onto the stack
      pushfd // decrement stack pointer by 4 and push EFLAGS onto the stack
      cli    // disable interrupt
   }
   
   // execute your ring0 code here ...
   
   // ring0 epilog
   _asm {
      popfd // restore registers pushed by pushfd
      popad // restore registers pushed by pushad
      retf  // you may retf <sizeof arguments> if you pass arguments
   }
}

    推送所有的寄存器到堆栈中可以让我们在ring0代码执行的时候保存下所有的寄存器。

    还剩一步,调用callgate……

    一个基本的调用不适用与这种定位于ring0而实际在ring3的callgate程序。我们需要进行"far call"
(inter-privilege level call), 因此为了调用callgate,必须这样做:

short farcall[3];
farcall[0 --> 1] = offset from the target operand. This is ignored when a
callgate is used according to "IA-32 Intel Architecture Software
Developer's Manual (Volume 2)" (see [5]).

farcall[2] = callgate selector

    这个时候,我们可以调用自己的callgate通过inline汇编。

_asm {
   push arg1
   ...
   push argN
   call fword ptr [farcall]
}

    我忘记提醒了,callgate函数中,farcall的第一个参数定位在[ebp+0Ch]。


----[ 4.4 深入进程列表

    现在我们可以去看看怎么用最低的等级去列举核心的进程。这个目的就是为了在低等级下创建一个
核心进程枚举程序,可以用来查看被rootkit隐藏的进程 (修改过taskmgr.exe,系统调用hook等)

    - Process32First/Process32Next, 最简单的公开途径(基态)
    - 使用Class 5的NtQuerySystemInformation,Native API。虽然没有被公开,但是网上有很多例
子(level -1)
    - 用ExpGetProcessInformation,它实际是被NtQuerySystemInformation所调用的(level -2)
    - 读取双向链表PsActiveProcessHead (Level -3) :p

    现在我们已经足够深入了。这个双向链表看起来象这样:

APL (f): ActiveProcessLinks.FLink
APL (b): ActiveProcessLinks.BLink

        process1          process2          process3          processN
0x000 |----------|      |----------|      |----------| 
      | EPROCESS |      | EPROCESS |      | EPROCESS |
      |   ...    |      |   ...    |      |   ...    |
0x0A0 |  APL (f) |----->|  APL (f) |----->|  APL (f) |-----> ...
0x0A4 |  APL (b) | \-<--|  APL (b) | \-<--|  APL (b) | \-<-- ...
      |   ...    |      |   ...    |      |   ...    |
      |----------|      |----------|      |----------|

    正如你所见(也许我的示意图画得不好),ActiveProcessLinks结构的next/prev指针不是_EPROCESS
结构指针。它们指向的是另一个LIST_ENTRY结构。这意味着如果我们要得到_EPROCESS结构地址,必须调
节指针。

    (请看例程中kmem.h定义的_EPROCESS结构)

    LIST_ENTRY ActiveProcessLinks位于_EPROCESS结构的偏移0x0A0:
 --> Flink = 0x0A0
 --> Blink = 0x0A4

    因此,我们可以创建一些宏供以后使用:

#define TO_EPROCESS(_a) ((char *) _a - 0xA0) // Flink to _EPROCESS
#define TO_PID(_a) ((char *) _a - 0x4)       // Flink to UniqueProcessId
#define TO_PNAME(_a) ((char *) _a + 0x15C)   // Flink to ImageFileName

    LIST_ENTRY链表的头是PsActiveProcessHead。可以用kd得到它的地址:

kd> ? PsActiveProcessHead
? PsActiveProcessHead
Evaluate expression: -2142854784 = 8046a180

    只需要知道一件事情。这个链表改变非常的快,你可以在读之前锁定它。下面的程序是用汇编来读
ExpGetProcessInformation:

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -