Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
https://ctftime.org/event/1066
I did not solve the vast majority of challenges I am writing up here, but instead I am doing it to consolidate my own understanding of it. I also hope the writeups you find here help you understand the challenge, and if they did then it's fulfilled it's purpose and whether or not I originally completed them is irrelevant :)
If we disassemble, the solution is pretty clear.
gets()
is used to take in input, then the contents of another local variable are compared to 0xdeadbeef
. Basic buffer overflow then overwrite a local variable:
X-MAS{ah_yes__i_d0_rememb3r_you}
We receive a file called chall
. NX is disabled, which is helpful. We inject shellcode, use a jmp rsp
gadget and execute our own shellcode.
main()
is a fairly simple binary:
The buffer is 48
bytes long. After the buffer there is 16-bit integer check
, which acts as a canary. Then there are 8 bytes for the stored RBP. The total input it 71
, meaning after the stored RBP we have 13 bytes of overflow, including the RIP. No ROP is possible.
Note that the value -6913
is actually 0xe4ff
.
This was rather misleading as they gave you the LIBC.
Firstly:
Now we need some shellcode. pwntools' shellcraft.sh()
is 2
bytes too long, so we'll have to make it manually.
The general payload is as follows:
/bin/sh\x00
so we have it in a known location (relative to RSP)
Shellcode
Padding
0xe4ff
to overwrite the pseudo-canary
Padding
jmp rsp
Now we need to decide what shellcode we want to run. Well, since RSP points at the stack, we know that it will always be a static offset off our buffer. If we calculate it, we can just do
And execute the other half of our code! And at this point RSP will be exactly 8
bytes off /bin/sh\x00
, so we can use it to populate RDI as well!
X-MAS{sant4_w1ll_f0rg1ve_y0u_th1s_y3ar}
Messing with the XOR
Let's try running the file:
Perhaps it wants a flag.txt file? Let's create one with the words FwordCTF{flag_flaggety_flag}
:
This isn't quite counting the number of letters we enter. Let's see if the disassembly can shed any light on it.
First thing we notice is that every libc
function is built into the binary due to the stripped names. We can confirm this with rabin2
:
Many of the functions can be handled using the return address and the general context. Some of the decompilation - especially the references to strings - may not have loaded in yet; make sure GHidra finishes analysing. We don't even need the exact C names, as long as we get the general gist it's all fine.
The python equivalent of this is roughly
In short, it's an XOR function.
To calculate the length of the string it uses
The key here is strlen
stops at a null byte. If you input a character with the same value as the flag character in that position, it will XOR to become \x00
.
We can test every possible character. If the returned value is one less than the length of the string, the last character is correct as it XORed to create a null byte.
To test different offsets we can pad using a value definitely not in the flag, such as #
.
Now we can just switch out the process type on the remote server.
Flag: NuL1_Byt35?15_IT_the_END?Why_i_c4nT_h4ndl3_That!
Not really my forte, but here we go, I can only get better.
I didn't manage to solve a huge number of these - I was quite busy, plus I suck - but I'll dump some writeups here for those I caught up on later.
A ret2libc with a given leak
Running the binary prints and hex value and prompts for input:
We can definitely cause it to segfault:
So let's work out what this value is and how we can use it.
We chuck the binary into GHidra and get a simple disassembly. main
calls vuln
and does almost nothing else. vuln
, however, has some interesting stuff:
It prints the address of system
! Awesome.
Let's run the binary on the remote serevr to leak the libc version.
So now we essentially have a libc leak, we head over to find the libc version.
Annoyingly, there are 4 possible libc versions, and we can only get it from trial and error. Aside from the libc version itself, the exploit is quite simple - subtract the offset of system
from the leaked address to get libc
base, then use that to get the location of /bin/sh
.
The correct libc version is 2.30-0ubuntu2.1_i386
.
Cube Root Attack
In this challenge, we get a message.eml
file containing an email:
Applications such as Outlook block downloading the file due to it's "malicious nature", but we can open the .eml
file in VS Code easily and extract two things:
Firstly, there is a secret.enc
file with base64-encoded ciphertext:
Secondly, there is a pubkey.der
file containing an RSA public key:
We'll use the gmpy2
iroot()
function to calculate the cube root:
And bingo bango, we get the flag as HTB{w34k_3xp0n3n7_ffc896}
.
Once we visit the URL, we are shown some code:
Clearly this is some type of exploit, but I'm not that familiar with it except for 0e
md5 hashes and stuff. However, there are some restrictions here:
There can be no e
character in either parameter
The two parameters must be the same length
They can't strictly equal each other (!==
) but they must loosely equal each other (==
)
We get given challenge.py
and encrypted.bin
. Analysing challenge.py
:
And appends the result of that as the encrypted character in encrypted.bin
.
Gives us
So we can form two equations here using this information:
We subtract (2) from (1) to get that
And the resulting PDF has the flag HTB{4ff1n3_c1ph3r_15_51mpl3_m47h5}
within.
Common Mod, DIfferent e
In this challenge, we are given two sets of , and .
The plaintext encrypted to give is the same, and we can observe that the choice of is also the same, meaning the only difference is in the choice of . Here we can use some cool maffs with and to retrieve the original plaintext .
Firstly, if the greatest common divisor of and is , then there exists and such that
To calculate this, we can use the . But why is this helpful?
Well if we know that and and we know such that , we can then use this to calculate like this:
In practise is likely to be negative, and in modular arithmetic we use negative powers using the . Luckily, Sage can do this for us by default, so we can do even less steps:
And we get the flag as HTB{c0mm0n_m0d_4774ck_15_4n07h3r_cl4ss1c}
.
We can easily import the public key in Python and read the values for and using the Pycryptodome:
We can throw into FactorDB to see if the factors are known, but they are not. The more notable observation is that , which allows us to perform a on the ciphertext.
The logic here is simple: because the message is quite short and the public modulus is quite large, a small value of such as may make it such that . This makes the modulus ineffective as and we can simply take the th root of the ciphertext to recover the plaintext.
PHP comparision is a known piece of junk, so we can find some weaknesses using .
Once set of possible parameters is 01
and 1
, as they are both two characters long and - according to PHP's loose comparison - equal each other (thanks to for this solution after the CTF). It appears that objetcs are automatically converted to numbers for loose comparisions, as loose only compares values while strict also compares types. Therefore the example above would both equal 1
under loose comparison.
Another, more interesting set is 200
and 2E3
(thanks to ). Note that 2E3
is an exponential, equivalent to 2 * 10^2
. Once both are converted to integers, they pass the check.
It calculates two random values, and . For every byte in the plaintext file, it then calculates
The plaintext file appears to be letter.pdf
, and using this we can work out the values of and because we know the first 4 bytes of every PDF file are %PDF
. We can extract the first two bytes of encrypted.bin
and compare to the expected two bytes:
And we can multiply both sides by the modular multiplicative inverse of 43, i.e. , which is , to get that
And then we can calculate :
So now we have the values for and , it's simply a matter of going byte-by-byte and reversing it. I created a simple Sage script to do this with me, and it took a bit of time to run but eventually got the flag.
Meet-in-the-middle attack on AES
We are given challenge.py
, which does the following:
Creates two keys
Key1 is cyb3rXm45!@#
+ 4 random bytes from 0123456789abcdef
Key2 is 4 random bytes from 0123456789abcdef
+ cyb3rXm45!@#
Encrypts the flag with Key1 using AES-ECB
Encrypts the encrypted flag with Key2 using AES-ECB
We can use a meet-in-the-middle attack to retreive both keys. The logic here is simple. Firstly, there are 16
possible characters for each of the 4 random bytes, which is easily bruteforceable ().
We can also encrypt a given input and get the result - I choose to send 12345678
as the hex-encoded plaintext and receive . For these keys, the encrypted flag is given as:
Now we have a known plaintext and ciphertext, we can use both one after the other and bruteforce possible keys. Note that the encryption looks like this:
We do not know what the intermediate value x
is, but we can use brute force to calculate it by
Looping through all possibilities for key1
and saving the encrypted version of 12345678
Looping through all possibilities for key2
and saving the decryption of 449e2eb...
Finding the intersection between the encryption with key1
and the decryption with key2
Once we find this intersection, we can use that to work back and calculate key1
and key2
, which we can then utilise to decrypt the flag.
And we get the flag as HTB{m337_m3_1n_7h3_m1ddl3_0f_3ncryp710n}
!