Rev without R - Autorské řešení úlohy
Solution #
Když se připojíme na port, zjistíme, že jsme v shellu a máme k mání binárku
rev_without_r a main.c (zřejmě její zdrojový soubor). Jak se píše v zadání,
tak se zcela jistě bude jednat o jednoduchou úlohu, takže by by mělo stačit jen
spustit daný program:
$ ./rev_without_r
You want a flag? Too bad you can't read it.
But I'm nice, so I will read it for you.
So here goes your flag:
Hmm... Strange. I got a SIGSEGV when reading it.
I guess I can't read it either.
Well... Good luck trying to read it then...
… což ale zcela nečekaně nefunguje. Podle vypsaného textu lze poznat, že
main.c nejspíše opravdu bude k tomuto programu. Ze zdrojového kódu lze
vyčíst, že program se skutečně pokusí vypsat vlajku, ale zřejmě opravdu nastane
segfault… Ze zdrojového kódu lze také přečíst, že vlajka se zadává při
kompilaci programu a pak se ukládá do zvláštně anotované proměnné:
// FLAG_STR je definovaný symbol ("z příkazové řádky") obsahující vlajku
__attribute__((section(".flag"))) volatile const char FLAG_DATA[] = FLAG_STR;
Vlajka je tedy “prostě” zapsaná do programu jako plaintext, ovšem do nějaké
speciální sekce, což ale nás asi nemusí moc zajímat… Tím pádem řešení by mělo
být jednoduché, prostě jen vzít strings a … Aha, binárka má práva 0o111 -
můžeme ji jen spouštět (ale ne číst).
Naštěstí to nám může postačit, protože se (nejspíše (podle relativně malé
velikosti)) jedná o dynamicky linkovaný program a tedy můžeme použít
LD_PRELOAD, abychom načetly “do programu” náš kód, který pak ale může číst
paměť programu (a tedy i (nějaké) části samotné binárky). Na internetu lze
najít nějaké kódy, které to dělají, pokud se nám nechce psát vlastní - osobně
jsem použil (jako můj výchozí bod) řešení z
https://unix.stackexchange.com/a/711936.
Když nějaké takové řešení připravíme a zkusíme, tak zjistíme, že se nám vlajku získat napodařilo… Pokud máme dostačně chytrý kód, tak nám sdělí, že při se mu v jeden moment nepodařilo z paměti nic přečíst (a nebo rovnou (stejně jako původní program) segfaltujeme). Můžeme udělat tip, proč se tomu tak děje: segfault znamená, že přistupujeme na “špatnou” pamět - buď neexistuje a nebo nemám práva. Vzhledem k tomu, že náš “injektovaný” kód by se měl pokoušet číst (ideálně) pouze namapovanou pamět (= “existující”), pak zbývá pouze druhá možnost.
Pokud ale nevíme, tak můžeme využít poměrně zajímavého faktu, že proces může
přečíst svoje /proc/self/maps, což my jako uživatel nemůžeme, i přestože jsme
vlastníkem procesu a i přestože maps soubor je podle práv readable - když si
soubor necháme vypsat, uvidíme něco podobného jako:
00400000-00401000 r-xp 00000000 08:20 216611 /home/user/rev_without_r
00401000-00402000 rw-p 00001000 08:20 216611 /home/user/rev_without_r
00402000-00404000 ---p 00002000 08:20 216611 /home/user/rev_without_r
00404000-00405000 r--p 00004000 08:20 216611 /home/user/rev_without_r
00910000-00931000 rw-p 00000000 00:00 0 [heap]
7f371825f000-7f3718262000 rw-p 00000000 00:00 0
7f3718262000-7f371828a000 r--p 00000000 08:20 1413334 /usr/lib/x86_64-linux-gnu/libc.so.6
7f371828a000-7f3718412000 r-xp 00028000 08:20 1413334 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3718412000-7f3718461000 r--p 001b0000 08:20 1413334 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3718461000-7f3718465000 r--p 001fe000 08:20 1413334 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3718465000-7f3718467000 rw-p 00202000 08:20 1413334 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3718467000-7f3718474000 rw-p 00000000 00:00 0
7f3718477000-7f3718478000 r--p 00000000 08:20 565665 /home/user/memdump.so
7f3718478000-7f3718479000 r-xp 00001000 08:20 565665 /home/user/memdump.so
7f3718479000-7f371847a000 r--p 00002000 08:20 565665 /home/user/memdump.so
7f371847a000-7f371847b000 r--p 00002000 08:20 565665 /home/user/memdump.so
7f371847b000-7f371847c000 rw-p 00003000 08:20 565665 /home/user/memdump.so
7f371847c000-7f371847e000 rw-p 00000000 00:00 0
7f371847e000-7f371847f000 r--p 00000000 08:20 1413314 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f371847f000-7f37184aa000 r-xp 00001000 08:20 1413314 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f37184aa000-7f37184b4000 r--p 0002c000 08:20 1413314 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f37184b4000-7f37184b6000 r--p 00036000 08:20 1413314 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f37184b6000-7f37184b8000 rw-p 00038000 08:20 1413314 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffc157e2000-7ffc15803000 rw-p 00000000 00:00 0 [stack]
7ffc158ec000-7ffc158f0000 r--p 00000000 00:00 0 [vvar]
7ffc158f0000-7ffc158f2000 r-xp 00000000 00:00 0 [vdso]
Vidíme, že tu je jedna podivná sekce (0x00402000), která nemá nastavena žádná
práva. Naštěstí tohle se dá z vnitřka programu změnit použitím (například)
mprotect(..., PROT_READ). Můžeme si teda upravit náš kód na získání paměti,
aby před čtením/kopírováním zkusil změnit práva paměti.
Po úpravě a spuštění již dostaneme celou paměť programu, nyní i s tou částí paměti, kde je vlajka.
Exploit script #
// Base taken from https://unix.stackexchange.com/a/711936
// but with added some extra things (maps file dump, comments, mprotect call, ...)
// Usage:
// gcc -fPIC -shared -o memdump.so memdump.c
// LD_PRELOAD=./memdump.so ./rev_without_r
// strings ./memdump.bin | grep HCKR
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define OUTPUT_FILE "memdump.bin"
// If paths are longer than 8000 chars, then... well, this world is probably doomed anyways...
#define BUFF_SIZE 8192
static void init(void) __attribute__((constructor));
static void init(void) {
FILE *map_file, *out;
char exe_path[BUFF_SIZE], page_path[BUFF_SIZE];
char *start, *end;
size_t offset;
// Read program path
int tmp = readlink("/proc/self/exe", exe_path, sizeof(exe_path));
exe_path[tmp] = '\0';
// Read program memory mapping
out = fopen(OUTPUT_FILE, "w");
map_file = fopen("/proc/self/maps", "r");
// Dump map file to stdout
printf("Dumping map file:\n");
size_t n;
do {
char buff[BUFF_SIZE];
n = fread(buff, 1, BUFF_SIZE, map_file);
fwrite(buff, 1, n, stdout);
fflush(stdout);
} while (n == BUFF_SIZE);
fseek(map_file, 0, SEEK_SET); // Don't forget to move back to start...
printf("\n");
// Some crazy scanf format for parsing the map file...
while((tmp = fscanf(map_file, "%p-%p%*s%lx%*s%*s%*[ ]%[^\n ]\n", &start, &end, &offset, (char*)&page_path)) != EOF) {
// Dump only pages with program
if (tmp == 4 && !strcmp(page_path, exe_path)) {
printf("Dumping [%p - %p] from %s at offset %#lx... ", start, end, page_path, offset);
// Try to make it readable...
if (mprotect(start, end - start, PROT_READ) != 0) {
printf("mprotect() failed: %s\n", strerror(errno));
continue;
}
// Write data to file
fseek(out, offset, SEEK_SET);
printf(fwrite(start, end - start, 1, out) == 1 ? "OK\n" : "FAIL\n");
}
}
// We exit the process because why not.
fclose(out);
fclose(map_file);
exit(0);
}