题目描述
#just exec
题目给了py文件,都是Base64的数据
但是非常大,脚本文件大小达到5M。
根据题目描述直接执行
额,还报错了,应该是解密最后的代码少了个单引号,也不知道是疏漏还是故意的😅
那我们就大概知道,这题应该是一直解密,然后得到最后的Python代码,得到的话基本题目就出了。
取Base64编码解密得到如图
那大概就是毫无意义的Base加密套娃,每次解密大概都会有exec(basexx.xxxdecode(r”””………”””)),编码方式大概有Base64、Base32、Base58、Base85
这里又学到了个知识,原始字符串
我们需要一直执行代码解密直到得到最后的Python代码。当时比赛情况下,我是手搓的,并且还花了挺长时间的,因为发现Python的Base64模块用的Base编码和解密方式在一些在线网站解密的话不行,我做了很久才发现这一点,所以只能在Python里面解密了。
最后解密了非常多次,成功将题目的Python代码弄出来了。
这题并不难,复现是为了记录下如何自动化利用Python脚本解密那一大坨Base编码,问题主要出现在每次都会出现exec(basexx.xxxdecode(r”””………”””)),所以肯定不能无脑decode,要有一些处理方式,当然也并不难。
目前看到的方法大概有两种,大概是
//提取exec()函数中的代码,再执行
//对exec函数进行hook
第一种方法的Python代码如下
import base64
with open(r"C:\Users\Daki\Desktop\chall.py",'r') as f:
data=f.read()
#读文件数据
data=data.replace("import base64","")
#把文件里导入模块的代码去掉
while "exec" in data:
data=eval(data.replace("exec","")).decode()
#每次都把exec去掉,执行括号内的代码
print(data)
这种方法应该是最简便的,这里的eval和exec有一个区别就是,eval会返回代码执行后的结果,但是exec不会返回结果。所以我们用eval执行,用data接收。
然后群友还提出了hook exec函数的做法,感觉这种想法挺有意思的,我想了一下,然后试了一下,代码如下
import base64
with open(r"C:\Users\Daki\Desktop\chall.py",'r') as f:
data=f.read()
data=data.replace("import base64","")
def my_exec(x):
x=eval(x.replace("exec","")).decode()
if "exec" in x:
my_exec(x)
if "exec" not in x:
print(x)
exec = my_exec
exec(data)
但是感觉其实和第一种做法异曲同工,不知道有没有更帅的hook方法。但是做法应该大概都是这样把。
得到的Python代码如下
a = True
d = len
G = list
g = range
s = next
R = bytes
o = input
Y = print
def l(S):
i = 0
j = 0
while a:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield K
def N(key, O):
I = d(key)
S = G(g(256))
j = 0
for i in g(256):
j = (j + S[i] + key[i % I]) % 256
S[i], S[j] = S[j], S[i]
z = l(S)
n = []
for k in O:
n.append(k ^ s(z) + 2) #这里魔改了一下
return R(n)
def E(s, parts_num):
Q = d(s.decode())
S = Q // parts_num
u = Q % parts_num
W = []
j = 0
for i in g(parts_num):
T = j + S
if u > 0:
T += 1
u -= 1
W.append(s[j:T])
j = T
return W
if __name__ == '__main__':
L = o('input the flag: >>> ').encode()
assert d(L) % 2 == 0, 'flag length should be even'
t = b'v3ry_s3cr3t_p@ssw0rd'
O = E(L, 2)
U = []
for i in O:
U.append(N(t, i).hex())
if U == ['1796972c348bc4fe7a1930b833ff10a80ab281627731ab705dacacfef2e2804d74ab6bc19f60', '2ea999141a8cc9e47975269340c177c726a8aa732953a66a6af183bcd9cec8464a']:
#这里的单引号我自己补上去的
Y('Congratulations! You got the flag!')
else:
Y('Wrong flag!')
很明显的一个RC4加密算法,这个E函数自己代码拉出来执行了一下,其实就是将输入平均分成两半。
但是奇怪的是U数组中的两个十六进制字符串位数却不相等。
这里的RC4算法小小改了一下最后异或的时候
for k in O:
n.append(k ^ s(z) + 2)
需要注意的是,会先执行s(z) + 2,再执行异或,一开始以为是先异或再加法,导致没解出flag
可以用以下代码验证一下运算顺序
data1=66^14+2
data2=(66^14)+2
data3=66^(14+2)
print(data1)
#82
print(data2)
#78
print(data3)
#82
挺神奇的,一直以为是异或先算。
以下是解题脚本
def KSA(key):
""" Key-Scheduling Algorithm (KSA) """
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
return S
def PRGA(S):
""" Pseudo-Random Generation Algorithm (PRGA) """
i, j = 0, 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield K
def RC4(key, text):
""" RC4 encryption/decryption """
S = KSA(key)
keystream = PRGA(S)
res = []
for char in text:
res.append(char ^ next(keystream)+2)
return bytes(res)
key = b'v3ry_s3cr3t_p@ssw0rd'
plaintext1 =bytes.fromhex('1796972c348bc4fe7a1930b833ff10a80ab281627731ab705dacacfef2e2804d74ab6bc19f60')
ciphertext1 = RC4(key, plaintext1)
print(ciphertext1)
#b'flag{thEn_I_Ca5_BE_YoUR_Onl7_ExeCUti6n'
plaintext2 =bytes.fromhex('2ea999141a8cc9e47975269340c177c726a8aa732953a66a6af183bcd9cec8464a')
ciphertext2 = RC4(key, plaintext2)
print(ciphertext2)
#b'_So_Use_m3_t0_R0n_tH17_Ex3Cuti0n}'
#flag{thEn_I_Ca5_BE_YoUR_Onl7_ExeCUti6n_So_Use_m3_t0_R0n_tH17_Ex3Cuti0n}
所以flag就是flag{thEn_I_Ca5_BE_YoUR_Onl7_ExeCUti6n_So_Use_m3_t0_R0n_tH17_Ex3Cuti0n}
这个脚本用GPT写的,然后才发现RC4加密和解密可以用同一个代码,还是做题少了😅