chall
#!/usr/bin/python3
import os
import socketserver
import random
import signal
import string
import tempfile
import tarfile
from hashlib import sha256
from Crypto.Cipher import ChaCha20
from secret import flag,key
MAXNUM = 9
MAXFILESZ = 100
MAXTARSZ = 100000
class LicenseRequired(Exception):
pass
class Task(socketserver.BaseRequestHandler):
def proof_of_work(self):
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
digest = sha256(proof.encode('latin-1')).hexdigest()
self.request.send(str.encode("sha256(XXXX+%s) == %s\n" % (proof[4:],digest)))
self.request.send(str.encode('Give me XXXX:'))
x = self.request.recv(10).decode()
x = x.strip()
xx = x+proof[4:]
if len(x) != 4 or sha256(xx.encode('latin-1')).hexdigest() != digest:
return False
return True
def recvint(self):
try:
return int(self.request.recv(10))
except:
return 0
def recvfile(self, maxsz):
self.request.sendall(b"size: ")
sz = self.recvint()
assert sz < maxsz
self.request.sendall(b"file(hex): ")
r = 2*sz+1
res = b''
while r > len(res):
res += self.request.recv(r)
dat = bytes.fromhex(res.strip().decode('latin-1'))
return dat
def savefile(self, name, dat):
fname = os.path.join(self.dir, name)
with open(fname, "wb") as f:
f.write(dat)
self.request.sendall(f"[OK] {name} added\n".encode('latin-1'))
def addsec(self):
if len(self.data) > MAXNUM:
self.request.sendall(b"[Error] too many secrets\n")
raise LicenseRequired
name = os.urandom(4).hex()
self.data[name] = 1
dat = self.recvfile(MAXFILESZ)
self.savefile(name, dat)
def upload(self):
dat = self.recvfile(MAXTARSZ)
c = ChaCha20.new(nonce=dat[:8], key=key)
pt = c.decrypt(dat[8:])
self.savefile("a.tar", pt)
tname = os.path.join(self.dir, "a.tar")
if not tarfile.is_tarfile(tname):
self.request.sendall(b"[Error] not tar file\n")
self.request.sendall(pt.hex().encode('latin-1')+b'\n')
return
f = tarfile.open(tname)
cnt = 0
for fname in f.getnames():
if fname.startswith('/') or '..' in fname:
self.request.sendall(b"[Error] you need a license to hack us\n")
raise LicenseRequired
cnt += 1
if cnt > MAXNUM:
break
if len(self.data) + cnt > MAXNUM:
self.request.sendall(b"[Error] too many files\n")
raise LicenseRequired
for fname in f.getnames():
self.data[fname] = 1
f.extractall(path=self.dir)
os.unlink(tname)
self.request.sendall(b"[OK] upload succeeded\n")
def readsec(self):
raise LicenseRequired
def download(self):
nonce = True
for name in self.data:
if self.data[name] == 0:
nonce = False
fname = os.path.join(self.dir, "a.tar")
with tarfile.open("a.tar", 'w') as f:
for name in self.data:
f.add(name)
with open(fname, 'rb') as f:
cont = f.read()
c = ChaCha20.new(key=key)
dat = c.encrypt(cont)
if nonce:
dat = c.nonce+dat
self.request.sendall(f"[OK] ctar file size: {len(dat)}\n".encode('latin-1'))
self.request.sendall(dat.hex().encode('latin-1')+b'\n')
def addflag(self):
if len(self.data) > MAXNUM:
self.request.sendall(b"[Error] too many secrets\n")
raise LicenseRequired
name = os.urandom(4).hex()
self.data[name] = 0
self.savefile(name, flag)
def handle(self):
if not self.proof_of_work():
return
self.data = {}
self.dir = tempfile.mkdtemp()
os.chdir(self.dir)
signal.alarm(120)
self.request.sendall(b"*** Welcome to ctar service ***\nUpload and archive your secrets with our super fancy homemade secure tar file format. You'll like it\nNotice: the file size is limited in your free trial\n")
while True:
try:
self.request.sendall(b"1. add your secret\n2. upload ctar file\n3. read secrets\n4. download ctar file\n0. add flag\n> ")
i = self.recvint()
if i==1:
self.addsec()
elif i==2:
self.upload()
elif i==3:
self.readsec()
elif i==4:
self.download()
elif i==0:
self.addflag()
else:
break
except:
pass
os.system(f"rm -r {self.dir}")
self.request.close()
class ForkedServer(socketserver.ForkingTCPServer, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10001
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
analysis
We can upload any data as long as its length is less than 100, and then re-download the data encrypted with chacha20.
Since download() returns the encrypted data along with the nonce used in chacha20, we can decrypt the downloaded data by using upload() with the nonce.
Additionally, we can observe the result of the decryption process using one-byte error
After calling addflag(), download() does not provide us with nonce to be used in upload().
This means we cannot decrypt the output of download().
cause
Since we know the filename of the flag file, if we include the flag_filename in a.tar, it becomes 'self.data=1'.
But, the flag file will be overwritten with the content of a.tar, which is different from the original flag, during decompression.
However, overwriting can be prevented by triggering an error within the extractall() function, as it is enclosed within a try-except statement.
solve
1. addsec()
2. download()
3. addflag()
4. modify the filename in a.tar
5. generate an error by changing the mode and typeflag
6. calculate a checksum for .tar and then modify the checksum field.
7. upload() with that file
8. download() includes the flag
9. upload() to see the decrypt output
An error occurred when the mode was set to 0777 and the type flag was set to 3
from pwn import *
from hashlib import sha256
r = remote("chall.ctf.0ops.sjtu.cn", "30001")
''' POW '''
r.recvuntil(b'XXXX+')
suffix=r.recvuntil(b') == ').decode().split(')')[0]
sha=r.recvline()[:-1].decode()
print(suffix,sha)
alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def pow():
for a1 in alphabet:
for a2 in alphabet:
for a3 in alphabet:
for a4 in alphabet:
proof=a1+a2+a3+a4+suffix
if(sha256(proof.encode('latin-1')).hexdigest()==sha):
print('[+]pow found')
r.sendline((a1+a2+a3+a4).encode())
return
pow()
''''''
'''addsec()'''
r.sendlineafter("> ", "1")
r.sendlineafter("size: ", "3")
r.sendlineafter("file(hex): ", "aabbcc")
''''''
'''download()'''
r.sendlineafter("> ", "4")
r.recvuntil("ctar file size: ")
siz = r.recvline()[:-1]
dat = r.recvline()[:-1]
'''get a original file using upload()'''
r.sendlineafter("> ", "2")
r.sendlineafter("size: ", siz)
r.sendlineafter("file(hex): ", dat[:16] + b"00" + dat[18:])
r.recvuntil("[Error] not tar file\n")
server_tar = r.recvline()[:-1].decode()
server_tar = "2E" + server_tar[2:]
stream = []
for i in range(len(dat[16:])//2):
stream.append(int(server_tar[2*i:2*i+2], 16) ^ int(dat[16+2*i:16+2*i+2], 16))
'''addflag()'''
r.sendlineafter("> ", "0")
flagfile_name = r.recvline()[:-1].split(b' ')[1]
assert len(flagfile_name) == 8
''''''
'''modify a.tar'''
data = bytes.fromhex(server_tar)
our_data = data[0x400:0x400+0x1f4]
# filename
our_data = flagfile_name + our_data[8:]
# mode
our_data = our_data[:100] + b"0000777\x00" + our_data[100+8:]
# typeflag
our_data = our_data[:156] + b"3" + our_data[156+1:]
''''''
'''calc checksum'''
chk_data = our_data[:148] + b" " * 8 + our_data[148+8:]
checksum = 0
for i in range(len(our_data)):
checksum += chk_data[i]
# final
our_data = our_data[:148] + bytes("%06o\0" % checksum, "ascii") + b"\x20" + our_data[148+8:]
attack_tar = data[:0x400] + our_data + data[0x400+0x1f4:]
''''''
'''upload()'''
r.sendlineafter("> ", "2")
r.sendlineafter("size: ", siz)
payload = []
for i in range(len(stream)):
payload.append(stream[i] ^ attack_tar[i])
r.sendlineafter("file(hex): ", dat[:16] + bytes(payload).hex().encode('latin-1'))
'''download()'''
r.sendlineafter("> ", "4")
r.recvuntil("ctar file size: ")
siz = r.recvline()[:-1]
dat = r.recvline()[:-1]
'''upload()'''
r.sendlineafter("> ", "2")
r.sendlineafter("size: ", siz)
r.sendlineafter("file(hex): ", dat[:16] + b"00" + dat[18:])
r.recvuntil("[Error] not tar file\n")
flag = bytes.fromhex(r.recvline()[:-1].decode())
for i in range(len(flag)):
if b"flag" == flag[i:i+4]:
print(flag[i:i+100])
r.interactive()
'writeups' 카테고리의 다른 글
2024MoveCTF (0) | 2024.01.28 |
---|---|
2023 X-mas CTF (web3 - alpha hunter) (0) | 2023.12.31 |
GlacierCTF 2023 - smartcontract (0) | 2023.11.26 |
N1CTF 2023 - blockchain(pool by sec3) (0) | 2023.10.23 |
SECCON CTF 2023 Quals - [misc, blockchain](tokyo payload) (0) | 2023.09.22 |