Vashisht CTF 2024
Writeups for Vashisht CTF 2024 #
I would say that I am not a crypto
guy.
Let’s start from the easy ones:
Web: Hidden #
The flag is in the headers.
% curl -I http://vashishtctf.eastus2.cloudapp.azure.com:15004/
HTTP/1.1 200 OK
X-Powered-By: Express
flag: SURGE{b329b2d7373612098b9a64efad366663}
Content-Type: text/html; charset=utf-8
Content-Length: 1849
ETag: W/"739-sbnia6K080KT6KK7/b4oFYdHQyw"
Date: Mon, 01 Apr 2024 18:50:52 GMT
Connection: keep-alive
Cryptography: keys #
Let’s start by inspecting the attachment.
% file .\key.pub
.\key.pub: OpenPGP Public Key Version 4, Created Wed Mar 13 13:05:32 2024, RSA (Encrypt or Sign, 3072 bits); User ID; Signature; OpenPGP Certificate
So, the given file is a public key. To extract more info from a public key, we use the tool gpg
.
To display all the information associated with a PGP file, we can use gpg --list-packets
.
`gpg --list-packets`
% gpg --list-packets key.pub
# off=0 ctb=99 tag=6 hlen=3 plen=397
:public key packet:
version 4, algo 1, created 1710335132, expires 0
pkey[0]: [3072 bits]
pkey[1]: [17 bits]
keyid: ACA70402C6AE8F21
# off=400 ctb=b4 tag=13 hlen=2 plen=70
:user ID packet: "SVTDC\x7b01d53328111gc224ad5f27058c0hc6dc\x7d (keep it safe) <no@thanks.com>"
# off=472 ctb=89 tag=2 hlen=3 plen=462
:signature packet: algo 1, keyid ACA70402C6AE8F21
version 4, created 1710335132, md5len 0, sigclass 0x13
digest algo 10, begin of digest 99 fb
hashed subpkt 33 len 21 (issuer fpr v4 8BC77664BD0BE5251D381F90ACA70402C6AE8F21)
hashed subpkt 2 len 4 (sig created 2024-03-13)
hashed subpkt 27 len 1 (key flags: 03)
hashed subpkt 11 len 4 (pref-sym-algos: 9 8 7 2)
hashed subpkt 21 len 5 (pref-hash-algos: 10 9 8 11 2)
hashed subpkt 22 len 3 (pref-zip-algos: 2 3 1)
hashed subpkt 30 len 1 (features: 01)
hashed subpkt 23 len 1 (keyserver preferences: 80)
subpkt 16 len 8 (issuer key ID ACA70402C6AE8F21)
data: [3072 bits]
# off=937 ctb=b9 tag=14 hlen=3 plen=397
:public sub key packet:
version 4, algo 1, created 1710335132, expires 0
pkey[0]: [3072 bits]
pkey[1]: [17 bits]
keyid: 501B86BE2569D7E2
# off=1337 ctb=89 tag=2 hlen=3 plen=438
:signature packet: algo 1, keyid ACA70402C6AE8F21
version 4, created 1710335132, md5len 0, sigclass 0x18
digest algo 10, begin of digest 07 7e
hashed subpkt 33 len 21 (issuer fpr v4 8BC77664BD0BE5251D381F90ACA70402C6AE8F21)
hashed subpkt 2 len 4 (sig created 2024-03-13)
hashed subpkt 27 len 1 (key flags: 0C)
subpkt 16 len 8 (issuer key ID ACA70402C6AE8F21)
data: [3071 bits]
If we observe the user
packet:
:user ID packet: "SVTDC\x7b01d53328111gc224ad5f27058c0hc6dc\x7d (keep it safe) <no@thanks.com>"
where SVTDC{01d53328111gc224ad5f27058c0hc6dc}
corresponds to the name of the person owning the key. Note that the hex chars \x7b
and \x7d
corresspond to {
and }
. As the flag format is SURGE{...}
, we can use the vignere cipher to decode it.
And SURGE{01d53328111fa224df5f27058b0ff6fc}
is the flag.
Crypto: Bad encryptor #
It wasn’t really an encryptor.
from Crypto.Util.number import isPrime
flag = open("./flag.txt").read()
primes = [] # Contains len(flag) prime numbers
last = 1
while len(primes) < len(flag): # Get primes into list: primes
if isPrime(last):
primes.append(last)
last += 1
N = 1 # Final Message
lp = 0 # Last Used prime
for char in flag:
N *= pow(primes[lp], ord(char)) # multiplies N with the power of the last used prime and ASCII value of character
lp += 1
print(N)
By prime factor decomposition, we can get:
By converting the exponents into ASCII and joining them, we get:
SURGE{9258a16b7c556b}
Crypto: Refresh your basics! #
Simple RSA with one weak prime with 64 bits. Therefore, n
can be factorized.
from Crypto.Util.number import *
flag = b'<redacted>'
flag = bytes_to_long(flag)
p = getPrime(64)
q = getPrime(256)
n = p * q
assert n > flag
e = 65537
ct = pow(flag, e, n)
print("n:", n)
print("ct:", ct)
The decrypt script would look like:
from Crypto.Util.number import *
# factorized n
n = 806704453925916376241138342513764759694016480895315246055011522287508180289505799369947191610601
ct = 762455931346013832056509175748789782334396365067918709406946600251430637403555697175178736630332
p = 10391365104330587849
q = 77632192289126993297792365174469782083249762592589102440391630336358015094049
e = 65537
assert p * q == n
phi = (p-1) * (q-1)
d = pow(e, -1, phi)
m = pow(ct, d, n)
print(long_to_bytes(m)) # SURGE{1e222ae77cde0b8e69bf5ce303680a69}
Crypto: Very suspicious #
Again, a simple RSA but I alternate the bits of the primes and leak data.
from Crypto.Util.number import getPrime, bytes_to_long
def SUS(num1, num2):
bin_num1 = bin(num1)[2:]
bin_num2 = bin(num2)[2:]
max_len = max(len(bin_num1), len(bin_num2))
bin_num1 = bin_num1.zfill(max_len)
bin_num2 = bin_num2.zfill(max_len)
result = ''
for bit1, bit2 in zip(bin_num1, bin_num2):
result += bit1 + bit2
return int(result, 2)
p = getPrime(256)
q = getPrime(256)
n = p * q
e = 65535
m_ = b'<redacted>'
m = bytes_to_long(m_)
assert n > m
ct = pow(m, e, n)
print(ct)
print(SUS(p, q))
The decryption script would look like:
from Crypto.Util.number import *
# from output.txt and **not** from script -- apologies
sus = 12172853354996487738262727969778023965364605972349804309429387389453296963162006798117626100923740148151187023397158587810227904623962151254491594772921555
ct = 4987966219043334529713424728358109322053443045107415872476484761290274854498710937425004695325905521798917127819180022265420759803068692357967429356255683
def recover_nums(result):
bin_result = bin(result)[2:]
bin_num1 = bin_result[::2]
bin_num2 = bin_result[1::2]
num1 = int(bin_num1, 2)
num2 = int(bin_num2, 2)
return num1, num2
p, q = recover_nums(sus)
n = p * q
e = 65537
phi = (p-1) * (q-1)
d = pow(e, -1, phi)
m = pow(ct, d, n)
print(long_to_bytes(m))
Misc: Safest place to store flags #
Inspecting the attachment, we find that it is a .docx
file. But .docx
files are actually zip files with XML in it.
Unzipping the zip
file, we have:
There we have the flag.
Misc: PNG Havoc #
Using the first hint, we are asked to read the description, and we can find the word XOR.
As it is XOR, we need plain text and a key.
Using the second hint, we can use an online image repairing tool to find out that:
Looking at the image and the description, we can guess that the text is in tenctonese alphabet.
By translating the given text, we would obtain:
F3PJJF5HVEYT3PAJLUHHKCMJSAPL5IHG2PSWEZHPBYEV64IC3WJU3P5F5CD6ULY=
which is encoded in base32
.
Now, decoding this would give us:
But remember the first hint which said about XOR
. We should find the key.
The key is none other than the value of the corrupted chunk CRCs. For solving image challenges, I prefer to use stegsolve.
Joining all the corrupted CRCs of the chunks, you have the key (16 bytes).
By XORing them, we have:
Which is a nice, but less secure steganography technique.
Misc: Scared of Heights #
The website presents us with a world rendered using the babylon engine, if someone inspected the network tab, they would have seen:
Apart from the usual javascript, html and css files, we have a heightmap.jpg
.
Analyzing heightmap.jpg
using exiftool
gives:
As the flag format doesn’t match, we can try the vigenere cipher:
Reverse Engineering: Easy it is! #
Flag is on the string table
Reverse Engineering: Missing encryption #
Using IDA, first, let us look at the following lines:
We can observe that:
- key length is 1
- sleeps for 5 seconds before testing the key
- gets a couple of pages from
mmap
, base address ataddr
(notice the flags, it also hasPROT_EXEC
) - copies the key into
addr
- calls a function
addr
-> unhandled errors could occur, but the program should proceed - unmaps the pages using
munmap
Therefore, we should define a function which only takes 1
byte, and should not raise errors, and that would be ret
.
Opcode of ret
corresponds to 0xc3
, which would be the correct input.
Which looks like AES, let’s check with cyberchef.
Reverse Engineering: Catch me if you can! #
Description explicitly mentions to install requests
and the file is a pycompiled
binary.
Compiled as mentioned in: my blog page, that I wrote a year ago.
So, we cannot look at the code in the binary. We can place a hook in the requests
library, to see what data is going in, or we can use the debugger to pause at a specific syscall
. But, the easiest approach is to create a python file, mask PYTHONPATH
environment variable to disable searching for external modules from site-packages
and print all the arguments. In our case, that file would be named as requests.py
to mask the requests
module.
# mask requests.post(...)
def post(*args, **kwargs):
print(args, kwargs)
# mask requests.get(...)
def get(*args, **kwargs):
print(args, kwargs)
Now running with the masked PYTHONPATH
, we have:
But due to my mistake, the RC4 encoder got messed up, but this is a correct way among a 10000 ways to solve these kinds of problems.
You could think of:
- Man in the middle (with your certificates)
- Requests library hooking
- Binary patching - Patching URL in string table
- Debugging
- Instrumenting calls to
openssl
(as it is https and it has to be decrypted at some point) - Patching
https
tohttp
and inspecting network packets (wireshark)
…
Reverse engineering: San Andreas #
A large part of the given binary is decompiled in the repository: gta-reversed-modern. This was the github repository you had to find as a part of your OSINT. The file name gta-sa-compact
is a very direct clue to this repo, as all the functions in this repo would corresspond to the offsets in that binary.
Let’s look at Mod.dll
using IDA.
I create a thread while attaching to a process, as seen below:
And digging into MyFunction
, we can find:
Analyzing it, we can find:
If the ‘T’ key is pressed, it calls a function located at memory address 0x43A4A0.
If the ‘V’ key is pressed, it calls a function located at memory address 0x43A0B0 and passes the value 523 to it.
If the ‘O’ key is pressed, it increments the values stored at memory addresses 0xB7CE50 and 0xB7CE54 by 100,000.
By searching these memory addresses in the repo, we can find:
- For 0x43a4a0
which seems to call the tank cheat, which gives 1/2 vehicles: RHINO
- For 0x43A0B0
which seems to spawn a vehicle with the argument a enum
type.
We can reverse the 523
enum type into its model id, which is 523
which is my favourite bike: HPV1000
2/2 vehicles.
Flag is:
SURGE{RHINO_HPV1000} (or)
SURGE{HPV1000_RHINO}
The story with the remaining lines is left as an exercise to the reader - figure out and improve your skills!
Anyway, this is an inefficient way to code a mod for GTASA.
Reverse Engineering: I like rolls! #
By using IDA, we can figure out that, two same functions is called multiple times, let’s call them F1
and F2
.
- Disassembly of
F1
:
which seems to XOR a flag with 0xD
- Disassembly of
F2
:
which seems to compare a string with a string at qword_4042C0
- Memory at
qword_4042C0
and we don’t have any data yet, as it is in the .bss
section. Let’s use a debugger to populate that pointer.
From the start, we should know that the length of the flag is 39 bytes.
Using radare, breaking the F2
, we can get:
If we observe it carefully, rdi
gets the base address of our key string.
That is the required address: 0x012e42b0
, now inspecting the key string:
^pXf_rJxHyvjif:x5n;p4b?d8didlf>e>n?tlenm9f8q9x4o=khm8fojlhkslt<n:khr=w?xkv9spw
If you notice the disassembly of function F2, you should have seen:
v1 = *(a1 + 8);
v2 = *(a2 + 16); // twice
So, we know that one character is skipped everytime.
Now, XOR-ing the key (by skipping bytes) with 0xD
, we have the flag:
SURGE{d786925da332ac45490e5bafa17e02f4}
You might also want to check the source code of this challenge to know why the binary looks like this.
Source: i-like-rolls/main.cpp
pwn: Greeter #
Inspecting the binary, we can have:
we now know that it is a statically linked binary, using printf
, scanf
.
By passing in some input, we should deduce that it is vulnerable to format string
exploit.
The final goal would be to write something to the global variable, whose pointer is loaded on to the stack.
Let’s run a debugger to locate the position of the pointer on the stack. From disassembly, we can know that the pointer is: 0x5b1550
- Format string exploit:
Cool, we find the pointer 0x5b1550
at the 13
place. Now using %n
, we can write data to this location:
Using this payload on the server, we have: