nullconCTF/Crypto/RockPaperScissors

2020年2月8号的nullconCTF Online的一道Crypto题

拿到题目,打开rps.py是源码, 然后又给了nc的地址, 直接nc进去看给出的是一段文字(这里由于结束后官方关闭了端口,后面无法进行nc), 直接看源码, 不难发现,这是一道代码审计题, 题目给出的RockPaperScissors是石头剪子布. 下面我们一个函数一个函数的来看.

def gen_commitments():
    secret = bytearray(Random.get_random_bytes(16))
    rc = hash(secret + b"r")
    pc = hash(secret + b"p")
    sc = hash(secret + b"s")
    secret = hex(bytes_to_int(secret))[2:]
    rps = [("r", rc), ("p", pc), ("s", sc)]
    random.shuffle(rps)
    return secret, rps

先给出这个函数, rc, pc, sc 就是经过hash加密后的三个数, 不难发现, 每一个数都是经过secret加上一个字符加密后得到, 最后转换成hex输出, 并打乱顺序, 这就是nc中所显示的内容, 然后他说了The fisrt one is my move, 所以这时候就需要我们去找出他到底出了什么. 然后我们用下面这段逻辑去赢他,总共赢20轮,这就是这道题的大体思路了.

def check_win(move, inp):
    if move == "r":
        if inp == "p":
            return True
        else:
            return False
    elif move == "s":
        if inp == "r":
            return True
        else:
            return False
    elif move == "p":
        if inp == "s":
            return True
        else:
            return False
    return False

进入主题, 这道题最重要的就是对下面这段函数的分析,就是对他的加密过程的分析.

def hash(secret):
    state = bytearray([208, 151, 71, 15, 101, 206, 50, 225, 223, 14, 14, 106, 22, 40, 20, 2])
    secret = pad(secret, 16)
    secret = group(secret)
    for roundkey in secret:
        for _ in range(round):
            state = repeated_xor(state, roundkey)
            for i in range(len(state)):
                state[i] = sbox[state[i]]
            temp = bytearray(16)
            for i in range(len(state)):
                temp[p[i]] = state[i]
            state = temp
    return hex(bytes_to_int(state))[2:]

先从

for i in range(len(state)):
	state[i] = sbox[stat[i]]

开始, 这里: state的值是sbox的下标, 并将state的值为下标的sbox对应的值给到state,

sbox = [221, 229, 120, 8, 119, 143, 33, 79, 22, 93, 239, 118, 130, 12, 63, 207, 90, 240, 199, 20, 181, 4, 139, 98, 78, 32, 94, 108, 100, 223, 1, 173, 220, 238, 217, 152, 62, 121, 117, 132, 2, 55, 125, 6, 34, 201, 254, 0, 228, 48, 250, 193, 147, 248, 89, 127, 174, 210, 57, 38, 216, 225, 43, 15, 142, 66, 70, 177, 237, 169, 67, 192, 30, 236, 131, 158, 136, 159, 9, 148, 103, 179, 141, 11, 46, 234, 36, 18, 191, 52, 231, 23, 88, 145, 101, 17, 74, 44, 122, 75, 235, 175, 54, 40, 27, 109, 73, 202, 129, 215, 83, 186, 7, 163, 29, 115, 243, 13, 105, 184, 68, 124, 189, 39, 140, 138, 165, 219, 161, 150, 59, 233, 208, 226, 176, 144, 113, 146, 19, 224, 111, 126, 222, 178, 47, 252, 99, 87, 134, 249, 69, 198, 164, 203, 194, 170, 26, 137, 204, 157, 180, 168, 162, 56, 81, 253, 213, 45, 21, 58, 24, 171, 37, 82, 53, 50, 84, 196, 232, 242, 244, 64, 80, 10, 114, 212, 187, 205, 28, 51, 182, 16, 107, 245, 211, 85, 92, 195, 5, 197, 200, 31, 183, 61, 123, 86, 167, 154, 41, 151, 35, 247, 246, 153, 95, 206, 149, 76, 112, 71, 230, 106, 188, 172, 241, 72, 156, 49, 14, 214, 155, 110, 102, 116, 128, 160, 135, 104, 77, 91, 190, 60, 42, 185, 96, 97, 251, 218, 133, 209, 65, 227, 3, 166, 255, 25]

经过测试,发现sbox为0-255, 并且无重复不修改, 这样子, 每个值对应的下标就是固定的, 所以我们就可以通过state的值去匹配sbox的下标,并将下标给到state,通过这样子来恢复原有的state.

for i in range(16):   //16是state的长度
		for j in range(256):   //256是sbox的长度
			if state[i] == sbox[j]:
				state[i] = j
 				break

test.py
out

 p = [5, 9, 1, 8, 3, 11, 0, 12, 7, 4, 14, 13, 10, 15, 6, 2]
 temp = bytearray(16)
 for i in range(len(state)):
      temp[p[i]] = state[i]
 state = temp

这里的temp = bytearray(16)是先占用了16个空字节, 可以不用管, 这里的逻辑跟上面差不多, p是0-15,并且无重复不修改. 这里的state是从下标0-15的值依次给到temp对应p的值为下标的位置, 例如state[0]的值给到temp[p[0=5] 最后再将state更新为temp, 所以定义一个temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] len(temp) = 16

只需要将state[p[0] = 5] 的值给回temp[0] 即可, 最后更新state = temp

test.py
out

def repeated_xor(p, k):
    return bytearray([p[i] ^ k[i % len(k)] for i in range(len(p))])
state = repeated_xor(state, roundkey)

这里通过函数名不难发现是异或, state和roundkey异或最后的值再给到state, 然后发现roundkey是在secret中的, 并且secret = group(secret)

def group(input, size = 16):
    return [input[i * size: (i + 1) * size] for i in range(len(input) // size)]

这个地方是最重点的地方, 先随机生成一个16字节的secret, 加上"r"

在这里插入图片描述

定义题目中的pad函数, 然后调用, 将值给到secret

在这里插入图片描述

发现pad函数的作用是在后面填充了一堆0xf, 再调用题目中的group函数

在这里插入图片描述

group函数是将secret分成两部分然后再对每个部分进行加密, 所以在

for roundkey in secret中, roundkey只有两个, 第一个是secret, 第二个是r,p,s中的其中一个然后在后面加上填充字符.

所以

1: roundkey2只有三个值:
      rkey = bytearray(b'r\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')     
      pkey = bytearray(b'p\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')
      skey = bytearray(b's\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')
 2: secret+r/p/s 不论加上哪一个, 其实都是单独加密的, 由于sbox, state, 和p都是固定, 
     每一轮, secret的加密结果都是一样的

由于加密的时候先对secret加密,再对roundkey2加密,所以反过来, 解密的时候要先对roundkey2进行解密, 解出来的结果就是roundkey1也就是secret的加密后的结果

这样就可以通过列举, 在给出的三个数据中, 对每一个数据单独进行r p s解密后,对其结果进行比较, 只有当三个数据解出来的结果一样时, 才能确定r p s的顺序.

在这里插入图片描述

如上图res(s1, skey) == res(s2, rkey) == res(s3, pkey) ,确定顺序为s r p

ps: 有一点值得注意, 就是有时侯给出的数据会是31位的, 这样运行的时候会出现Odd String, 这时候只需要在该数据的最前面填充0即可.
def check_len(state):
	if len(state) != 32:
		return '0' + state
	return state
给出最终的exp.py
from pwn import *
from Crypto.Util.number import *

sbox = [221, 229, 120, 8, 119, 143, 33, 79, 22, 93, 239, 118, 130, 12, 63, 207, 90, 240, 199, 20, 181, 4, 139, 98, 78, 32, 94, 108, 100, 223, 1, 173, 220, 238, 217, 152, 62, 121, 117, 132, 2, 55, 125, 6, 34, 201, 254, 0, 228, 48, 250, 193, 147, 248, 89, 127, 174, 210, 57, 38, 216, 225, 43, 15, 142, 66, 70, 177, 237, 169, 67, 192, 30, 236, 131, 158, 136, 159, 9, 148, 103, 179, 141, 11, 46, 234, 36, 18, 191, 52, 231, 23, 88, 145, 101, 17, 74, 44, 122, 75, 235, 175, 54, 40, 27, 109, 73, 202, 129, 215, 83, 186, 7, 163, 29, 115, 243, 13, 105, 184, 68, 124, 189, 39, 140, 138, 165, 219, 161, 150, 59, 233, 208, 226, 176, 144, 113, 146, 19, 224, 111, 126, 222, 178, 47, 252, 99, 87, 134, 249, 69, 198, 164, 203, 194, 170, 26, 137, 204, 157, 180, 168, 162, 56, 81, 253, 213, 45, 21, 58, 24, 171, 37, 82, 53, 50, 84, 196, 232, 242, 244, 64, 80, 10, 114, 212, 187, 205, 28, 51, 182, 16, 107, 245, 211, 85, 92, 195, 5, 197, 200, 31, 183, 61, 123, 86, 167, 154, 41, 151, 35, 247, 246, 153, 95, 206, 149, 76, 112, 71, 230, 106, 188, 172, 241, 72, 156, 49, 14, 214, 155, 110, 102, 116, 128, 160, 135, 104, 77, 91, 190, 60, 42, 185, 96, 97, 251, 218, 133, 209, 65, 227, 3, 166, 255, 25]
p = [5, 9, 1, 8, 3, 11, 0, 12, 7, 4, 14, 13, 10, 15, 6, 2]
round = 16

rkey = bytearray(b'r\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')
pkey = bytearray(b'p\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')
skey = bytearray(b's\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f')

def check_len(state):
	if len(state) != 32:
		return '0' + state
	return state

def repeated_xor(p, k):
    return bytearray([p[i] ^ k[i % len(k)] for i in range(len(p))])

def byte_decode_hex(state):
	return bytearray(state.decode('hex'))

def res(state, roundkey):
	state = check_len(state)
	state = byte_decode_hex(state)
	for _ in range(round):
		temp = bytearray(16)
		for i in range(len(state)):
			temp[i] = state[p[i]]
		state = temp
		for i in range(16):
			for j in range(256):
				if state[i] == sbox[j]:
					state[i] = j
	 				break
		state = repeated_xor(state, roundkey)
	return state

conn = remote("crypto1.ctf.nullcon.net", "5000")
conn.recvuntil("is my move: ")
a = conn.recvline()
state = a.split()

print state[0]
print state[1]
print state[2]

count = 1

while(count <= 20) {
	conn.recvuntil("is my move: ")
	state = conn.recvline().split()
	# r s p
	if res(state[0], rkey) == res(state[1], skey) and res(state[0], rkey) == res(state[2], pkey):
		print("r")
		conn.sendline("p")
	# r p s
	elif res(state[0], rkey) == res(state[1], pkey) and res(state[0], rkey) == res(state[2], skey):
		print("r")
		conn.sendline("p")
	# s p r
	elif res(state[0], skey) == res(state[1], pkey) and res(state[0], skey) == res(state[2], rkey):
		print("s")
		conn.sendline("r")
	# s r p
	elif res(state[0], skey) == res(state[1], rkey) and res(state[0], skey) == res(state[2], pkey):
		print("s")
		conn.sendline("r")
	# p r s
	elif res(state[0], pkey) == res(state[1], rkey) and res(state[0], pkey) == res(state[2], skey):
		print("p")
		conn.sendline("s")
	# p s r
	elif res(state[0], pkey) == res(state[1], skey) and res(state[0], pkey) == res(state[2], rkey):
		print("p")
		conn.sendline("s")
	count += 1	
}

if count=21:
	conn.recvuntil("Your reward is")
	a = conn.recvline()
	print a


conn.interactive()

附上题目源码:

#!/usr/bin/env python3
from Crypto import Random
from Crypto.Random import random
from Crypto.Util.number import *
from secret import flag

sbox = [221, 229, 120, 8, 119, 143, 33, 79, 22, 93, 239, 118, 130, 12, 63, 207, 90, 240, 199, 20, 181, 4, 139, 98, 78, 32, 94, 108, 100, 223, 1, 173, 220, 238, 217, 152, 62, 121, 117, 132, 2, 55, 125, 6, 34, 201, 254, 0, 228, 48, 250, 193, 147, 248, 89, 127, 174, 210, 57, 38, 216, 225, 43, 15, 142, 66, 70, 177, 237, 169, 67, 192, 30, 236, 131, 158, 136, 159, 9, 148, 103, 179, 141, 11, 46, 234, 36, 18, 191, 52, 231, 23, 88, 145, 101, 17, 74, 44, 122, 75, 235, 175, 54, 40, 27, 109, 73, 202, 129, 215, 83, 186, 7, 163, 29, 115, 243, 13, 105, 184, 68, 124, 189, 39, 140, 138, 165, 219, 161, 150, 59, 233, 208, 226, 176, 144, 113, 146, 19, 224, 111, 126, 222, 178, 47, 252, 99, 87, 134, 249, 69, 198, 164, 203, 194, 170, 26, 137, 204, 157, 180, 168, 162, 56, 81, 253, 213, 45, 21, 58, 24, 171, 37, 82, 53, 50, 84, 196, 232, 242, 244, 64, 80, 10, 114, 212, 187, 205, 28, 51, 182, 16, 107, 245, 211, 85, 92, 195, 5, 197, 200, 31, 183, 61, 123, 86, 167, 154, 41, 151, 35, 247, 246, 153, 95, 206, 149, 76, 112, 71, 230, 106, 188, 172, 241, 72, 156, 49, 14, 214, 155, 110, 102, 116, 128, 160, 135, 104, 77, 91, 190, 60, 42, 185, 96, 97, 251, 218, 133, 209, 65, 227, 3, 166, 255, 25]
p = [5, 9, 1, 8, 3, 11, 0, 12, 7, 4, 14, 13, 10, 15, 6, 2]
round = 16


def pad(data, size = 16):
    pad_byte = (size - len(data) % size) % size
    data = data + bytearray([pad_byte]) * pad_byte
    return data


def repeated_xor(p, k):
    return bytearray([p[i] ^ k[i % len(k)] for i in range(len(p))])


def bytes_to_int(xbytes):
    return bytes_to_long(xbytes)


def int_to_bytes(x):
    return long_to_bytes(x, 16)


def group(input, size = 16):
    return [input[i * size: (i + 1) * size] for i in range(len(input) // size)]


def hash(secret):
    state = bytearray([208, 151, 71, 15, 101, 206, 50, 225, 223, 14, 14, 106, 22, 40, 20, 2])
    secret = pad(secret, 16)
    secret = group(secret)
    for roundkey in secret:
        for _ in range(round):
            state = repeated_xor(state, roundkey)
            for i in range(len(state)):
                state[i] = sbox[state[i]]
            temp = bytearray(16)
            for i in range(len(state)):
                temp[p[i]] = state[i]
            state = temp
    return hex(bytes_to_int(state))[2:]

def gen_commitments():
    secret = bytearray(Random.get_random_bytes(16))
    rc = hash(secret + b"r")
    pc = hash(secret + b"p")
    sc = hash(secret + b"s")
    secret = hex(bytes_to_int(secret))[2:]
    rps = [("r", rc), ("p", pc), ("s", sc)]
    random.shuffle(rps)
    return secret, rps

def check_win(move, inp):
    if move == "r":
        if inp == "p":
            return True
        else:
            return False
    elif move == "s":
        if inp == "r":
            return True
        else:
            return False
    elif move == "p":
        if inp == "s":
            return True
        else:
            return False
    return False

def main():
    print("Beat me in Rock Paper Scissors 20 consecutive times to get the flag")
    for i in range(20):
        secret, rps = gen_commitments()
        move = rps[0][0]
        print("Here are the possible commitments, the first one is my move:", " ".join(map(lambda s: s[1], rps)))
        inp = input("Your move:")
        res = check_win(move, inp)
        print("My move was:", move, "Secret was:", secret)
        if not res:
            print("You lose!")
            exit(0)

    print("You win")
    print("Your reward is", flag)
    exit(0)

if __name__ == '__main__':
    main()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值