Guenael

CTF, challenges & writeup

Article - TinyBeacon project

TinyBeacon enables an easy setup of VHF and UHF beacons, through a simple and compact design, using a credit card-size PCB, at a low cost, and with easy installation close to the antenna. The PLL system is disciplined by GPS, allowing digital modes like WSPR and PI4, which require good clock stability and time synchronization. The system integrates a DC-DC regulator that accepts flexible power between 10 and 15 volts, with a common QRP output power of 5W. The reference design is provided for 50, 144, 220 and 432 MHz, but the circuit could be adapted to other bands.

Download here

TinyBeacon project

Article - Slot antennas for amateur radio bands

My last paper presents designs and calculations for a set of slot antennas centered on the amateur microwave bands. Special attention has been given to ease of construction, using simple supply materials available in standard sizes in the construction industry. Also, the traditional calculation formulas, which are only valid with a perfect waveguide, have been extended using iterative simulation and optimization to apply to common materials, while maintaining good performance.

Download here

Slot antennas for amateur radio bands

ebCTF - CRY100: Clear text attack

The second challenge for the ebCTF teaser was very easy too, and solved by a very large number of participants. For this crypto challenge named CRY100, the Zip file provided was self-contained and it can be started offline (available here ).

After unpacking, the README file drop a password, useful to decrypt the first encrypted message, but not the second. So, we have to decrypt this second message without additional help, but the answer is not very far :) Indeed, watching the code gives interesting details, and the most important one should be about the ciphering function, where a XOR is simply used. Another interesting point was about the input key, hashed many time before to be used on the XOR function.

The context given with the previous decrypted message illustrate a perfect case of plaintext attack : With the previous decrypted message, we can bet than the second ciphered message have the same header. Since the ciphering function was a simple XOR, we only have to take the first bloc of the ciphered text and XOR it with the clear text bloc to get the key. Of course, it’s not the original text key, but the computed key. For sure we cannot deduce easily the initial text key (hash = one way-function), but we don’t need to know this key, deciphering the second message is good enough :)

Well, adding only 2 lines of code allows to retrieve the computed key and get the content of the ciphered message.

clearText = "From: Vlugge Jap"
k = xor(msg[0:16], clearText)

In the application code:

#!/usr/bin/python2
import hashlib, string, sys

ROUNDS = 20000

def xor(a, b):
    l = min(len(a), len(b))
    return ''.join([chr(ord(x) ^ ord(y)) for x, y in zip(a[:l], b[:l])])

def h(x):
    x = hashlib.sha256(x).digest()
    x = xor(x[:16], x[16:])
    return x

def verify(x):
    return all([ord(c) < 127 for c in x])

def crypt(msg, passwd):
    k = h(passwd)

    for i in xrange(ROUNDS):
        k = h(k)

    out = ''

    # Clear text attack - ebCTF-CRY100 (2 lines)
    clearText = "From: Vlugge Jap"
    k = xor(msg[0:16], clearText)

    for i in xrange(0, len(msg), 16):
        out += xor(msg[i:i+16], k)
        k = h(k + str(len(msg)))

    return out

def encrypt(msg, passwd):
    msg = crypt(msg, passwd)

    return msg.encode('base64')

def decrypt(msg, passwd):
    msg = crypt(msg.decode('base64'), passwd)

    if verify(msg):
        return msg
    else:
        sys.stderr.write('Looks like a bad decrypt\n')
        sys.exit(1)

if len(sys.argv) < 5 or sys.argv[1] not in ('encrypt', 'decrypt'):
    print 'Usage:\tcrypto.py encrypt <password> <infile> <outfile>'
    print '\tcrypto.py decrypt <password> <infile> <outfile>'
    sys.exit(1)

op, passwd, infile, outfile = sys.argv[1:]

inp = open(infile).read()

if op == 'encrypt':
    ct = encrypt(inp, passwd)
    open(outfile, 'w').write(ct)
elif op == 'decrypt':
    pt = decrypt(inp, passwd)
    open(outfile, 'w').write(pt)

And finally, the result:

ebCTF CRY100 Decoded message

Thanks again Eindbazen!

ebCTF - BIN100: The dices secret face

Last week-end, I played some hours to the ebCTF teaser. The first challenges (100 points) were very easy, but funny in the same time. BIN100 was a console application for Windows, containing a tiny dice game. The goal was to get the right sequence of dices to win the game. I’m pretty sure that it was possible to beat the game in different ways, but I choose an easy one, using IDA to understand the internals and force the destiny :)

Playing the game in a usual way shows some interesting strings, for example : “You rolled a three! Good!”, and I simply chose to search “Good” in IDA to locate some interesting blocs of code.

ebCTF BIN 100 Game screen capture

The first hit was without appeal and you can see on the screenshot below the conditional jump before the printed result. So, I nopped all the five calls to complete the sequence, directly on the binary file. By chance, there is no internal check (ex. hash check on the code) and the program gave me the flag without any more question.

ebCTF BIN 100 IDA debugging

The funny joke appear during the last dice roll, the next screenshot speaks for itself :)

ebCTF BIN 100 Dice joke

Thanks Eindbazen!

PlaidCTF giga250: Common factor attack

The last PlaidCTF was full of very interesting challenges. Giga was one of them in the crypto thread. The source code of the application was provided and it reveal an RSA cryptosystem. Each time you connect the server, it generate a new key and encrypt the flag with it, after you can send any strings and get the encrypted result.

A quick look into the code shows extra effort on the random function, to insert a vulnerability. The quality of the generated random is degraded and partially reused beetween sessions. So, we can guess a faulty key generation, and a possible attack is about common factor. A good reading is available here.

The source code provided is self-contained and I started to debug my attack locally, simulating the same conditions :

#!/usr/bin/env python

# PlaidCTF [FFF] Crypto challenge : giga 250pts
# Writeup : Guenael
#
# Vulnerability : Random fault RSA key generation
# Attack : RSA common factor attack 
# References : http://www.loyalty.org/~schoen/rsa/

import os
import sys
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.PublicKey import RSA
from Crypto.Hash import MD5

DEBUG = 1

# Code from Rosettacode : http://rosettacode.org/wiki/Greatest_common_divisor#Python
def gcd(u, v):
	return gcd(v, u % v) if v else abs(u)

# Code from Rosettacode : http://rosettacode.org/wiki/Modular_inverse#Python
def extended_gcd(aa, bb):
    lastremainder, remainder = abs(aa), abs(bb)
    x, lastx, y, lasty = 0, 1, 1, 0
    while remainder:
        lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder)
        x, lastx = lastx - quotient*x, x
        y, lasty = lasty - quotient*y, y
    return lastremainder, lastx * (-1 if aa < 0 else 1), lasty * (-1 if bb < 0 else 1)

# Code from Rosettacode : http://rosettacode.org/wiki/Modular_inverse#Python
def modinv(a, m):
	g, x, y = extended_gcd(a, m)
	if g != 1:
		raise ValueError
	return x % m

# Random fault vulnerability
def rng(n):
	if DEBUG>1: print 'arg. n= ' + str(n)
	global rbuf
	rand = rbuf[:n]
	rbuf = rbuf[n:]
	while (len(rbuf) < 4096):
		hr.update(os.urandom(4096))
		rbuf += rbuf + hr.hexdigest()
	if DEBUG>1: print 'ret. rand= ' + str(bytes_to_long(rand))
	return rand

rbuf = os.urandom(4096)
hr = MD5.new()

clearText = 'theSecretLocalFlag'

nArray = []
while True:
	rsa = RSA.generate(1024,rng)
	e=65537 #e = getattr(rsa,'e') #65537 by default
	ciphertext = bytes_to_long(rsa.encrypt(clearText,"")[0])

	a = 2**e - bytes_to_long(rsa.encrypt("\x02","")[0])
	b = 3**e - bytes_to_long(rsa.encrypt("\x03","")[0])
	n = long(extended_gcd(a,b)[0])

	for nc in nArray:
		p = extended_gcd(nc,n)[0]
		if (p > 2**510): # Possible cadidate close to 1024/2 bits ?
			q = n / p    # Deduct q
			d = modinv(e, (p-1)*(q-1)) # Calculate the private key
			if DEBUG:
				print "n= " + str(n) + "\n"
				print "p= " + str(p) + "\n"
				print "q= " + str(q) + "\n"
				print "d= " + str(d) + "\n"

			print("Decrpyted ciphertext: ")
			print long_to_bytes(pow(ciphertext, d, n))

			quit()
	nArray.append(n)
	if DEBUG: print "Trying another candidate..."

In a second time, the code was adapted to connect the real server :

#!/usr/bin/env python

# PlaidCTF [FFF] Crypto challenge : giga 250pts
# Writeup : Guenael
#
# Vulnerability : Random fault RSA key generation
# Attack : RSA common factor attack 
# References : http://www.loyalty.org/~schoen/rsa/

import re
import telnetlib
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.PublicKey import RSA
from Crypto.Hash import MD5

DEBUG = True

# Code snip from Rosettacode : http://rosettacode.org/wiki/Modular_inverse#Python
def extended_gcd(aa, bb):
    lastremainder, remainder = abs(aa), abs(bb)
    x, lastx, y, lasty = 0, 1, 1, 0
    while remainder:
        lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder)
        x, lastx = lastx - quotient*x, x
        y, lasty = lasty - quotient*y, y
    return lastremainder, lastx * (-1 if aa < 0 else 1), lasty * (-1 if bb < 0 else 1)

# Code snip from Rosettacode : http://rosettacode.org/wiki/Modular_inverse#Python
def modinv(a, m):
	g, x, y = extended_gcd(a, m)
	if g != 1:
		raise ValueError
	return x % m

e=65537 #65537 used by default on Crypto.PublicKey API

HOST = '184.73.59.25'
PORT = 4321
MSG  = "Now enter a message you wish to encrypt: "

nArray = []
while True:
	tn = telnetlib.Telnet(HOST,PORT)
	ciphertext = long(re.search('={34}\W(\w+?)\W={34}', tn.read_until(MSG), re.MULTILINE).groups()[0],16)

	tn.write("\x02") # Short values are faster for next gcd
	
	a = 2**e - long(re.search('={34}\W(\w+?)\W={34}', tn.read_until(MSG), re.MULTILINE).groups()[0],16)

	tn.write("\x03")
	b = 3**e - long(re.search('={34}\W(\w+?)\W={34}', tn.read_until(MSG), re.MULTILINE).groups()[0],16)

	tn.close()

	n = long(extended_gcd(a,b)[0]) # Extract the public key

	for nc in nArray:
		# Looking for a common factor with the previous keys
		p = extended_gcd(nc,n)[0] 
		# Remove false positives. Prime close to 1024/2 bits ?
		if (p > 2**510): 
			q = n / p
			d = modinv(e, (p-1)*(q-1)) # Calculate the private key
			if DEBUG:
				print "n= " + str(n) + "\n"
				print "p= " + str(p) + "\n"
				print "q= " + str(q) + "\n"
				print "d= " + str(d) + "\n"

			print("Decrpyted ciphertext: ")
			print long_to_bytes(pow(ciphertext, d, n)) # Decode ciphertext with the private key

			quit()
	nArray.append(n)
	if DEBUG: print "Trying another candidate..."

And finally, the original source code of the challenge (available on other place, but given here to be self-contained)

Thanks FFF!

#!/usr/bin/env python
import os
from Crypto.PublicKey import RSA
from Crypto.Hash import MD5
import SocketServer
import threading
import time

rbuf = os.urandom(4096)
hr = MD5.new()

flag = open("secret").read()

def rng(n):
  global rbuf
  rand = rbuf[:n]
  rbuf = rbuf[n:]
  while (len(rbuf) < 4096):
    hr.update(os.urandom(4096))
    rbuf += rbuf + hr.hexdigest()
  return rand


class threadedserver(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

class incoming(SocketServer.BaseRequestHandler):
  def handle(self):
    cur_thread = threading.current_thread()
    welcome = """
*******************************************
*** Welcome to GIGA! ***
**the super secure key management service**
*******************************************

We are generating an RSA keypair for you now.
(Please be sure to move your mouse to populate the entropy stream)
"""
    self.request.send(welcome)
    rsa = RSA.generate(1024,rng)
    print getattr(rsa,'n')
    #make it look like we're doing hardcore crypto
    for i in xrange(20):
      time.sleep(0.2)
      self.request.send(".")
    self.request.send("\nCongratulations! Key created!\n")

    #no one will ever be able to solve our super challenge!
    self.request.send("To prove how secure our service is ")
    self.request.send("here is an encrypted flag:\n")
    self.request.send("==================================\n")
    self.request.send(rsa.encrypt(flag,"")[0].encode("hex"))
    self.request.send("\n==================================\n")
    self.request.send("Find the plaintext and we'll give you points\n\n")

    #now they can be safe from the FBI too!
    while True:
      self.request.send("\nNow enter a message you wish to encrypt: ")
      m = self.request.recv(1024)
      self.request.send("Your super unreadable ciphertext is:\n")
      self.request.send("==================================\n")
      self.request.send(rsa.encrypt(m,"")[0].encode("hex"))
      self.request.send("\n==================================\n")

server = threadedserver(("0.0.0.0", 4321), incoming)
server.timeout = 4
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()

server_thread.join()

The website is up!

I decided to replace my old website by this new blog, focuses on CTF security challenges. No rocket science here, only my tiny experiments and some lines of code. Feel free to send me an email if you have questions or if you want exchange on some ctf problems. Thank you for your visit and good reading.