The keystream is created at initialization. I haven’t look in detail at the functions PRGA and KSA, because .. we don’t need to 😄
1
2
3
4
5
6
def__init__(self,key,n=256):# this generates a keystream of length n, based on the input key# if the key expansion is secure, this is the right way to do it# because then, the XOR key has the same length as the message# and XOR becomes very secureself.KeyGenerator=self.PRGA(self.KSA(key,n),n)
The program always show the encrypted flag:
1
2
3
encrypted_flag=CrypTopiaSC(key).encrypt(flag)..print(f"Oh, one last thing: {encrypted_flag.hex()}")
And then, we can have it encrypt our message. We don’t know the key, but we’ll have the plaintext and ciphertext of our message.
1
2
3
pt=input("Enter your message: ").encode()ct=CrypTopiaSC(key).encrypt(pt)print(ct.hex())
The flaw relies on the fact the key is chosen once and for all, and consequently same for the keystream. So we have the same keystream to encrypt the flag and to encrypt our message. XOR encryption is secure if the key is long (okay) and only used once (ouch!).
Let’s suppose we encrypt a long plaintext (longer than the flag).
Then, we have ct = pt ^ keystream. We can work out the keystream:
keystream = ct ^ pt.
Then, we can easily decrypt the encrypted flag: flag = enc_flag ^ keystream.
Solution
We run the program a first time and see that the encrypted flag is, in hex: 5ec5b97e7b2996d72381fd988f2e4483ceac4466412075. This means the flag is 23 characters long. So, we chose a plaintext of 23 characters (or more).
defgen_pub_key(size):q=number.getPrime(size)# searches for p that divides p-1k=1p=k*q+1whilenotnumber.isPrime(p):k+=1p=k*q+1# searches for a generator g, order q modulo ph=randint(2,p-1)g=pow(h,(p-1)//q,p)whileg==1:h=randint(2,p-1)g=pow(h,(p-1)//q,p)# return public key pairreturnp,gdefget_encrypted_flag(k):# hash the shared secret to have adequate key length for AESk=sha256(k).digest()iv=get_random_bytes(AES.block_size)# read flag file and encryptdata=open("flag","rb").read()cipher=AES.new(k,AES.MODE_CBC,iv)padded_data=pad(data,AES.block_size)# prefixes IV to ciphertextencrypted_data=iv+cipher.encrypt(padded_data)returnencrypted_dataif__name__=='__main__':# generate public key pairp,g=gen_pub_key(N)# a is the private key - chosen randomlya=randint(2,p-1)# k_a = g^a mod p is the secret to share with the other endk_a=pow(g,a,p)# show p, then g, then k_asys.stdout.buffer.write(p.to_bytes(N))sys.stdout.buffer.write(g.to_bytes(N))sys.stdout.buffer.write(k_a.to_bytes(N))sys.stdout.flush()# read N bytes: k_b = g^b mod pk_b=int.from_bytes(sys.stdin.buffer.read(N))# compute k = k_b ^ a mod p = g^b^a mod pk=pow(k_b,a,p)# send encrypted flagsys.stdout.buffer.write(get_encrypted_flag(k.to_bytes((k.bit_length()+7)//8)))
The program initiates its own keys for Diffie-Hellman, then it expects my own secret (k_b) to compute a shared secret k.
Finally, it encrypts the flag with the shared secret.
Solution
So, this is nearly “not a challenge”, but simply implementing Diffie-Hellman on our side!
We need to (1) generate our private key b, (2) compute the shared secret: k=k_a ^ b mod p, and (3) create an AES key from SHA256(k) and (4) decrypt the flag.
We adapt to what the other ends sends us:
Read p
Read g
Read k_a
Those are bytes, we convert them to integers. Diffie-Hellman works with integers.
Generate b
Compute k_b
Send k_b
Compute k
Read the flag
Decrypt using AES
I encountered silly issues reading from the socket… because I was only reading 128 bytes, not 1024. Took me a while to see my mistake.
importsocketfromrandomimportrandintfromCrypto.CipherimportAESfromCrypto.Util.Paddingimportunpadfromhashlibimportsha256N=1024# Same as serverdefmain():# Connect to the serverhost='0.cloud.chals.io'port=26625withsocket.socket(socket.AF_INET,socket.SOCK_STREAM)ass:s.connect((host,port))# Receive p, g, and k_a from server - need to receive exactly N bytes eachp_bytes=b''whilelen(p_bytes)<N:chunk=s.recv(N-len(p_bytes))ifnotchunk:breakp_bytes+=chunkg_bytes=b''whilelen(g_bytes)<N:chunk=s.recv(N-len(g_bytes))ifnotchunk:breakg_bytes+=chunkk_a_bytes=b''whilelen(k_a_bytes)<N:chunk=s.recv(N-len(k_a_bytes))ifnotchunk:breakk_a_bytes+=chunkprint(f"Received {len(p_bytes)} bytes for p")print(f"Received {len(g_bytes)} bytes for g")print(f"Received {len(k_a_bytes)} bytes for k_a")# Convert bytes to integersp=int.from_bytes(p_bytes,'big')g=int.from_bytes(g_bytes,'big')k_a=int.from_bytes(k_a_bytes,'big')print(f"Received p: {hex(p)}")print(f"Received g: {hex(g)}")print(f"Received k_a: {hex(k_a)}")# Generate our private keyb=randint(2,p-1)print(f"Generated private key b: {hex(b)}")# Compute our public keyk_b=pow(g,b,p)print(f"Computed public key k_b: {hex(k_b)}")# Send our public key to serverk_b_bytes=k_b.to_bytes(N,'big')s.send(k_b_bytes)print(f"Sent {len(k_b_bytes)} bytes for k_b")# Compute shared secretshared_secret=pow(k_a,b,p)print(f"Computed shared secret: {hex(shared_secret)}")encrypted_flag=b''s.settimeout(2.0)try:whileTrue:chunk=s.recv(1024)ifnotchunk:breakencrypted_flag+=chunkexceptsocket.timeout:passprint(f"Received encrypted flag length: {len(encrypted_flag)}")print(f"Encrypted flag (hex): {encrypted_flag.hex()}")iflen(encrypted_flag)<AES.block_size:print("Error: Received data is too short to contain IV")return# Decrypt the flag# Convert shared secret to bytes (same way as server)k_bytes=shared_secret.to_bytes((shared_secret.bit_length()+7)//8,'big')print(f"Shared secret bytes length: {len(k_bytes)}")print(f"Shared secret bytes (hex): {k_bytes.hex()}")key=sha256(k_bytes).digest()print(f"AES key (hex): {key.hex()}")iv=encrypted_flag[:AES.block_size]ciphertext=encrypted_flag[AES.block_size:]print(f"IV (hex): {iv.hex()}")print(f"Ciphertext length: {len(ciphertext)}")print(f"Ciphertext (hex): {ciphertext.hex()}")iflen(ciphertext)%AES.block_size!=0:print(f"Warning: Ciphertext length ({len(ciphertext)}) is not a multiple of block size ({AES.block_size})")# Decrypttry:cipher=AES.new(key,AES.MODE_CBC,iv)decrypted_padded=cipher.decrypt(ciphertext)print(f"Decrypted padded (hex): {decrypted_padded.hex()}")# Remove paddingflag=unpad(decrypted_padded,AES.block_size)print(f"Decrypted flag: {flag.decode('utf-8')}")# Write flag to filewithopen('decrypted_flag.txt','w')asf:f.write(flag.decode('utf-8'))print("Flag written to decrypted_flag.txt")exceptExceptionase:print(f"Decryption error: {e}")if__name__=='__main__':main()
Initially, I wasn’t writing the flag to a file, but I immediately recognized in the decrypted flag the PNG header. So I dumped it to a file, and the PNG gave the flag.
Press Me If You Can
Description
This is a basic website with a silly button that’s moving all the time.
The challenge is to click on it nevertheless.
Web page analysis
This is the source code of the webpage. Pretty simple.
``html
Press me if you can
</body>
```
The logic behind the flying button is in script.js
1
2
3
4
5
6
7
8
9
10
11
12
constbtn=document.querySelector("button");...btn.style.left=endPoint.x+"px";btn.style.top=endPoint.y+"px";btn.disabled=true;...// Add an event listener for mouse movement
document.addEventListener('mousemove',(event)=>{const{clientX:mouseX,clientY:mouseY}=event;...
Solution
I first try simply to click on the button, through the developer console (F12):
1
document.querySelector('form').submit();
But that doesn’t work, probably because it doesn’t do the real mouse event, just calls submit.