To search

w3pwnz

w3pwnz, therefore we are

Saturday, October 26 2013 03:30

Hack.lu CTF 2013 - FluxArchive Part 1 & 2

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.

Thursday, October 18 2012 16:01

HackYou CTF - Reverse100, Reverse200, Reverse300 Writeups

Reverse 100 - Open-Source



Download file code.c.
Simply read the source...

% ./code `python2 -c 'print 0xcafe'` 25 h4cky0u
Brr wrrr grr
Get your key: c0ffee

Flag: c0ffee

Reverse 200 - LoseYou



Download file rev200.zip.

Extract the archive to obtain task2.bin and task2.exe.
I decided to study task2.bin.
The routine to reverse is sub_80483DC.
We quicky notice this:

hackyou_reverse200.png

That's very basic, eax will contain our guessed number, ecx will contain the randomly generated number.
All we need to do is to break on the "cmp eax, ecx" at 08048503 and set eax to ecx.

(gdb) b *0x08048503
Breakpoint 1 at 0x8048503
(gdb) r
Starting program: /tmp/task2.bin 
warning: Could not load shared library symbols for linux-gate.so.1.
Do you need "set solib-search-path" or "set sysroot"?
Welcome to the LoseYou lottery!
Generating random.....
Make your guess (number 0 or 1): tg
 
Breakpoint 1, 0x08048503 in ?? ()
(gdb) set $eax=$ecx
(gdb) c
Continuing.
You... you... win??? so lucky! Grab the flag:
::: oh_you_cheat3r :::
[Inferior 1 (process 27619) exited normally]
(gdb)

Flag: oh_you_cheat3r

Reverse 300 - ashtree



Download file rev300.zip.
Once again I decided to study the ELF version: task3.bin.

Step 1 - Unpack

The binary seems to be packed by a modified UPX (at least, UPX string is replaced by LOL...).
Let's trace execution:

% strace ./task3.bin 
execve("./task3.bin", ["./task3.bin"], [/* 34 vars */]) = 0
[ Process PID=5640 runs in 32 bit mode. ]
getpid()                                = 5640
gettimeofday({1350215641, 677903}, NULL) = 0
unlink("/tmp/upxCRBOGQOAFQI")           = -1 ENOENT (No such file or directory)
open("/tmp/upxCRBOGQOAFQI", O_RDWR|O_CREAT|O_EXCL, 0700) = 3
ftruncate(3, 9036)                      = 0
old_mmap(NULL, 9036, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0xf775a000
old_mmap(0xf775d000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf775d000
munmap(0xfffffffff775a000, 9036)        = 0
close(3)                                = 0
open("/tmp/upxCRBOGQOAFQI", O_RDONLY)   = 3
getpid()                                = 5640
access("/proc/5640/fd/3", R_OK|X_OK)    = 0
unlink("/tmp/upxCRBOGQOAFQI")           = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
execve("/proc/5640/fd/3", ["./task3.bin"], [/* 41 vars */]) = 0
[...]

The binary is unpacking itself into a file "/tmp/upxCRBOGQOAFQI", which is a randomly generated name. Then it is unlinked and executed by execve().

The unlink() function is guaranteed to unlink the file from the file system hierarchy but keep the file on disk until all open instances of the file are closed.

Once the execution is over, the file is deleted, so we need to find the file name, and copy the file elsewhere before it's unlinked. Let's toggle a breakpoint before execve() then.

(gdb) b *0x004024dd
Breakpoint 1 at 0x4024dd
(gdb) r
Starting program: /tmp/task3.bin 
 
Breakpoint 1, 0x004024dd in ?? ()
(gdb) x/s $ebx
0xffffd494:	"/proc/12767/fd/7"
(gdb)
% file /proc/12767/fd/7
/proc/12767/fd/7: broken symbolic link to `/tmp/upxC5RF3NOAMO5 (deleted)'

Ok! Now we can work on the unpacked binary.

Step 2 - Keygen

Our routine is sub_8048617.
Several conditions must be met:

  • - argv[1] = username
  • - argv[2] = password
  • - username != 'hackyou'
  • - len(password) == 14
  • - password[4] == '-' and password[8] == '-'
  • - sub_804838C(username, password) == True
  • - sub_804844B(username, password[5:]) == True
  • - sub_804850A(username, password[10:]) == True

Although our username must not be 'hackyou', if you take a look at sub_80485F0, the goodboy message, you'll notice it prints "Great! Now submit the license key for 'hackyou'".
sub_804838C, sub_804844B and sub_804850A all proceed in the same way, which finally compares individually 4 bytes of the given password parameter:

hackyou_rev300_ida.png

Our password character is in edx, the expected one is in eax. Lazy as i am, i didn't go much deeper :)

All we need now is to break on:

.text:0804842B                 cmp     eax, edx

... dump eax, and fix edx so that it will be equal to eax. Same for the two last routines. Let's create a basic pythonGDB script to do the work for us.

import gdb
 
passwd = ""
passwd_len = 0
 
def callback_username_condition():
    gdb.execute("set $eax=1")
 
def callback_compare_password():
    global passwd, passwd_len
 
    gdb.execute("set $edx=$eax")
 
    if passwd_len in (4, 9):
        passwd += '-'
        passwd_len += 1
 
    passwd += chr(gdb.parse_and_eval("$eax"))
    passwd_len += 1
 
    print "[+]", passwd
 
class HitBreakpoint(gdb.Breakpoint):
    def __init__(self, loc, callback):
        super(HitBreakpoint, self).__init__(
            loc, gdb.BP_BREAKPOINT, internal=False
        )
        self.callback = callback
 
    def stop(self):
        self.callback()
 
        return False
 
HitBreakpoint("*0x08048665", callback_username_condition)
HitBreakpoint("*0x0804842B", callback_compare_password)
HitBreakpoint("*0x080484EA", callback_compare_password)
HitBreakpoint("*0x080485A9", callback_compare_password)

And run it...

(gdb) source script.py 
Breakpoint 1 at 0x8048665
Breakpoint 2 at 0x804842b
Breakpoint 3 at 0x80484ea
Breakpoint 4 at 0x80485a9
(gdb) r hackyou 0123-4567-8910
Starting program: /tmp/unpacked_rev300 hackyou 0123-4567-8910
warning: Could not load shared library symbols for linux-gate.so.1.
Do you need "set solib-search-path" or "set sysroot"?
[+] k
[+] ke
[+] kec
[+] kecc
[+] kecc-h
[+] kecc-ha
[+] kecc-hac
[+] kecc-hack
[+] kecc-hack-y
[+] kecc-hack-yo
[+] kecc-hack-yo0
[+] kecc-hack-yo0u
Great! Now submit the license key for 'hackyou'
[Inferior 1 (process 14524) exited with code 01]
(gdb)

Flag: kecc-hack-yo0u

Tuesday, April 5 2011 00:12

NDH2k11 Prequals - Rce300

L'épreuve qui nous a donné le plus de mal, est sans doute la RCE300. C'était la dernière épreuve qu'il nous restait, et toute la team s'y est attaqué afin de finir ces préquals :p

On nous fournissais un fichier crackme.nds, avec pour seule indication qu'il s'agissait d'une application pour nintendo DS !

L'intéressant ici était de se documenter sur le format, et d'arriver à se faire un environnement de débug + reverse :)

Au final, nous avons utilisé les outils suivants :

Avec ça on est paré, on peut donc maintenant lancer DeSmuME en mode dev avec la commande :

DesMume_dev.exe --arm9gdb=5555 crackme.nds

Pour y attacher ensuite un gdb, avec la commande :

target remote localhost:5555

rce300_gdb_desmusme.png

On utilise ensuite IDA, avec le loader permettant de charger le *.nds :

rce300_ida_nds.png

Au passage il est bon de noter que la DS utilise un processeur ARM, il va donc falloir se documenter un peu sur l'ARM, ici, ici, ici et ici.

En cherchant la chaîne password, on tombe sur une première routine intéressante : sub_20002B8. Dans laquelle on voit clairement l'affichage du prompt et la demande de serial (on devine que sub_2003888 est un printf() et que sub_2003ACC est un scanf()) : rce300_ida_printf_scanf.png

On voit ensuite que si on entre un serial, le chemin emprunté passe par 2 boucles qui ont la même structure :

  • Multiplication de R0 par une constante
  • Appel de sub_2003530 (qui prend en paramètre R0, et le caractère courant du serial entré dans R1, et qui renvoie R0 modifié)
  • Xor de R0 par une constante
  • En fin de boucle : comparaison de R0 à une checksum : granted si les 2 checksums matchent, denied sinon !

gdb_ida_checksums.png

On voit que :

  • La première checksum est 0x33E0D2F1
  • La deuxième checksum est 0xBCFA8D3F

Et enfin, on analyse la fonction sub_3530, qui était assez impréssionnante : rce300_ida_sub3530.png

Plusieurs hints avaient été données sur l'IRC #ndhprequals, notamment qu'il fallait faire un brute-force, et qu'il fallait se limiter au charset alphanumérique et à une longueur de 6 caractères. En refaisant l'alogrithme en C (le plugin hex-rays pour IDA peut grandement aider...) on est alors capable de faire un rapide bruteforce, qui au bout de quelques dizaine de minutes nous a sorti le serial valide "DsLrox" ! \o/

Voilà donc une épreuve vraiment sympathique, dont le plus intéressant pour moi a été de découvrir comment mettre en place un environnement de débug comme celui ci. Et ce fut également un bel exemple de travail en équipe ;)