R3CTF - r1system Writeup

This challenge was the continuation of r0system and also wasn’t involved with crypto that much.

I still don’t know if they release r1system as the “real” final stage of r0system but they had a mistake or if the mistake was actually intended:

elif option == 3:
    username = bytes.fromhex(input(b"Username[HEX]: ".decode()))
    if username == AliceUsername or username == AliceUsername:
    print(b"You can't!")
    return

r1system had a few differences from r0system, the main one was being able to send messages through the “PublicChannel”.

What I did was register as bob and ten just send a message to alice, this would show the shared AB key:

change_key = USER.ecdhs[USERNAME].exchange_key(ToPublickey)
print((b"Exchanged Key is: " + change_key.hex().encode() ) .decode())

This was still executed:

def Alice_transfer_flag_to_Bob(AliceUsername,BobUsername):
    global PublicChannels
    PublicChannels += transfer_A2B(USER,AliceUsername,BobUsername,b" Halo bob, I will give your my flag after we exchange keys.")
    PublicChannels += transfer_A2B(USER,BobUsername,AliceUsername,b" OK, I'm ready.")
    PublicChannels += transfer_A2B(USER,AliceUsername,BobUsername,b" My Pubclic key is: " + USER.getsb_public_key(AliceUsername).hex().encode())
    PublicChannels += transfer_A2B(USER,BobUsername,AliceUsername,b" My Pubclic key is: " + USER.getsb_public_key(BobUsername).hex().encode())
    PublicChannels += transfer_A2B(USER,AliceUsername,BobUsername,b" Now its my encrypted flag:")
    PublicChannels += transfer_A2B(USER,AliceUsername,BobUsername,   FLAG1 ,enc=True)
    PublicChannels += transfer_A2B(USER,BobUsername,AliceUsername,b" Wow! I know your flag now! ")

Then with the AB key you just decrypt the AES encrypted flag.

Here is the final exploit:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template server.py --host localhost --port 1337
from pwn import *
from Crypto.Cipher import AES
from hashlib import md5

# Set up pwntools for the correct architecture
context.update(arch='i386')
exe = 'server.py'

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141 EXE=/tmp/executable
host = args.HOST or 'ctf2024-entry.r3kapig.com'
port = int(args.PORT or 30718)

def start_local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process(['python3'] + [exe] + argv, *a, **kw)

def start_remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return start_local(argv, *a, **kw)
    else:
        return start_remote(argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

ALICE=b'416c6963654973536f6d65426f6479'
BOB  =b'426f6243616e4265416e79426f6479'

BOB_PUBK=''
BOB_PRVK=''

ALICE_PUBK=''
ALICE_PRVK=''

ENC_FLAG=''

def register(username, password):
    io.sendlineafter(b'Now input your option:', b'3')
    io.sendlineafter(b'Username[HEX]:', username)
    io.sendlineafter(b'Password[HEX]:', password)
    log.info(f"Registered {username} with password: {password}")

def login(username, password):
    io.sendlineafter(b'Now input your option:', b'1')
    io.sendlineafter(b'Username[HEX]:', username)
    io.sendlineafter(b'Password[HEX]:', password)
    log.info(f"Logged in as {username}")

def resetpassword(username, newpassword):
    io.sendlineafter(b'do you need any services?', b'1')
    io.sendlineafter(b'Password[HEX]:', newpassword)
    log.info(f"Reseted password of {username} to {newpassword}")

def getkeys():
    io.sendlineafter(b'do you need any services?', b'4')
    io.recvuntil(b'Your private key is:')
    priv = io.recvline().strip().decode()
    priv = b2i(bytes.fromhex(priv))
    io.recvuntil(b'Your public key is:')
    pub = io.recvline().strip().decode()
    pub = b2p(bytes.fromhex(pub))
    return [pub, priv]

def exchange_keys(dest):
    io.sendlineafter(b'do you need any services?', b'2')
    io.sendlineafter(b'ToUsername[HEX]:', dest)
    io.recvuntil(b'Exchanged Key is: ')
    key = io.recvline().strip().decode()
    key = bytes.fromhex(key)
    return key

def get_enc_flag():
    io.sendlineafter(b'do you need any services?', b'3')
    io.recvuntil(b'Now its my encrypted flag:\n')
    io.recvuntil(b'[AliceIsSomeBody] to [BobCanBeAnyBody]: ')
    enc_flag = io.recvline().strip().decode()
    return enc_flag

def logout():
    io.sendlineafter(b'do you need any services?', b'5')
    log.info(f"Logged out")

# Key is derived from A + B key exchange
def enc(msg,key):
    aes = AES.new(key,AES.MODE_ECB)
    return aes.encrypt(pad(msg))

class Curve: 
    def __init__(self):
        # Nist p-256
        self.p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
        self.a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
        self.b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
        self.G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 
                  0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
        self.n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551

    def add(self,P, Q):
        if (P == (0, 0)):
            return Q
        elif (Q == (0, 0)):
            return P
        else: 
            x1, y1 = P
            x2, y2 = Q
            if ((x1 == x2) & (y1 == -y2)):
                return ((0, 0))
            else:
                if (P != Q):
                    l = (y2 - y1) * pow(x2 - x1, -1, self.p)
                else:
                    l = (3 * (x1**2) + self.a) * pow(2 * y1, -1, self.p)
            x3 = ((l**2) - x1 - x2) % self.p
            y3 = (l * (x1 - x3) - y1) % self.p
            return x3, y3

    def mul(self, n , P):
        Q = P
        R = (0, 0)
        while (n > 0):
            if (n % 2 == 1):
                R = self.add(R, Q)
            Q = self.add(Q, Q)
            n = n // 2
        return R

def i2b(i,l):
    return int.to_bytes(i,length=l,byteorder='big')

def b2i(b):
    return int.from_bytes(b,byteorder='big')

def p2b(P):
    return i2b(P[0],32) + i2b(P[1],32)

def b2p(m):
    return (b2i(m[:32]),b2i(m[32:]))

MOD  = 0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000283
SEED = b2i(os.urandom(128))
class RandomNG:
    def __init__(self, mod, seed):
        self.coeffs = [randint(1,mod) for _ in range(8)]
        self.mod = mod
        self.state = seed 

    def next(self):
        old_state = int(self.state)
        self.state = sum(coeff * self.state**i for i,coeff in enumerate(self.coeffs)) % self.mod
        return old_state

class ECDH:
    def __init__(self, pub, priv):
        self.curve = Curve()
        self.private_key = priv
        self.public_key  = pub

    def exchange_key(self,others_publickey):
        return md5(str(self.curve.mul(self.private_key,others_publickey)).encode()).digest()

io = start()

register(BOB, b'16')
login(BOB, b'16')

ab = exchange_keys(ALICE)

enc_flag = get_enc_flag()

aes = AES.new(ab,AES.MODE_ECB)
flag = aes.decrypt(bytes.fromhex(enc_flag)).decode(errors='ignore').strip()

log.success(f"Flag found: {flag}")