Jcxp's blog love and peace

2018-11-8-defcamp-finals-authenticator Writeup

2018-11-08
p4-team

ctf
 

authenticator (pwn, 2 solves)

This task was categoried as pwn, but in fact it was RE and simple crypto. The hardest part of this task was to reverse engineer the binary.

main calls following functions:

000000000406950 is sha1 function from boost library. The proof is the string /usr/include/boost/uuid/sha1.hpp.

0000000000406410 is also a cryptography function. Let’s call it crypto_function now, we will look at it closely later.

The pseudocode of main() function is below:

int main()
{
	sha = boost_sha1(); //buffer of 16 bytes
	
	if (strcmp(user_input(),"HELLO")) return 0;
	
	bytes = read("/dev/urandom",16); //buffer of 16 bytes
	print_bytes_hex(bytes);
	bytes2 = read("/dev/urandom",16); //buffer of 16 bytes
	print_bytes_hex(bytes2);
	
	print_some_strange_data(); //idk what is this, it's not needed anyway :)
	
	if(crypto_function(bytes2, user_input(), 16, some_bytes) == bytes1)
	  print crypto_function(bytes2, flag, 16, some_bytes);
}

user_input is a function which reads data from the user.

Now let’s investigate crypto_function. I was sure that this isnt any new crypto but just from some library. We can see that it calls various virtual methods from vtable. After navigating to the memory of them we can gain more information.

For example above offset 73F500 is located the following string:

.data.rel.ro:000000000073F4F8                 dq offset _ZTIN8CryptoPP14CTR_ModePolicyE ; `typeinfo for'CryptoPP::CTR_ModePolicy
.data.rel.ro:000000000073F500 unk_73F500      db    0  

also this one is interesting:

.data.rel.ro:0000000000741BC8                 dq offset _ZTIN8CryptoPP8Rijndael4BaseE ; `typeinfo for'CryptoPP::Rijndael::Base
.data.rel.ro:0000000000741BD0 unk_741BD0      db    0                 ; DATA XREF: sub_406410+3CD↑o

So I just concluded that this is AES-128 encryption in CTR mode. I’ve set a breakpoint at this function and printed their arguments:

  • 1: random data, different at every time.
  • 2: user input
  • 3: int 16
  • 4: 0x0d00000f0d0a0af7, 0x0d00000f0d0a0a0b
  • 5: where encrypted data will be stored

I came to the conclusion that the first argument is ctr, 4 is the key. I checked if my theory about this cipher function is correct - I copied arguments and output abd wrote the following python script:

from pwn import *
from crypto_commons.symmetrical import aes
        
def aes_encode(input):
	global key
	global ctr
	AES = aes.AES()
	AES.init(key)
	w = AES.encrypt(ctr)
	w = xor(input, w)
	return w

def aes_decode(input):
	global key
	global ctr
	AES = aes.AES()
	AES.init(key)
	w = AES.encrypt(ctr)
	w = xor(input, w)
	return w	
	
key = "f70a0a0d0f00000d0b0a0a0d0f00000d" #wytestowac nowe kombinacje
ctr = "f3 e9 cf 98 bb 8d 94 58 43 61 21 f4 f8 e3 19 ad"
input = "a"*16

ctr = ctr.replace(" ","").decode("hex")
key = key.replace(" ","").decode("hex")
	
x = aes_encode(input)
print "encrypted user input: "+x.encode("hex")
print "the output from binary: 9bcefda8b86570db6d8330986472ac5e"

output:

a@x:~/Desktop/Authenticator$ python test.py 
encrypted user input: 9bcefda8b86570db6d8330986472ac5e
the output from binary: 9bcefda8b86570db6d8330986472ac5e

It proves that my predictions were correct.

if we want to pass if(crypto_function(bytes2, user_input(), 16, some_bytes) == bytes1) in main function, it’s obvious, that user input needs to be equal the output of the decryption function AES-128 CTR mode with bytes1 as data to decrypt

The key to crypto_function was the same at every run when ASLR was switched off. When ASLR was switched on, the first byte of the key was different at every run. I also checked this on different linux systems and it was the same. So I wrote the exploit that connects to the server and tries to brute-force all possibilities of the first byte of the key:

from pwn import *
from crypto_commons.symmetrical import aes

def recvall(r):
    d = ""
    n = "a"
    
    while n:
        n = r.recv(timeout = 0.2)
        d += n
        
    return d 
	
def aes_decode_(key,input,ctr):
	print ctr
	ctr = hex(ctr)[2:]
	ctr = ctr.rjust(32,"0")
	ctr = ctr.decode("hex")
	
	AES = aes.AES()
	AES.init(key)
	
	w = AES.encrypt(ctr)
	w = xor(input, w)
	return w	

def split_string(string, split_string):
    return [string[i:i+split_string] for i in range(0, len(string), split_string)]

def aes_decode(key,input,ctr):
	ctr = ctr.encode("hex")
	ctr = int(ctr,16)
	input = split_string(input, 16)
	decrypted = ""
	for i in input:
		decrypted += aes_decode_(key, i, ctr)
		ctr += 1
	return decrypted

def try_key(key):
	r = remote("46.101.180.78", 13031)
	r.sendline("HELLO")

	data = recvall(r)
	print data
	random1 = data.split("\n")[0]
	random2 = data.split("\n")[1]
	print "-----------"
	print random1
	print random2

	random1 = random1.replace(" ","").decode("hex")
	random2 = random2.replace(" ","").decode("hex")

	key = key.decode("hex")

	inp = aes_decode(key, random1, random2)
	print inp.encode("hex") 

	r.send("1"+inp+"\n")

	print "encrypted flag:"

	encrypted_flag = r.recv()
	print len(encrypted_flag)
	print encrypted_flag
	print "###"
	decrypted = aes_decode(key,encrypted_flag,random2)
	print decrypted
	if "DCTF" in decrypted:
		exit()
	r.close()

for brut_byte in range(0x00,0x100):
	gg = hex(brut_byte)[2:]
	gg=gg.rjust(2,"0")
	print gg
	try_key(gg+"0a0a0d0f00000d0b0a0a0d0f00000d")

and the flag is:

DCTF{a1fee34f2a3e6e010d786f02865dc39896faa6b589d1f57f565bac9bd1d85cae}

Summing up, the task was very easy if you reversed the binary properly. The task was categoried wrongly and this could mislead people, maybe this is why this task hadn’t many solves.


Similar Posts

Comments