How to Setup for Interactive Crypto Problems? (出挂载在服务端的密码题指北)

Introduction

要给校赛出题了,想出一个挂在服务器端的题目。但是不知道怎么搞,去研究了一下之前SUCTF的题目源码。

Dependency

  • OS:CentOS 7 x64
  • CPU:1 vCore
  • RAM:1024 MB
  • SSD:25 GB SSD

服务器如何搭建相关环境?

 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
yum update -y
yum groupinstall -y "Development tools"
yum install -y zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel gmp python-devel socat lrzsz nc


# Install pip
cd /usr/src
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
python2.7 get-pip.py
pip install --upgrade pip

# Install pwntools
pip install --upgrade pwntools

# Install pycrypto
pip install pycryptodome
# Test pycrypto: python -m Cryptodome.SelfTest

# Install numpy
pip install numpy


# -----------------------------------------------------
# For gmpy2
mkdir -p $HOME/src
mkdir -p $HOME/static

# Install m4
v=1.4.18
cd $HOME/src
wget http://ftp.gnu.org/gnu/m4/m4-${v}.tar.gz
tar xf m4-${v}.tar.gz && cd m4-${v}
./configure -prefix=/usr/local
make && make check && make install

# Install gmp
v=6.1.2
cd $HOME/src
wget https://gmplib.org/download/gmp/gmp-${v}.tar.bz2
tar -jxvf gmp-${v}.tar.bz2 && cd gmp-${v}
./configure --prefix=$HOME/static --enable-static --disable-shared --with-pic
make && make check && make install

# Install mpfr
v=4.0.1
cd $HOME/src
wget http://ftp.gnu.org/gnu/mpfr/mpfr-${v}.tar.bz2
tar -jxvf mpfr-${v}.tar.bz2 && cd mpfr-${v}
./configure --prefix=$HOME/static --enable-static --disable-shared --with-pic --with-gmp=$HOME/static
make && make check && make install

# Install mpc
v=1.1.0
cd $HOME/src
wget ftp://ftp.gnu.org/gnu/mpc/mpc-${v}.tar.gz
tar -zxvf mpc-${v}.tar.gz && cd mpc-${v}
./configure --prefix=$HOME/static --enable-static --disable-shared --with-pic --with-gmp=$HOME/static --with-mpfr=$HOME/static
make && make check && make install

# Install gmpy2
v=2-2.1.0a1
cd $HOME/src
wget https://github.com/aleaxit/gmpy/releases/download/gmpy${v}/gmpy${v}.tar.gz
v=2-2.1.0a1
tar xf gmpy${v}.tar.gz && cd gmpy${v}
python setup.py build_ext --static-dir=$HOME/static install


# Setup firewall
# systemctl status firewalld
# systemctl start firewalld
# systemctl unmask firewalld
# firewall-cmd --zone=public --add-port=10001-10005/tcp --permanent
# firewall-cmd --reload
# firewall-cmd --zone=public --list-ports

注意Linux下的换行是\n,而Win下的换行是\r\n,从Win复制到服务器上的时候要留意一下这个问题。


服务器能运行题目,但是本地连不上服务器?

防火墙端口的问题,需要配置一下防火墙。

  • 查看当前防火墙状态:systemctl status firewalld
  • 启动防火墙:systemctl start firewalld
  • 重启防火墙:systemctl restart firewalld.service
  • 停止防火墙:systemctl stop firewalld.service
  • 永远停止防火墙,开机不会启动:systemctl disable firewalld.service
  • 开启某段端口:firewall-cmd --zone=public --add-port=10001-10005/tcp --permanent
  • 关闭某段端口:firewall-cmd --zone=public --remove-port=10001-10005/tcp --permanent
  • 重新加载防火墙(每次开启或者关闭后必须重新加载才有效):firewall-cmd --reload
  • 查看当前开放的端口与协议:firewall-cmd --zone=public --list-ports

Example

Setup

下面复现一道Crypto题的环境,选择SUCTF 2019Prime

  1. 首先从GitHub 上下载这场比赛的源码 git clone https://github.com/team-su/SUCTF-2019
  2. 然后根据上面那个sh脚本里面的一些命令安装socat以及该题用到的Pythonpycryptodome, numpy, gmpy2(挺麻烦的)
  3. 给的源码文件start_from_here.py里默认开放端口为22222,可以根据自己的喜好修改
  4. 开启防火墙相应的端口,并重新加载
    systemctl start firewalld
    firewall-cmd --zone=public --add-port=22222/tcp --permanent
    firewall-cmd --reload
    firewall-cmd --zone=public --list-ports
    
    能看到22222/tcp,说明22222端口已经开启。
  5. xxx/SUCTF-2019/Crypto/Prime目录下运行python start_from_here.py

这样,这一题的环境就成功在我们自己的服务器上面搭好了。

如果是阿里云的服务器,可能还需要在阿里云控制台那里开放一下相应的端口。

Test

现在,我们要从本地去打这一道题。

首先nc ip 22222尝试一下能否连接到服务器。

能够成功连到,并返回题目内容。

直接用当初打比赛的时候写的exp.py打一发:

 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
# python2

from pwn import *
from random import choice
from string import ascii_letters, digits
from hashlib import md5
from Crypto.Util.number import *


r = remote('xxx.xxx.xxx.xxx', 22222)

# context.log_level = 'debug'


def fuck():
    r.recvuntil('[*] Please find a string that md5(str + ')
    salt, part_hash = r.recvline().strip().split(')[0:5] == ')
    # print "salt: " + salt
    # print "part_hash: " + part_hash
    s = ''
    while hashlib.md5(s + salt).hexdigest()[0:5] != part_hash:
        s = ''.join([choice(ascii_letters+digits) for _ in range(10)])
    r.recvuntil('> ')
    r.sendline(s)

fuck()

cs, ns, ms = list(), list(), list()
for _ in range(4):
    cs.append(int(r.recvline().strip().split('= ')[-1].strip('L'), 16))
    ns.append(int(r.recvline().strip().split('= ')[-1].strip('L'), 16))

for i in range(4):
    p0 = GCD(ns[i], ns[(i+1) % 4])
    p1 = GCD(ns[i], ns[(i+2) % 4])
    p2 = GCD(ns[i], ns[(i+3) % 4])
    p3 = ns[i] // p0 // p1 // p2
    phi = (p0-1)*(p1-1)*(p2-1)*(p3-1)
    e = ns[i] % phi
    d = inverse(e, phi)
    ms = hex(pow(cs[i], d, ns[i]))
    r.recvuntil('ms[%d] = ' % i)
    r.sendline(ms)


r.interactive()

成功!

也能在服务器端看到log输出。

Next

下一步就要根据这个模板来自己写一道题了。

Good luck to me.

Reference

题目源码:https://github.com/team-su/SUCTF-2019 Centos下配置防火墙:https://www.cnblogs.com/TTyb/p/9871706.html Linux换行符:https://blog.csdn.net/mulangren1988/article/details/54316783 Crypto库安装:https://blog.csdn.net/zhangpeterx/article/details/96428212 numpy库安装:https://github.com/jupyter/help/issues/141 netcat使用:https://wangjun.dev/2017/11/netcat-general/ rz/sz安装:https://blog.csdn.net/ljxfblog/article/details/38396421 socat使用:https://medium.com/@copyconstruct/socat-29453e9fc8a6 gmpy2库安装:https://www.cnblogs.com/pcat/p/5746821.html


Docker

以上是2019年10月份左右写的,下面以2020WMCTF的一道Game题目来展示一下如何使用docker来搭建。

当前目录下的文件:

1
2
3
4
5
6
7
~/ctf/problems/wmctf/Game/Crypto_Game/deploy ❯ tree -L 1
.
├── Dockerfile
├── secret.py
└── task.py

0 directories, 3 files

要写好task.py文件(题目具体内容),并把flag放在secret.py文件中。

只需要把task.py文件给选手即可,这样选手就可以自己在本地运行了,而且本地运行的时候可以把proof of work给注释掉。

其中task.py中的内容为:

  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
# !/usr/bin/env python3
import socketserver
import os, sys, signal
import string, random
from hashlib import sha256

from Crypto.Cipher import AES

from secret import flag

BANNER = br"""
      ___           ___           ___                         ___
     /\  \         /\  \         /\__\                       /\__\
    _\:\  \       |::\  \       /:/  /          ___         /:/ _/_
   /\ \:\  \      |:|:\  \     /:/  /          /\__\       /:/ /\__\
  _\:\ \:\  \   __|:|\:\  \   /:/  /  ___     /:/  /      /:/ /:/  /
 /\ \:\ \:\__\ /::::|_\:\__\ /:/__/  /\__\   /:/__/      /:/_/:/  /
 \:\ \:\/:/  / \:\~~\  \/__/ \:\  \ /:/  /  /::\  \      \:\/:/  /
  \:\ \::/  /   \:\  \        \:\  /:/  /  /:/\:\  \      \::/__/
   \:\/:/  /     \:\  \        \:\/:/  /   \/__\:\  \      \:\  \
    \::/  /       \:\__\        \::/  /         \:\__\      \:\__\
     \/__/         \/__/         \/__/           \/__/       \/__/
"""
MENU = br"""
1. encrypt
2. guess
3. exit
"""

class Task(socketserver.BaseRequestHandler):
    def _recvall(self):
        BUFF_SIZE = 2048
        data = b''
        while True:
            part = self.request.recv(BUFF_SIZE)
            data += part
            if len(part) < BUFF_SIZE:
                break
        return data.strip()

    def send(self, msg, newline=True):
        try:
            if newline:
                msg += b'\n'
            self.request.sendall(msg)
        except:
            pass

    def recv(self, prompt=b'> '):
        self.send(prompt, newline=False)
        return self._recvall()

    def recvhex(self, prompt=b'> '):
        self.send(prompt, newline=False)
        try:
            data = bytes.fromhex(self._recvall().decode('latin-1'))
        except ValueError as e:
            self.send(b"Wrong hex value!")
            self.close()
            return None
        return data

    def close(self):
        self.send(b"Bye~")
        self.request.close()

    def pad(self, data):
        pad_len = 16 - len(data)%16
        return data + bytes([pad_len])*pad_len

    def proof_of_work(self):
        random.seed(os.urandom(8))
        proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
        _hexdigest = sha256(proof.encode()).hexdigest()
        self.send(f"sha256(XXXX+{proof[4:]}) == {_hexdigest}".encode())
        x = self.recv(prompt=b'Give me XXXX: ')
        if len(x) != 4 or sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
            return False
        return True

    def handle(self):
        signal.alarm(1200)

        self.send(BANNER)
        if not self.proof_of_work():
            return

        secret = os.urandom(48)
        key = os.urandom(16)
        IV = os.urandom(16)
        aes = AES.new(key, mode=AES.MODE_CBC, iv=IV)
        self.send(f"IV is: {IV.hex()}".encode())
        self.send(b"Guess the secret, and I will give you the flag if you're right~!")

        while True:
            self.send(MENU, newline=False)
            choice = self.recv()

            if choice == b"1":
                msg = self.recvhex(prompt=b"Your message (in hex): ")
                if not msg: break
                cipher = aes.encrypt(self.pad(msg + secret))
                self.send(cipher.hex().encode())
                continue
            elif choice == b"2":
                guess = self.recvhex(prompt=b"Your guess (in hex): ")
                if not guess: break
                if guess == secret:
                    self.send(b"TQL!!! Here is your flag: " + flag)
                else:
                    self.send(b"TCL!!!")

            self.close()
            break

class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass

if __name__ == "__main__":
    HOST, PORT = '0.0.0.0', 10000
    server = ForkedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

其中大部分内容也都是照着别人的模版来写的。

secret.py文件中就是定义了一个flag:

1
flag = b"WMCTF{Dont_ever_tell_anybody_anything___If_you_do__you_start_missing_everybody}"

Dockerfile配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
FROM python:3.8-alpine
LABEL Description="Game" VERSION='1.0'

RUN apk update && apk add gcc g++ make openssl-dev python3-dev libffi-dev autoconf

WORKDIR /opt/game
RUN mkdir -p /opt/game

COPY task.py .
COPY secret.py .

RUN pip install pycryptodome

EXPOSE 10000
CMD ["python", "-u", "task.py"]

其实也很简单,使用了一个python3.8的镜像,然后安装了一下依赖和Crypto库,再把当前目录下的那3个文件全部copy到docker镜像中,并开放10000端口,最后运行task.py文件启动TCP服务。

把这3个文件给运维就可以完事了。

不过也可以先本地测试一下是否ok:

  1. docker build . -t wmctf-game build一下镜像

  2. docker images检查一下

  3. docker run --name game -d -p 10000:10000 wmctf-game创建一个container容器来运行

    其中,-d表示run了之后不进入到container内部,-p 10000:10000将container内部的10000端口映射出来到本地的10000端口

  4. docker container ls可以看到已经有一个container在运行了

  5. nc 127.0.0.1 10000即可连接到题目的TCP服务