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
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
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()