ALLES! CTF 2020
ALLES! CTF 2020 with Kurisutina - 93rd
Really fun ctf. I ended up spend my time on crypto challs as my teammates already done all the web :(
[crypto] Actual ASLR 1
I looked at this challenge first because it was from LiveOverflow. The idea of this challenge is reimplementing ASLR using the rand
function but in the first part it only requires us to do some guess stuff, nothing to do with the ASLR :v
The challenge give us a binary aaslr
which provide 4 options
Welcome To The Actual-ASLR (AASLR) Demo
1. throw dice
2. create entry
3. read entry
4. take a guess
Select menu Item:
1
[>] Threw dice: 4
Welcome To The Actual-ASLR (AASLR) Demo
1. throw dice
2. create entry
3. read entry
4. take a guess
Select menu Item:
2
Enter your data (max length: 100):
aaaa
[>] Created new entry at index 0
Welcome To The Actual-ASLR (AASLR) Demo
1. throw dice
2. create entry
3. read entry
4. take a guess
Select menu Item:
3
Enter entry index (max: 0):
1
[>] 1. (null)
Welcome To The Actual-ASLR (AASLR) Demo
1. throw dice
2. create entry
3. read entry
4. take a guess
Select menu Item:
4
(1/16) guess next dice roll:
1
(2/16) guess next dice roll:
2
(3/16) guess next dice roll:
3
(4/16) guess next dice roll:
4
(5/16) guess next dice roll:
5
(6/16) guess next dice roll:
6
(7/16) guess next dice roll:
1
(8/16) guess next dice roll:
2
(9/16) guess next dice roll:
3
(10/16) guess next dice roll:
4
(11/16) guess next dice roll:
5
(12/16) guess next dice roll:
6
(13/16) guess next dice roll:
7
(14/16) guess next dice roll:
1
(15/16) guess next dice roll:
2
[!] WRONG!
Open the binary in IDA, I found a take_guess
function which give us the flag.
We have to correctly guess 16 times, each time it calls throw_dice
to get a number. The throw_dice
function calls ranval
and force the number to be from 1 to 6.
ranval
does some magic bit-shifting with a global variable called RANCTX
and return an integer value.
RANCTX
is initialized in raninit
function and then ranval
is called 0x14 times.
ran_init
is called once in init_heap
. This is where the ASLR implemented but as I said above we don’t need it for the first part. init_heap
initialize the random seed, which is RANCTX[3]
, with time(0)
.
At this point the idea is simple. We have to sync our time with the server so that the rand()
calls later we will return the same value as the server. This is what I came up with in C.
#include <stdio.h>
#include <time.h>
unsigned long RANCTX[4] = {};
unsigned long ranval()
{
long v0; // ST08_8
v0 = RANCTX[0] - ((RANCTX[1] << 27) | (RANCTX[1] >> 5));
RANCTX[0] = ((RANCTX[2] << 17) | (RANCTX[2] >> 15)) ^ RANCTX[1];
RANCTX[1] = RANCTX[3] + RANCTX[2];
RANCTX[2] = v0 + RANCTX[3];
RANCTX[3] = v0 + RANCTX[0];
return RANCTX[3];
}
void raninit(long time_seed)
{
RANCTX[0] = 0xF1EA5EEDLL;
RANCTX[3] = time_seed;
RANCTX[2] = RANCTX[3];
RANCTX[1] = RANCTX[2];
int i = 0;
for (i = 0LL; i <= 0x13; ++i)
ranval();
}
void init_heap()
{
long v0 = time(0) + 1; // add 1 to sync time with challenge server
raninit(v0);
}
unsigned long throw_dice()
{
unsigned long random_val;
random_val = ranval();
return (unsigned long)((int)random_val + (int)(random_val / 6) * -6 + 1);
}
int main(int argc, char const *argv[])
{
init_heap();
ranval();
ranval();
int i = 0;
for (i = 0; i < 16; i++)
{
printf("%lun", throw_dice());
}
return 0;
}
Some people might want to use Python ctypes to do this but I was lazy to deal with the type difference between the two languages :/ Implementing in C is just copy pasting from IDA.
Then just connect to the server to get the flag. Easy!
from pwn import *
def get_rands():
l = process('./rand')
result = l.recvall().decode('ascii')
l.close()
return result.split('n')
if __name__ == '__main__':
# p = process('./aaslr')
p = process('/bin/ncat --ssl 7b000000a29cdf5d98f6e81d.challenges.broker4.allesctf.net 1337', shell=True)
rands = get_rands()
p.sendlineafter('Select menu Item:n', '4')
for i in range(15):
print("sending: " + rands[i])
p.sendlineafter('roll:', rands[i])
p.interactive()
Flag: ALLES{ILLEGAL_CARD_COUNTING!_BANNED}
[crypto] Doors of Durin
This challenge must be easier if I was a crypto man. In the end I learned a lot about hash collision :D
We are given 2 files, door.c
and gatekeeper.py
.
door.c
takes a string up to 254 chars andstrcmp
it with"sp3akfr1end4nd3nt3r"
, if the two strings are equal, we get the flag.
#define MAGIC_WORD "sp3akfr1end4nd3nt3r"
int main() {
char input[255];
char flag[255];
scanf("%254s", input);
printf("You said: %sn", input);
if (strcmp(input, MAGIC_WORD) == 0)
{
int fd = open("./flag", O_RDONLY);
if (fd == -1)
{
printf("Something went wrong! Thats a bug, please report!n");
return 1;
}
read(fd, flag, 254);
printf("Flag: %sn", flag);
}
else
{
printf("Nope :/n");
}
return 0;
}
gatekeeper.py
get user input as a string, if its hash ingoodHashes
, pass through. Else, if the hash contains the secret, exit. Otherwise, add the hash intogoodHashes
and pass through.
while True:
try:
currentInput = input("Give me your input:")
bytesInput = base64.b64decode(currentInput)
print("Doing magic, stand by")
hashed = generateSecretHash(bytesInput)
if hashed in goodHashes:
print(passToGate(bytesInput))
else:
if b"sp3akfr1end4nd3nt3r" in bytesInput:
print(
"Everybody knows the magic words. I can't hear it anymore! Go away! *smash*")
exit(0)
else:
goodHashes[hashed] = bytesInput
print(passToGate(bytesInput))
except Exception as e:
print(e)
User input get hashed like this
def generateSecretHash(byteInput):
md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha256 = hashlib.sha384()
blake2b = hashlib.blake2b()
md5.update(byteInput)
sha1.update(md5.digest())
md5.update(sha1.digest())
for i in range(0, 2938):
sha256.update(md5.digest())
for k in range(-8222, 1827, 2):
sha1.update(sha256.digest())
sha256.update(sha1.digest())
for j in range(20, 384):
blake2b.update(sha256.digest())
return blake2b.hexdigest()
This seems so freaking secure :v How can one challenge hash so many times :(
The problems I realized so far:
- Storing input hashes as keys in a dictionary may cause a replacement on same hash.
- It check for occurence of the secret after finding the hash in
goodHashes
. If we can somehow get our secret’s hash intogoodHashes
then next time we can just paste in the secret. generateSecretHash
calculate MD5 first, which voids the meaning of later hashing. Same MD5 results in same hash after all.- Hash collision can occur in MD5. I know it exist but haven’t tried yet.
Everything leads to hash collision :D Let’s try it! Our target is to find a string that have the same hash as our secret "sp3akfr1end4nd3nt3r"
.
After some quick googling, I found this repository. It indicates that what we are looking for is called MD5 Preimage Attack which is not possible at the moment. But there is still something interesting to try, notice the 2 later options.
In case you don’t know, the author of the repository keep mentioning hash of “files”, mean the same as “strings” because calculating MD5 checksum of a file is actually calculating MD5 hash of its content.
So take a look at the options we have:
- get two different files with the same MD5: instant.
This probably not what we want because we are looking to have some control over the file content so we can put the secret in. - make two arbitrary files get the same MD5: a few hours.
Arbitrary indicates that we have some control over the file content. <– Let’s try this.
According to the repo above, one technique used hashclash to calculate the collision. Basically we have to provide 2 files with the prefixes we need and just let it run. This took hours to complete and the CTF ended while it was running :'( I even got a bsod running it haha
In the morning I checked out the writeups. Turns out, people use another technique called “Unicoll” which is much faster. This technique is included in the hashclash repository and use another script. The reason why I didn’t choose it was because it’s labeled “identical-prefix collision” but I need my two strings to be different to bypass the check.
I didn’t read carefully enough. The prefixes are not identical. There are differences, in the 10th byte is +1, 73rd byte is -1, the other are similar. Anyway, I eventually managed to solve it. Unicoll tooks about 36 minutes to run and produced 2 files with the same hash.
Also notice the null byte in the end of the secret. That makes strcmp
ignore the gibberish after.
Flag: ALLES{st0p_us1ng_md5_alr34dy_BabyRage_1836d}