经过一个假期的不断摸索,终于把Crypto交互的部分搞明白了。本篇记录了Crypto交互题的解题过程,以及一道交互题是如何实现其功能的。
0x1. 交互题概述
交互题的实质,就是挂载在服务器的一个端口上的程序。解交互题时,一般给选手一个nc码,包含服务器ip以及端口号,选手在控制台输入nc码,即可进入与服务器的交互。有时候题目附件还包含服务器端的代码,方便选手查看加密过程,但严格限制选手的权限,不允许选手查看flag的内容。
0x2. 解交互题
0x2.1 编写交互脚本
对于交互次数较少的题目,当然可以选择不写交互脚本,但如果交互次数较多,每次单独执行一遍exp就会很麻烦,这时候就要编写自动化交互脚本。
交互脚本需要用到python的pwn库,包含各种能实现收、发包的函数。
1 conn=remote('HOST' , PORT)
初始化conn,确认要与哪个服务器的哪个端口交互。
一次接收到的内容,不对换行符敏感,内容的类型为bytes
一次接收到的内容,以换行符为结束,类型也是bytes
向服务器发送指定内容,对于int类型要变换为bytes类型:f'{x}'.encode
。
1 conn.sendlineafter(b'aaa' ,b'bbb' )
在收到特定的字节流后发送指定内容,搭配上一条使用。
0x2.2 特别注意
第一,要准确把握程序在一个循环的每个阶段输的内容,才能正确编写交互脚本,否则要么报错,要么无法返回正确答案。有的题目非常操蛋,跟鸡拉屎一样的一截一截的发,那就只能一段一段的recvline()
,而不是recv().split('\n')
。如果没有附件,最好一步步打印输出内容。
交互过程中有两个特殊部分,就是交互最开始和最末尾。最开始不会像交互过程中返回一个Yes或者No,因此要单独执行再进入循环(循环可以用while
True,因为你不知道要执行多少次)。而最末尾输出正确后就直接给flag了,而交互脚本还在解码,所以大概率要报错,因此无论返回值是啥都要先print再继续。
第二,交互脚本只是工具,正确的攻击思想和严密的逻辑才是解答密码题的核心!(打枪没有用的.jpg)
0x3 交互题是怎样炼成的
0x3.1 概述
1 socat -d -d tcp-l:|port| ,reuseaddr,fork EXEC:"python -u server.py"
socat直接挂载,一步到位,非常无脑,只使用于自己玩。
出题面临的情况是:服务器没有python,也没有python的那些库函数;运维是一只会敲打键盘的猩猩,除了按照指示在控制台上输入命令啥也不会。总之,要在一个光板机上用最简单的方式搭建题目环境。不过,情况也没那么糟糕,因为主机上一般有一个很重要的应用:Docker。
61d7ee0b2cab4295.jpg
docker的logo是一个巨鲸,它上面托运了很多集装箱;可以把鲸鱼看成是货轮,相当于是一个平台,上面放的集装箱可以看成是容器,集装箱容器里面装的就是各种项目,而且集装箱与集装箱之间没有任何联系,它们是相互隔离的。
Docker
可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的
Linux
机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似
iPhone 的 app),更重要的是容器性能开销极低。
Dockerfile
是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
0x3.2 出发了!(日丹诺夫并感)
交互题server.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 from hashlib import sha256import socketserverfrom secret import flagimport signalimport stringimport randomimport osimport gmpy2 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 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'[+] Plz tell me XXXX: ' ) if len (x) != 4 or sha256(x+proof[4 :].encode()).hexdigest() != _hexdigest: return False return True def handle (self ): signal.alarm(60 ) self.send('This is a test of gmpy2' ) x=gmpy2.powmod(2 ,3 ,3 ) self.send(b'gmpy2.powmod(2,3,3)=' +f'{x} ' .encode()) if not self.proof_of_work(): self.send(b'[!] Wrong!' ) return self.send(b'here is your flag' ) self.send(flag) class ThreadedServer (socketserver.ThreadingMixIn, socketserver.TCPServer): pass class ForkedServer (socketserver.ForkingMixIn, socketserver.TCPServer): pass if __name__ == "__main__" : HOST, PORT = '0.0.0.0' , 10001 server = ForkedServer((HOST, PORT), Task) server.allow_reuse_address = True print (HOST, PORT) server.serve_forever()
Dockerfile,在服务器端运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM python:3.8 LABEL Description="baby_try" VERSION='1.0' COPY server.py . COPY secret.py . RUN pip install --upgrade pip RUN pip install gmpy2 RUN chmod +x server.py EXPOSE 12345 CMD ["python" , "server.py" ]
docker-compose.yml,简化创建镜像和启动容器的步骤
1 2 3 4 5 6 7 8 9 version: '3' services: checkin: image: signin build: . ports: - "9999:10001" restart: always
输入命令一键搭建:docker-compose up -d