Note: Before I could even start the VM with Virtualbox, I had to unpack the .ova, delete the .mf file and remove xml entries from the .ovf file.
The first portscan yielded only ssh as an open port. Upon connecting we get this banner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
“Knock Friend to Enter” and “Easy as 1,2,3” so that has to be port-knocking. I tried to “knock” with nc
, but that didn’t work. I hoped the sequence was indeed 1,2,3 so I just used nmap to do it:
1 2 3 4 5 6 7 8 9 |
|
And after this, port 1337 was open:
1
|
|
It’s a webserver that serves a single page. I tried to view robots.txt but was given another picture (two hipster hobbits or something). However, the page source contained <!--THprM09ETTBOVEl4TUM5cGJtUmxlQzV3YUhBPSBDbG9zZXIh>
. Let’s decode that base64:
1 2 3 4 |
|
OK so I browsed to that location. It’s a login page that is vulnerable to SQLi. I had a hard time finding and exploiting it, because I made a silly mistake. I dumped the database I needed, finally, with this command:
1
|
|
In first instance, however, I only used --data "username=bleh&password=d
, which prevented SQLmap from finding the SQLi. Note to future self: always include all form parameters!
Anyway, dumping all the data from the db took too long because it was blind SQLi so I just dumped Webapp
once I found the database names. Long live SQLmap! This database contained usernames based on Lord of the Rings. I tried logging in to the webpage but couldn’t find anything interesting. I tried the five usernames against ssh and struck gold with smeagol:MyPreciousR00t
.
From here, there were two ways to root the box: one via a suid binary, one via mysql
. I’ll start with the SQL route.
mysql
is running as root:
1 2 |
|
That’s really nice. I grabbed the login from the web source:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
We can now login to mysql
as root and upload lib_mysqludf_sys
to enable command execution. I ripped parts from my Kvasir writeup. I cloned the lib_mysqludf_sys repo and ran the following commands:
1 2 3 |
|
payload
was ran through tr -d '\n'
to remove newlines and the output of that command was entered into the mysql prompt. This creates the udf_exploit.so
file on the remote box. From there, I ran:
1 2 |
|
I created ssh keys for this joyous occasion and upload the public key to the root directory:
1 2 3 4 5 6 7 8 9 10 |
|
Now I could ssh in as root:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
So that’s the mysql
route. Now on to the binary!
1 2 3 4 5 6 7 8 9 10 |
|
Ah, interesting. This reminds me of knock-knock.
1 2 3 4 |
|
I grabbed door3
and started analysing it on my local box. It contains a strcpy
that allows us to control EIP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
I quickly found that I had to run the binary with the first argument containing 171 chars + EIP to overwrite the saved return address on the stack. Let’s see how we can spawn a shell…
1 2 3 4 5 6 7 8 |
|
Okay, so no protections whatsoever. We could place shellcode on the stack and pwn like it’s 1999.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Let’s just jump to the stack! However, there is no jmp esp
opcode in the binary, so it looks like we have a problem. ASLR is enabled on the remote box and none of the registers point directly to the “shellcode” on the stack. We can hardcode the stack address but that will lead to lots of failed attempts. Let’s be smart about this.
Verify no jmp esp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
However, we do have strcpy
and this memory region:
1 2 3 4 |
|
0x08049000
is rwx
! We can now use strcpy
to write ff e4
to memory and return to it, which bypasses ASLR because the binary is always loaded on the same address. Let’s ROP this! strcpy@plt
is at 0x8048320 but we can’t use that address because it contains a space. We’ll just use strcpy+6, which jumps to the resolver to resolve strcpy.
1 2 3 4 5 |
|
With that out of the way, we need a pop2ret to balance the stack. This is easily located with gdb-peda’s ropgadget
command and I chose 0x804850e. Last thing I needed was the address of the bytes 0xff
and 0xe4
and again gdb-peda provided those. Putting the ROP chain together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Death to nopsleds!
1 2 3 4 5 6 7 8 9 10 |
|
And there you go. A reliable way to spawn a root shell from that vulnerable binary. Game over.
1 2 3 |
|
We’re given a 32-bit ELF binary, which uses malloc() and free(). Must be some kind of heap vulnerability then, right?
The binary is some kind of contact storage. We can enter names, phone numbers and descriptions. The name gets stored in the .bss segment, while the phone number and description are stored on the heap. We can overflow the name field like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
So suppose the struct looked something like
1 2 3 4 5 6 7 |
|
We can use this to leak information by overwriting the next struct’s phoneNumber and description. We cannot use this to write anything, as the program will automatically call free() on the description. If it doesn’t contain a valid heap address, the binary will crash. First, we’ll abuse the leak to determine the remote libc version.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
|
This gave us two addresses, and libcdb.com gave us no less than five options. We logged back into the server for pwn100, which had a different IP address so was probably a differnent box. We figured that the CTF organizers used the same base image for each box. The symbols of the libc on pwn100’s box seemed to match what we found via the leak and we just got the address of system from that machine.
Now, we needed a way to write to the got. Turns out there was another format string vulnerability in the description field:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
We used the same trick as in ekoparty’s echoes binary, selecting a stack address (actually a saved stack frame pointer) which points to another stack address, which we can then use to write out an address on the stack. Finally, use that third format string argument to write anything anywhere.
We then use the leak to determine libc’s base address, add the offset for system, and finally overwrite memset@got with system. Then, once we delete an element with name /bin/sh
, we in fact call system('/bin/sh')
and we win.
Without further ado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
|
And in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Gotta love format string vulnerabilities. The flag was flag{f0rm47_s7r1ng5_4r3_fun_57uff}
.
The public key is an RSA key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
It has a rather weird number of bits, not a multiple of 256. The modulus is converted to hex using python and yields:
1
|
|
Hmm. Since the modulus n
is made from the product of two primes, we’d better find those primes. Luckily, I found another writeup and a website called factordb.com. Factordb does exactly what it says on the box and yields two factors, 3133337
and another large number. The first number made me think I was on the right way!
Now, we need to reconstruct the key. We have n
, p
and q
which is all we need. I couldn’t decrypt the message in Python, as Python’s cryptolib requires the modulus to be a multiple of 256 bits. So I resorted to the voodoo magic that is openssl
. I found this nice post on stackoverflow about creating specific rsa keys.
I followed that post, computing:
1 2 3 |
|
for which I needed
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Then I put everything in a file called newkey.der
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The privExp
was calculated like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
I then ran $ openssl asn1parse -genconf newkey.der -out key.der
. After mucking around with the flag, I verified I had the right private key by encrypting a test message with the public key. I noticed that the flag was base64 encoded whereas my encrypted message wasn’t…
1 2 3 |
|
That last openssl
command was just tested with -raw
and -oaep
(options for padding). -oaep
worked!
Flag was EKO{classic_rsa_challenge_is_boring_but_necessary}
. I learned something \o/
1 2 3 4 5 |
|
However this wasn’t the flag. As we’ll see later, this was part of the challenge. We started dumping the entire binary using the string format vuln. Our format string can be found at offset 8 with 1 byte of padding:
bAAAA%8$x
1 2 3 4 5 |
|
From the raw dump, we reconstructed parts of the binary. We came across a string that said “OMG! Well done, here’s your flag” or something along those lines. Looking through the raw dump, we found where that string is referenced. Disassembling with radare2 gave us this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Since our input can never be larger than 12 bytes, we can never win the strcmp with Welcome to ekoparty 2015
. From here, we tried a lot of things. The 12 char input limit was annoying, cos we couldn’t really write anything, anywhere:
A\xde\xad\xbe\xef%255c%8$hn
was 15 chars… So we set out to make a write() function using the format string vuln.
On the stack, there are many DWORDS. Some of them contain stack addresses. Some of those addresses refer to other stack addresses (they’re usually stack frame pointers). Using three memory positions, we could construct a write function using format string argument 25, 61 and 124 (ASLR is off so the addresses remain constant). Here’s how:
$25 contains the address of $61. Let’s assume $25 contains 0xfffff080.
$61 is at 0xfffff080 and contains 0xfffff1a0, which is $124 (math doesn’t work out but bear with me).
$124 contains nothing of interest, and isn’t used by the binary.
If we now use the format string, we use $61 to write to $124, and $25 to update $61. Finally, once we’ve written out an address in $124, we can use the format string to write to that location.
Let’s assume we want to write 0x41414141 to 0x804b020. We first do this:
1
|
|
Then, we update the pointer at $61 by writing to $25:
1
|
|
So $61 will now contain 0xfffff1a1. Then we write to $124 again via $61:
1
|
|
Etc until we have 0x804b020 at $124. Then we write using $124:
1
|
|
And we’ll have written the first byte to 0x804b020. We repeat this process for the other bytes…
With our new and shiny write() function we set out to break this challenge. We first tried truncating the string “Welcome to ekoparty 2015!” in memory, so we could have an input that would fit the 12 char limit. Writing to that location didn’t work, presumably cos that string was in non-writeable memory. Remember, we needed to enter the code path here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
We dumped and reversed the function at 0x133720cc
, which apparently decodes the flag. However, we where only able to get the pieces EKO{
and b4by
. The dump process wasn’t perfect, so we continued.
So how could we get strcmp() to return 0? By overwriting the value in the got with a location that does xor eax, eax; ret
. We dumped bytes around the strcmp pointer (ASLR was off so libc was at a static position) and indeed, we identified a rop-like gadget that did just that! Careful overwriting of the got pointer of strcmp in two steps was necessary, but again we lucked out: strcmp was at 0xb7f60b60, the xor eax, eax
gadget was at 0xb7f61fc4. At 0xb7f61c60, there was this perfect gadget:
1 2 3 4 |
|
In two steps, we overwrote strcmp (we can’t have the got pointer pointing to illegal instructions, cos that would make the binary crash before we can overwrite the next value!) with the xor eax gadget… and we got something like this:
1
|
|
And then nothing?! We overwrote the free() got pointer with puts and this dumped more text, convincing us that the overwrite was working correctly. This lead us to believe the binary was broken… Which indeed it turned out to be, later on.
Next, loads of failed attempts later, we stumbled upon this writeup of another challenge that uses ret2dl. In other words, abuse the symbol resolver. Quickly after that, we found another good writeup, using a slightly easier technique. It traverses the link_map found in memory to grab system()
.
This turned out to work. We traversed the link_map by hand, adjusting the leaked bytes from time to time (remember, printf chokes on nul bytes).
At the start of the got, we find these bytes:
1 2 3 |
|
We take 0xb7fff938, the *link_map, and dump from there. link_map looks like this:
1 2 3 4 5 6 7 8 9 10 |
|
So at 0xb7fff938 we find:
1 2 3 4 |
|
This is the binary. On to the next one at 0xb7fffc28, which was linux-gate… Then the next one was indeed libc! This was confirmed by reading link_map->l_name. We now had the dynamic section of libc, time to parse it for system. Soon, using the printf format string vulnerability and a messy python script, we were dumping symbols. After about an hour, we hit the jackpot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
We had the address of system! Now, the plan of attack was as follows:
/bin/sh
to 0x13373008, which gets used in a free() callxor eax, eax; ret
gadgetThis almost worked out, but we set strcmp to 0xb7f61f60 and that turned out to be enough. We landed a shell!
The poc we used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 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 122 123 124 125 126 127 128 129 130 131 |
|
Yeah. Pretty horrible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
After 10 seconds, we got kicked out, but that was enough time to grab some files. Sadly, the flag file was not the flag… someone planted it there (thanks! but no thanks). So we grabbed the C source file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
|
Spot the mistake!
Found it? Yeah:
1
|
|
Thanks. Anyway, we reimplemented this file in Python (honestly couldn’t get the C program to run without segfaulting, bleh).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Which finally gave us the flag:
1 2 |
|
The flag was EKO{b4by_3xpl0it_FMT_str1ng_FTW!#$}
. Too bad the challenge was broken, nice to learn a new technique!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 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 |
|
And against the remote binary:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
|
We’re given the python server and a binary. We’re supposed to write a ROP chain to satisfy certain conditions. The server.py will then pass random values for the registers to the binary, along with the ROP chain we provide. After execution of the ROP chain, it will check if the ROP chain has calculated the right answer. Pretty nifty, if you ask me!
The binary itself is actually packed with everything we need:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The expressions we need to satisfy look like this: $rax + $rbx + 1337 (store result in rax)
I’ll just give the final exploit as it’s not that difficult:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
And in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 |
|
Hmm. Let’s see what makes this thing tick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
We see strings like bc8bcbe1H
, which look like part of a hash being pushed onto the stack. Combining the hash part gives a SHA256 hash which has no known plaintext. Hmmm! Since this is an exploit-focused binary, let’s exploit it!
It gets interesting around this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
It passes a few strings to the stack. We’ll be seeing them later. Let’s run the binary in gdb
.
1 2 3 4 5 6 |
|
PIE is enabled, so find out the base address of the binary with vmmap
. Our input gets processed here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
We have an unchecked strcpy
. Lovely. Let’s see what it will copy where:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
So it will copy our input to the stack. Let’s examine the stack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Nice, what are those bytes at 0x7fffffffe290
?
1 2 3 |
|
Hey, that second one looks like the SHA256 hash! We’ll be able to overwrite this… Seeing as it’s stored as a string, better set a breakpoint on strcmp()
for later…
1
|
|
Restarted the binary:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
OK, so it will compare the SHA256 hash of our input with the one stored on the stack. The nice thing, however, is that it will only hash the first 0x100 bytes! This means we can predict the hash we get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
And if we do it again, but send 512 * ‘A’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
We’ve overwritten part of the hash on the stack, yet the hash of our input stayed the same. After some trial & error, I could reliably overwrite the hash:
1 2 |
|
What’s left now is to exploit it to grab the flag:
1 2 3 |
|
We’re given a binary and the source. We need to supply a name that will be processed into an integer. The resulting integer should be 0xCCC31337
. If you look at the function:
1 2 3 4 5 6 7 8 9 10 11 |
|
Finally, the value for level
is modulo’d with 0xcafe
. This means that level
can never be the required value 0xCCC31337
. We’ll need to co-opt another section of code to pass the check. This quickly came to mind:
1 2 3 4 5 6 7 8 |
|
Excellent. We have a format string vulnerability. After hex-editing the binary to get rid of the usleep()
calls, I bruteforced the location of our format string on the stack (starts at position 7). Next, the disassembly of hacker-level
shows us where level
is at in memory:
1
|
|
All I needed to do was to write the correct format string. I came up with:
1 2 3 4 5 6 7 8 9 10 |
|
Running this against the remote binary using nc
gave The flag is: CAMP15_337deec05ccc63b1168ba3379ae4d65854132604
. Done!
We’re given a 64-bit ELF binary and Swappage also managed to obtain the corresponding libc. Upon starting the binary, we’re presented with the following:
1 2 3 4 5 6 7 8 9 |
|
The x
after the BBBB
in the above example was actually 0x7f
, so the binary is leaking part of an address (we later determined it to be a stack address). In the end, I couldn’t make use of this, but it was interesting to see. Swappage already found the bugs: we can send a large message length and this will allow us to overflow a stack buffer. NX is enabled so it’s ROP time!
Besides NX, ASLR is also enabled. This means we have to first leak a libc address to calculate libc’s base address and then something like system()
. I made use of the puts@plt
to write out the contents of puts@got
. The latter contains the libc address of puts()
, which we can then receive. Superkojiman was able to find the one-shot RCE gadget. The ROP chain goes to read@plt
and awaits our input. Upon receiving the address of the one-shot RCE gadget in libc, the ROP chain overwrites puts@got
and restarts the binary from main()
. This latter decision was based on using system()
instead of the one-shot RCE gadget, but by the time I was done implementing the ROP chain, superkojiman had already supplied the offset. When main()
restarts, one of the first functions it calls is puts@plt
, which is now pointing to the shell-spawning one-shot RCE gadget. We land a shell and are happy!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
|
The reason for setting up rbp
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
And the exploit in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Next, we turned to phobos for 300 points, which is nearly the same binary but without NX! After a few small changes to the previous exploit, we obtained the flag for phobos as well:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
|
And in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
It’s quite nice to solve a 400 point challenge only to find out we can use nearly the same solution for an additional 300 points!
]]>1 2 3 4 5 6 7 8 9 10 |
|
Furthermore, the output of strings ./shell
shows that the binary is looking for creds.txt
. I placed a file in the same directory with admin:admin
as sole line and indeed, I could now login:
1 2 3 4 5 6 |
|
So we’ll need to login with valid credentials, which we do not have yet. The rest of the commands are not of interest. Let’s have a look at the disassembly, specifically at the part where our input is processed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
So basically, the calls to gets()
happen to write to the stack. We’re not able to overwrite the saved return address because of the canary, but we can overwrite the pointer at [rbp-0x18]
, which contains a pointer to the string creds.txt
. We can overwrite this with a pointer to another string, to make the binary open another file to check our credentials!
The only option that I could find was /lib64/ld-linux-x86-64.so.2
. The problem is, does this actually contain valid pairs of user:name
? I grabbed the file via a shell I obtained on bitterman and ran it through strings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Indeed, it does! So the plan is to overwrite the pointer on the stack with the pointer to /lib64/ld-linux-x86-64.so.2
, then login with one of those combinations of “username” and “password”. The exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
And it action:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We’re given a zip file containing a binary and the correspondig libc.so. How nice!
1 2 3 |
|
Running it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
It doesn’t do a whole lot, it takes two strings as input and then starts looping. Superkojiman and I quickly realized we could crash this C++ application by sending more than 300 bytes as input. However, this made it crash in strlen
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
This is unfortunate. It actually combined our inputs and then decided to crash via a null pointer in strlen
. We started investigating the binary in more detail. It fills two “Tag” structures on the heap with our input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
It checks the length of the input to be less than or equal to 0x201 or 513 bytes. If it is larger, the structure on the heap is not filled (hence our crash in strlen()
). The code at loc_80489ef
checks our input for the %
character… This hinted at a format string vulnerability!
If there is no %
character present, a flag in the structure on the heap will be set to 1
. If not, the flag will be 0
. This value is later checked and the binary will print:
1 2 3 4 5 6 7 8 |
|
I call this flag “Tag::verify”. I started playing around with the input and I was able to overwrite the verify flag of Tag2 using a buffer overflow in Tag1:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
First, I sent an invalid Tag1 and Tag2 buffer, containing a %
character. Then I get the option to send another input for Tag1. I submit 512*1
plus a newline (which is a
in hexadecimal). The newline ends up in Tag2::verify!
This means we can bypass the verification by overflowing Tag1 into Tag2::verify. I started a socat
listener and started experimenting in a python script.
1
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
The offsets are taken from the output of nm -D ./libc.so.6 | grep <function_name>
.
1 2 3 4 5 6 7 |
|
It works! We can bypass the string format protection by overflowing Tag1 into Tag2. Now things become interesting. We need a way to spawn a shell, so we need system()
. However, ASLR is probably enabled, so we need to leak a libc address somehow. We can easily leak addresses with the string format vulnerability. Let’s run it against the super slow remote server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
1 2 3 4 5 6 7 8 |
|
Looks good, right? Now, the trick is to invalidate Tag2 again, so we can set it to a new format string, then revalidate it again. The new format string will take care of overwriting a GOT pointer with our acquired system()
address.
I chose to overwrite memset@got
with system()
. One of the arguments to memset is the buffer which contains our input. By overwriting memset with system, the next time memset is called, we’ll effectively run system(our_input)!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
|
Let’s run it against the remote system:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
A lot of crap is printed, but in the end we land our shell!
]]>Again, it’s a 32 bit ELF binary. Running it yields the following:
1 2 3 4 5 6 7 8 |
|
Not very useful. The disassembly provided some hints, for it had functions like shuffle
, unshuffle
and bubblesort
. The program kicks off by clearing a lot of stack space and calling unshuffle
. Then, it asks for user input, maximum size 0x44 bytes. I decided to enter 0x44 * A
(what else?).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
When runnning the shuffle
function, the program executes system()
, which spawns /bin/dash
on my system, effectively stopping me from debugging it in gdb
. I patched system in gdb so it would return immediately and I could trace the program. Turns out shuffle
takes the GOT entries, all the function pointers, and shuffles them around. unshuffle
negates this operation. After the second time I entered 0x44 A’s, the program crashed with control over EIP and EBP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Cool, easy control over EIP. However, at this point, we cannot rely on the GOT entries, because they are still shuffled! We can’t just ret2system. I spent some time trying to return to unshuffle
, but kept losing control of the program execution.
But let’s take a step back here. Linux ELF binaries employ something called “lazy linking”. When a binary is started, the symbols are not resolved yet. Only when a function is called for the first time will the function address be resolved. The GOT entry will be pointing to this look up code (memcpy as example):
1 2 3 4 |
|
When called for the first time, 0x804b01c
will be pointing to 0x80486d6
, which will kick off the function resolver. So instead of using 0x80486d0
to do a memcpy, I’d just use 0x80486d6
. This bypasses the mess that shuffle
made!
With all this in hand, I wrote an exploit and the corresponding rop chain (well… more like ret2resolve ;)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
1 2 3 4 5 6 7 |
|
Easy peasy! The flag was flag{rand0mizing_things_with_l0ve}
. Nice!
We’re given a 32 bit Linux ELF. Upon running it, we’re presented with a library menu, where we can view titles, add them and exit the program:
1 2 3 4 5 6 7 8 9 |
|
So we can’t really add long titles. Upon inspection, Swappage and I noticed that the titles are stored on the stack, with the lengths in a special data structure in the .data
of the binary:
1 2 3 4 5 6 |
|
I noticed I could bypass the length check with a large number, effectively utilizing a signedness bug. This allowed us to overwrite the return address of main()
on the stack. Although NX wasn’t enabled, ASLR was enabled so we couldn’t just jump to the shellcode on the stack. There weren’t enough gadgets for a ROP chain. Instead, we needed to leak a stack address so we could return to the shellcode on the stack (bruteforcing it didn’t work). That’s where the read function came into play.
Looking up a book title via the read function was done like this:
1 2 3 4 5 6 7 8 |
|
By passing in a negative number, I was able to make 804867b: mov eax,DWORD PTR [eax*4+0x804a060]
point to 0x80493fc
, which contained 0xffffffe0
. Therefore, when this value is added to the pointer to the book titles, it actually is moved backwards and starts leaking stack addresses:
1 2 3 4 |
|
I now had a way to leak the book title buffer on the stack, where we could store shellcode in a book title. By exploiting the signedness bug, we could overwrite the return address of main()
. After setting all of this up, we’d ask the binary to exit and make it return to our shellcode. Putting it all together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
And running it:
1 2 3 4 5 6 7 8 9 10 |
|
The flag was flag{John_should_read_a_real_book_on_s3cur3_pr0gr4mm1ng}
.
We’re given nc haas.polictf.it 80
as point of attack. Let’s start:
1 2 3 4 5 6 7 8 9 |
|
Interesting. I now know I’m up against some Prolog program. Let’s see if we can inject Prolog code:
1 2 3 4 5 6 7 |
|
Looks like we can! Now, since I never learned Prolog as a kid (I’m more of a BASIC boy), I searched around for code examples. I couldn’t just grab the flag so I started with enumerating.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
I now had the location of the flag. Now, to read it… Turns out Prolog does not have a “normal” way to read arbitrary files (it expects Prolog terms or something). I searched for a good while and finally:
1 2 3 4 5 |
|
Back to python!
1 2 3 |
|
50 points and first blood for Team Vulnhub.
]]>I downloaded the APK and directly uploaded it to decompileandroid.com. Among the decompiled files I found src/it/politctf/LoginActivity.java
and three other java files. After inspecting LoginActivity.java
, I found this function:
1 2 3 4 5 6 7 8 9 10 11 |
|
Interesting. It performs a bunch of operations on a string, which I don’t know yet. However, on of the operations is this one:
1 2 3 4 |
|
So I did the following:
1 2 |
|
I now had the string and all the operations on the string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
Using this string, I started working my way back, applying all the operations of a.java
, b.java
and c.java
by hand. Finally, I ended up with the string flag{Maybe_This_Obfuscation_Was_Not_That_Good_As_We_Thought}
.
We’re given a host, dropper.polictf.it
. It has no open ports, but the challenge description mentioned that John did not need ports to communicate. I left this challenge for a while, focusing on others. When I got back, superkojiman noticed that pinging this host dropped a lot of packets. He saw patterns: sometimes one packet dropped, sometimes three in a row. This made me think of Morse immediately.
I started pinging the host and grabbed the output of ping
:
1 2 3 4 5 6 7 8 9 10 11 |
|
As you can see, request 2, 4 and 6 are dropped. With the following one-liner, I extracted the icmp_req numbers:
1
|
|
I wrote a python script to translate the drops to Morse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Which yielded:
1
|
|
The first three characters spell out “SOS”. I translated the rest by hand and found: SOS THIS IS THE FLAG?IT?IS?NEVER?TOO?LATE?FOR?A?DROP?
. I couldn’t really figure out the characters that are marked ?
. I guessed them to be underscores, but in the end, duckduckgo came to the rescue. The final flag was lowercase: flag{it-is-never-too-late-for-a-drop}
.
The download contains a text file with base64-encoded data, which becomes a .gz archive. After decompressing, I obtained a text file with biblical text. Not my cup of tea, but I immediately saw that certain sentences were duplicated. I wrote a python script to count the occurences of lines:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Which gave me these frequencies:
1 2 3 4 5 6 7 8 9 |
|
A total of 30 distinct strings were found. I guessed these strings represented letters, so I extended the python script a bit and started puzzling:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
I guessed that the most common string was a space, which indeed yielded word- and sentence-like output:
1
|
|
I then started to look for THE, THIS, A, AND to guess the first couple of letters:
1
|
|
Slowly but surely, I translated all the letters, and the words slowly emerged (I love that!):
1 2 3 4 5 6 7 |
|
The final translation dictionary was: x = dict(zip(sorted(d, key=d.get, reverse=True), " TESILAOHGNRYBWMUC'FP!,VQD{K?}"))
The challenge already said that the flag needed a bit more work. Indeed, flag{lbhtbgguvfsyntbbqwbo}
was not accepted. What then? Bitvijays suggested that the flag was another “ciphertext”, so I thought of Caesar cipher. The easiest is rot13 and indeed, the flag was flag{yougotthisflagoodjob}
.
We’re asked to connect to exorcise.polictf.it:80
. Upon connecting, we’re presented with a hexadecimal string. I pressed return and got another:
1 2 3 4 |
|
Because of the challenge title, I xor’ed the second string using the first as a key and got gibberish. I decided to reconnect and send a bunch of NULL bytes (Ctrl+Space). When I xor’ed that string against the first, I got this:
1
|
|
Hmmmmm. I reconnected again, sent a bunch of A characters and the resulting string was xor’ed vs 0x41:
1 2 3 4 5 6 7 8 |
|
1 2 |
|
There’s the flag: flag{_this_1s_s0_simple_you_should_have_solved__it_1n_5_sec}
. Took me a bit longer than 5 seconds…
level0
. In this case, we have the source code, which helps tremendously. Nevertheless, start by treating it as a blackbox.
First, enable coredumps.
1
|
|
Then, make sure you’re not running the exploits against a SUID binary. Linux, by default, will not generate coredumps for SUID binaries. Fair enough. Thanks to @Swappage for alerting me during the workshop!
1 2 |
|
Finally, disassemble the binary with objdump
:
1 2 |
|
In some cases, the binary is the only thing given, with no source code available. The disassembly will help to get an understanding of what the binary is doing.
Another useful command is file
:
1 2 |
|
This tells us that the binary is 32 bit and statically linked, which explains its large size.
Let’s have a look at the disassembly of main()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
We have a classic buffer overflow situation here:
1 2 3 |
|
The lea
command will load a stack address into eax
. That address is put on the stack as an argument for _IO_gets
, which will happily read more than enough bytes from STDIN to overflow the buffer and overwrite the saved return address on the stack.
Let’s switch to gdb-peda
and see the binary in action.
1 2 3 4 5 6 7 8 9 |
|
checksec
is a very useful command available in gdb-peda
(not in vanilla gdb
). In this case, one can see that only NX is enabled, meaning that the stack, heap and other data sections are not executable, whereas code sections are executable but not writeable. Let’s check this within gdb
. First, enter start
to run the binary and break at the main()
function automatically. Then, inspect the memory layout with vmmap
, which will show memory regions that are active in memory along with their memory protection flags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
The output of vmmap
clearly shows NX in effect: the stack is marked writeable but not executable; the binary, loaded at 0x8048000, is marked executable but not writeable.
So far, so good. Let’s continue to run the binary with c
and try to overwrite the saved return address on the stack, taking advantage of the _IO_gets
call. Note: you can use a patterned buffer for this as well, check out pattern_create
and pattern_offset
in gdb-peda.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Lucky shot. eip
loaded with LLLL
because we’ve overwritten the return address for main()
on the stack. As soon as the ret
at the end of main()
was executed, it popped the value off of the top of the stack into eip
and increased esp
with four. Because we’ve overwritten that value, we now control eip
. To have a look at the stack, issue the following command:
1 2 3 4 5 6 |
|
x
stands for inspect, with the format specifier and amount after the slash (in this case, 20 DWORDS). Finally, give it the address from which you want to inspect. In this case, I chose $esp-48
, which is the start of the buffer on the stack. Confirm that this is our input.
So let’s use this first bit of information and write a script to reliably overwrite the saved return address on the stack. This will serve as the skeleton for our exploit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
To verify that this will return to 0xdeadbeef
by overwriting the saved return address, we have two options:
Running the exploit in this way is the most accurate way, at least as far as memory layout and stack addresses are concerned. There might be a discrepancy between memory addresses when running within gdb
vs outside of gdb
. There is a way to fix this, using fixenv: I did not know of this solution until BSides!
1 2 3 4 5 6 7 8 9 10 11 |
|
This method is especially useful if you need to inspect the memory with vmmap
: gdb
cannot display memory layout of a coredump!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Regardless of which method is used, eip
now points at 0xdeadbeef
, which confirms that our proof-of-concept exploit works as intended. We can now start extending the ROP chain to start doing useful things.
In the workshop, I showed the mprotect/read/ret to shellcode strategy. In this writeup, I will use a different way to spawn a shell. We will need access to execve
or system()
for this.
A lot of students of the ROP workshop tried to find system()
, fruitlessly:
1 2 |
|
system()
is not linked in this binary! There is, however, one int 0x80; ret
gadget available, which we can use to build a ROP chain. During the workshop in London I showed the mprotect
and read
strategy. Now, I’d like to show how to do an execve
syscall using the ROP chain. For added fun, I’ll assume that NULL bytes are badchars.
First, however, upload the binary to ropshell.com or use Your-Favorite-ROP-Gadget-Dumper.
One thing that is absolutely mandatory is access to a gadget that does a syscall. ropshell.com suggests > 0x08052cf0 : int 0x80; ret
. Sometimes, there might be another gadget where extra instructions are present between the int
and the ret
. This is usually fine and you can find them in ropshell.com by searching like this: int 0x80 ?
. The extra ?
indicates that extra opcodes may be present.
Now that we have that all important gadget, we can start building the rest of the ROP chain. We’ll need to set a couple of registers and build the argument for execve
in memory.
For x86 syscalls, the arguments are passed in registers. This website contains a list of the syscalls and a short description of the arguments. For execve
, we see this:
1 2 3 4 |
|
However, I was unable to get the exploit to work when ecx
was pointing to a string. Instead, I opted to set ecx
and edx
to NULL. Let’s start building this ROP chain, starting from the PoC. We will need to write out the string /bin/sh
somewhere in memory. For this, we need two things:
For #1, we can look at the output of vmmap in gdb-peda:
1 2 3 4 5 6 7 |
|
ASLR is disabled, but taking the heap or stack is not my favorite option. Instead, let’s use 0x080ca000
to 0x080cb000
. This area is readable and writeable. Not executable but that doesn’t matter, as we will not store shellcode there anyway.
For #2, ropshell.com has no good suggestions, as they are add [r32], r32
instructions. If the memory contains values already, we’ll not be able to write out the string reliably, unless the block of memory contains NULL bytes.
To avoid complications, I searched for mov [?
in ropshell.com:
1 2 3 4 5 6 7 8 9 10 11 |
|
I like 0x08079191 : mov [edx], eax; ret
a lot. It’s only uses two registers and contains no unnecessary instructions. Let’s see how we can set edx
and eax
to what we need.
1 2 3 4 5 6 7 8 9 10 |
|
Plenty of gadgets we can use. The plan is now to pop the address 0x080ca040
into edx
and the value /bin
into eax
. The address is arbitrary, but chosen such that we don’t overwrite anything important or that the address contains NULL bytes. Let’s build the first PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Run it and expect the memory area:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Excellent, that worked. Now we have to set the registers accordingly. ebx
must be set to 0x80ca040
, eax
must be set to 0x0b
and we’ll zero out ecx
and edx
.
There are no gadgets that do xor ecx, ecx; ret
. Instead, I opted to load 0xffffffff
into ecx
and edx
and then increase the registers by one; this will overflow and make both of them zero.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Our next problem arises: I don’t want to use NULL bytes. However, we’ll need to set eax
to 0x0000000b
. I use the following sequence for this, making use of the movzx
instruction. movzx
is move into register, zero extend.
1 2 3 4 5 6 7 8 |
|
That’s it. Let’s try:
1 2 3 4 5 6 7 8 9 10 11 |
|
Nice! It looks like the shell was spawned! A final test consists of running it on the command line. The extra cat
is added to keep the spawned shell alive, by connecting stdin and stdout of the newly created shell.
1 2 3 4 5 6 7 |
|
That was about it. The ROP chain is able to set all the required registers, write out a string in memory and finally perform a syscall.
]]>So we’re given a binary and a place to connect to. Upon running and examing the binary, it seems like this is a very easy ROP challenge. The binary will give libc function addresses upon request; this makes it easy to defeat ASLR. The option of getting libc’s base address seems to return some strange address. Finally, the third option asks for a buffer, which is then copied to the stack, overwrites the saved return address and basically kicks off our ROP chain… couldn’t be easier, right?
1 2 3 4 5 6 7 8 9 |
|
So exploiting it should be relatively easy. The binary itself contains very little useable gadgets. We can defeat ASLR by leaking function addresses. There is, however, the problem of finding the correct libc version. This took us some time to figure out, but luckily Swappage found an offline tool to identify libc. It was libc6_2.19-0ubuntu6.6_i386
. Another nice tool to identify libc is libcdb.com. After identifying the right libc version, we could find all the necessary gadgets via ropshell.com. Our plan was to mprotect()
a certain region of memory as RWX, then read()
in some shellcode and return to it.
Now, the plan fell through. For some reason, the read()
syscall to read in the shellcode failed. Instead, I switched the exploit around a bit. We have access to system()
, so I set up a ROP chain to mprotect()
the first 0x1000 bytes of libc as RWX. Then, I wrote out the string /bin//sh
to memory. At this point, it was getting late and I could have just as easily written out /bin/sh,0
to memory… Finally, returning to system("/bin//sh")
spawned a shell, allowing us to read the flag!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
|
Putting it all together:
This was an easy one, but still took me a while to get back into binary exploitation. Especially getting the correct libc version took longer than necessary and my thanks go out to Swappage for persisting and finding the correct version!
]]>