You can download this challenge here.

Our target is a x64 ELF binary that archives files somehow, with some kind of encryption. Here is our target's usage:

Usage: ./archiv <command> <archiv> <password> <file>
commands:
-l <archiv> <password> - lists all files in the archiv.
-a <archiv> <password> <file> - adds a file to the archiv (when archiv does not exist create a new archiv).
-x <archiv> <password> <filename> - extracts the given filename from the archiv.
-d <archiv> <password> <filename> - delete the given filename from the archiv.

We don't know how files are encrypted and what is the password for the provided archive. In the first part we will only find what the password is, then we will go deeper into what this binary does in part 2.

Part1 - RE400: Bruteforce with LD_PRELOAD

These funny humans try to exclude us from the delicious beer of the Oktoberfest! They made up a passcode for everyone who wants to enter the Festzelt. Sadly, our human informant friend could not learn the passcode for us. But he heard a conversation between two drunken humans, that they were using the same passcode for this intercepted archive file. They claimed that the format is is absolutely secure and solves any kind of security issue. It's written by this funny hacker group named FluxFingers. Real jerks if you ask me. Anyway, it seems that the capability of drunken humans to remember things is limited. So they just used a 6 character passcode with only numbers and upper-case letters. So crack this passcode and get our ticket to their delicious german beer!

Here is the challenge: https://ctf.fluxfingers.net/static/downloads/fluxarchiv/hacklu2013_archiv_challenge1.tar.gz

Let's see what's going on with IDA, we quickly see this code:

rev400.png

The checkHashOfPassword function is only storing the SHA-1 in the static variable hash_of_password, and we can see from the code above that encryptDecryptData and verifyArchiv both only have one argument: the archive FILE pointer.

Ok, that's enough for me. I get lazy (more than usual :o), and decide to bruteforce from within the archiv process itself. That way we don't have to reverse the remaining functions (though, that was not complicated...), and the bruteforce will be efficient enough! To do that, I only load a shared library with LD_PRELOAD, "hook" the strcmp function and launch my bruteforce there.

#include <stdio.h>
 
int strcmp(const char *s1, const char *s2)
{
	const char *charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
	char password[7] = {0};
	size_t i0, i1, i2, i3, i4, i5 = 0;
 
	void (*checkHashOfPassword)(char *) = (void*) 0x0000000000402A01;
	int (*encryptDecryptData)(FILE *) = (void*) 0x0000000000401B9A;
	int (*verifyArchiv)(FILE *) = (void*) 0x0000000000402A4E;
 
	FILE *f =  fopen("FluxArchiv.arc", "r");
 
	for (i0 = 0, password[0] = charset[i0]; password[0]; password[0] = charset[++i0]) {
		printf("passwd[0] = %c\n", password[0]);
		for (i1 = 0, password[1] = charset[0]; password[1]; password[1] = charset[++i1]) {
			printf("  passwd[1] = %c\n", password[1]);
			for (i2 = 0, password[2] = charset[0]; password[2]; password[2] = charset[++i2])
				for (i3 = 0, password[3] = charset[0]; password[3]; password[3] = charset[++i3])
					for (i4 = 0, password[4] = charset[0]; password[4]; password[4] = charset[++i4])
						for (i5 = 0, password[5] = charset[0]; password[5]; password[5] = charset[++i5]) {
 
							checkHashOfPassword(password);
							//encryptDecryptData(f); // Useless actually, only used to check the MAGIC_VALUE
 
							if (verifyArchiv(f)) {
								printf("Password found: %s\n", password);
								return 0;
							}
						}
		}
	}
 
	fclose(f);
 
	return 0;
}

Now let's compile, execute and wait for the password. It takes less than 10 minutes.

awe@awe-laptop ~/hacklu/reverse/FluxArchiv1 % gcc -O3 -shared -fPIC -o hooker.so hooker.c -ldl
awe@awe-laptop ~/hacklu/reverse/FluxArchiv1 % time LD_PRELOAD=./hooker.so ./archiv -l FluxArchiv.arc BADGER
################################################################################
FluxArchiv - solved security since 2007!
Written by sqall - leading expert in social-kernel-web-reverse-engineering.
################################################################################
 
passwd[0] = A
  passwd[1] = A
  passwd[1] = B
  passwd[1] = C
* snip *
  passwd[1] = U
  passwd[1] = V
  passwd[1] = W
Password found: PWF41L
Given password is not correct.
zsh: exit 1     LD_PRELOAD=./hooker.so ./archiv -l FluxArchiv.arc BADGER
LD_PRELOAD=./hooker.so ./archiv -l FluxArchiv.arc BADGER  537.34s user 49.71s system 99% cpu 9:47.70 total

So now we can extract the archive files, etc. However, PWF41L was actually the flag.

Part2 - RE500: Extraction of deleted entries

These sneaky humans! They do not just use one passcode, but two to enter the Festzelt. We heard that the passcode is hidden inside the archive file. It seems that the FluxFingers overrated their programming skill and had a major logical flaw in the archive file structure. Some of the drunken Oktoberfest humans found it and abused this flaw in order to transfer hidden messages. Find this passcode so we can finally drink their beer!

(only solvable when FluxArchiv (Part 1) was solved)

Here is the challenge: https://ctf.fluxfingers.net/static/downloads/fluxarchiv/hacklu2013_archiv_challenge1.tar.gz

From the description, we quickly guess that there are issues with the archive deletion mechanism. First let's analyse the deleteArchive function. And well hmm, it doesn't behave like intended. That's because the author messed up the functions name. That can be quickly fixed by following the CFG backwards from the printed messages. We then have these functions / addresses:

Function name Segment Start
doRC4 .text 0000000000400E0C
createHashOfPassword .text 0000000000400E96
createFileEntry .text 0000000000400F05
real_addArchive .text 0000000000401043
findNextFreeChunk .text 0000000000401406
encryptDecryptData .text 0000000000401B9A
writeFileToArchiv .text 0000000000401CA7
real_searchArchive .text 0000000000401FED
real_deleteArchive .text 000000000040227D
real_extractArchive .text 00000000004025C8
checkHashOfPassword .text 0000000000402A01
verifyArchiv .text 0000000000402A4E
main .text 0000000000402B65

Let's analyse the real_deleteArchive function. It takes 2 parameters: the archive stream pointer and the offset for the entry we want to delete (which is returned from a previous call to real_searchArchive). Here is what the function does, from its CFG:

rev500_CFG.png

# PURPLE: go to the initial offset, read next_chunk (which will be used to calculate the offset for the next chunk)
f.seek(init_offset)
next_chunk = unpack("<Q", doRC4(f.read(8)))
 
# YELLOW: read max_chunks (total number of chunks)
max_chunks = unpack("<Q", doRC4(f.read(8)))
 
# DARK GREEN: overwrite next_chunk in the file with junk
next_offset = next_chunk << 4
next_offset = next_offset + (next_offset << 6) + 0x20
fseek(next_offset)
f.write(doRC4("\x00" * 8))
f.write(doRC4("\x00" * 8))
 
# RED: overwrite the MD5sum with junk
f.write(doRC4("\x00" * 16))
 
# BLUE: overwrite the file name with junk
f.write(doRC4("\x00" * 0x60))
 
# LIGHT GREEN: 
for chunk_index in range(max_chunks):
    # GREY: go to the next chunk offset, read next_chunk
    f.seek(next_offset)
    next_chunk = unpack("<Q", doRC4(f.read(8)))
 
    # ORANGE: overwrite the next_chunk in the file with junk
    f.seek(next_offset) ; f.write(doRC4("\x00" * 8))

So, they delete all metadatas associated to the file, but not the file content itself. We should be able to recover a file then! We miss some informations though:

  1. What is the size of each chunk?
  2. How many chunks do we have to read? That's overwritten, so we don't know.
  3. How are the next_chunk allocated? Is that sequential, random, ...? That's overwritten too.

We can't know the file name and we won't be able to verify the MD5 sum, but we don't care of these details. From the real_addArchive function we can get all we needed to know. It does the opposite, and shows us that for each chunk of the original file, 0x408 bytes are written, then the next_chunk value is simply incremented.

We can't know what the size of the file is, so we will assume max_chunks = 0xFF. Because can't know the initial value of next_chunk either, we will have to bruteforce, from 0 to 0xFF. We will create one file per initial next_chunk value. Because we choose a max_chunks which is overlong, we will have some garbage at the end of each file, but most file formats don't really care about that anyway.

#!/usr/bin/env python2
 
import Crypto.Cipher.ARC4 as RC4
import hashlib
 
def decryptRC4(s):
    return RC4.ARC4Cipher(hashlib.sha1("PWF41L").digest()).decrypt(s)
 
f = open("FluxArchiv2.arc", "rb")
 
for bf in xrange(0xFF):
    print '[*] Trying with offset %02x' % bf
    o = open("output/out_%02x" % bf, "w+")
 
    try:
        next_chunk = bf
        max_chunks = 0xFF
 
        for chunk_index in xrange(max_chunks):
            next_offset = next_chunk << 4
            next_offset = next_offset + (next_offset << 6) + 0x20
 
            f.seek(next_offset)
            f.read(8) # Skip overwritten next_chunk
 
            content = decryptRC4(f.read(0x408))
            o.write(content)
 
            next_chunk += 1
    except:
        pass
    finally:
        o.close()
 
f.close()

Now we go to output/, do a file *, find the documents we extracted previously from the part 1, but no new image file with the flag as I expected :( So, it is probably simply a text file...

head -n1 *|less
# Output: *snip*
# ==> out_9d <==
# <B7>9<AF>       <B0><FB><91>0<D8>fB^Sȑ
# 
# ==> out_9e <==
# Another one got caught today, it's all over the papers.  "Teenager
# 
# ==> out_9f <==
# els threatened by me...
# 
# ==> out_a0 <==
# e electron and the switch, the
# *snip*
less out_9e
# Output:
# Another one got caught today, it's all over the papers.  "Teenager
# Arrested in Computer Crime Scandal", "Hacker Arrested after Bank Tampering"...
# Damn kids.  They're all alike.
# *snip*
# 
# +++The Mentor+++
# 
# Flag: D3letinG-1nd3x_F4iL
# * snip*

The flag is D3letinG-1nd3x_F4iL.