📄 玩转windows -dev-mem.txt
字号:
->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 + -