pwn
| ⭐ ⭐ ⭐ | XeR
| 2 solves
🩸
Description
Entre les ACL et les LSM, on ne peut plus faire confiance aux permissions Unix.
Voici un programme qui essaye d’ouvrir un fichier de votre choix pour pallier ce problème.
Attachments
Dockerfile
,docker-compose.yml
public/file-checker
public/ld-2.39.so
,public/libc-2.39.do
src/file-checker.c
,src/Makefile
TL;DR
The double free vulnerability could be abused to perform House of Muney
to corrupt already mmaped pages (loaded libraries, mmaped files, …) with nearly arbitrary content. I used this to corrupt libc
’s .dynsym
section so that next loaded libraries will get their GOT corrupted. The House of Muney
mechanism can also be used to corrupt the gconv-modules.cache
mmaped file which is loaded when calling fopen
with a mode including ccs=
. The OOB within the modes
array allowed to specify arbitrary fopen
mode. Putting the gconv-modules.cache
overwrite and the arbitrary fopen
mode
argument allows the loading of any libraries on the system within the process address space. I finally targeted a library with a specific constructor to call a manually found one gadget.
File Checker
was a hard (more like really hard) difficulty pwn challenge written by XeR
. It looked like a classic glibc heap menu-based challenge at first, but it turned into an incredibly hard and interesting challenge (probably the most interesting I’ve ever done). I learned many things by solving it and had the joy to first blood it.
Source code analysis Link to heading
The source code for this challenge was provided which make the analysis easier.
Let’s start with the main
function :
int main(void)
{
// Ubuntu's libc is not relro
// Baddies use that to hijack pointers to e.g. strlen *in the libc*
// So we add an additional layer of protection here
if(NULL == getenv("LD_BIND_NOW")) {
fprintf(stderr, "LD_BIND_NOW is not set!\n");
return EXIT_FAILURE;
}
setbuf(stdout, NULL);
while(1) {
menu();
size_t choice;
if(!getInt(&choice)) {
fprintf(stderr, "Error: could not read integer\n");
return EXIT_FAILURE;
}
choice--;
static void (*const f[])(void) = {
prepare,
clean,
handle,
};
const size_t count = sizeof(f) / sizeof(*f);
if(count == choice)
return EXIT_SUCCESS;
if(choice < count)
f[choice]();
}
}
There is a classic menu structure that allows us to do 4 actions which will be detailed next :
Prepare a file
Clean a file
Handle a file
Exit
There are some interesting things to note :
First, the program is checking the presence of the LD_BIND_NOW
environment variable. Here is an explanation from the manual :
LD_BIND_NOW (since glibc 2.1.1)
If set to a nonempty string, causes the dynamic linker to resolve all symbols at program startup instead of deferring function call resolution to the point when they are first referenced. This is useful when using a debugger.
That means that even with the binary / libc compiled asPartial RelRO
, all the symbols would be resolved at startup (no more lazy loading). As the comment in the code suggests, this might be here to prevent direct libc GOT overwriting.
Next, a function array is used to call the matching function this usually is a good target in binary exploitation as it can provide a quick way to gain PC control. However, it is defined with a const
keyword, placing it inside a read only section.
The getInt
function is a simple wrapper that converts a string provided by the user to a 64-bit unsigned integer using scanf
:
__attribute__((nonnull, access(write_only, 1)))
static bool getInt(size_t *n)
{
return 1 == scanf("%lu", n);
}
Prepare a file Link to heading
Here is the code of the prepare
function :
static void prepare(void)
{
size_t index;
if(!getIndex(&index))
return;
size_t size;
printf("size: ");
if(!getInt(&size)) {
fprintf(stderr, "Could not read size\n");
return;
}
char *buffer = malloc(size + 1);
if(NULL == buffer) {
perror("malloc");
return;
}
memset(buffer, 0, size + 1);
// drop the newline
fgetc(stdin);
printf("file name: ");
if(NULL == fgets(buffer, size + 1, stdin)) {
perror("fgets");
free(buffer);
return;
}
buffer[strcspn(buffer, "\n")] = 0;
files[index] = buffer;
}
This function allows the user to allocate a filename with an arbitrary size and fill it with controlled data using fgets
. It finally stores the allocated buffer address at an arbitrary index inside the files
array which is defined as the following :
static char *files[5];
Here, the getIndex
function is used to get the filename index. It is a simple wrapper of getInt
, with some checks to prevent placing the filename pointer out of the bounds of the files
array :
__attribute__((nonnull, access(write_only, 1)))
static bool getIndex(size_t *n)
{
const size_t count = sizeof(files) / sizeof(*files);
size_t index;
printf("index: ");
if(!getInt(&index)) {
fprintf(stderr, "Could not read index\n");
return false;
}
if(index >= count) {
fprintf(stderr, "Index out of bounds\n");
return false;
}
*n = index;
return true;
}
Finally there is some things to note down :
-
The allocated filename is nulled-out just after allocating it using
memset(buffer, 0, size + 1)
- This would make the leaking of data harder
-
The input is retrieved using
fgets
which stops at a newline (\n
) character -
The terminating newline is replaced with a null byte
- No off by null here,
strcspn
returns the position of the first\n
occurence.
- No off by null here,
Clean a file Link to heading
Here is the code of the clean
function :
static void clean(void)
{
size_t index;
if(!getIndex(&index))
return;
free(files[index]);
}
It simply frees the filename at the provided offset (still using the getIndex
function).
Prepare a file Link to heading
This is the most interesting function of the program :
static void handle(void)
{
size_t index;
if(!getIndex(&index))
return;
static const char *const modes[] = {
"r",
"r+",
"a"
};
printf("Mode:\n");
puts("1. read-only");
puts("2. read + write");
puts("3. read + write + create + append"); // useful for dirs
size_t mode;
if(!getInt(&mode))
return;
// Open the file with the specified mode
FILE *fp = fopen(files[index], modes[mode - 1]);
if(NULL == fp)
return perror("fopen");
if(0 != fclose(fp))
return perror("fclose");
puts("Permission check passed!");
}
It asks an index and a mode from the user and try to open the corresponding file with a mode inside the modes
array. Note that nothing more is done between the opening of the file and its closing.
Bug Hunting Link to heading
Now let’s hunt for bugs !
You probably spotted the obvious Double Free
vulnerability inside the clean
function by reading the first section :
static void clean(void)
{
size_t index;
if(!getIndex(&index))
return;
free(files[index]);
}
The files[index]
entry is not set to NULL
after freeing it, allowing the free of already freed filename.
The second bug lies in the prepare
function. A wrapper function is correctly used to get the index
of the file to handle. However for the mode
, getInt
is used which doesn’t apply any checks to the provided value. This leads to OOB access in the modes
array.
The modes
array is declared as static const
, it will be located inside a global section of the binary (the same as the actions array). As the files
array is global as well, the OOB can be used to make the mode
point to a files
entry such as modes[offset] == &files[0]
. This allows full control over the fopen
’s mode
parameter.
No other bugs were spotted in this program, only these two bugs might be required to gain RCE on the remote instance.
As these bugs were very easy to find, I guess the most complicated part of this challenge will be to exploit them to gain RCE.
House Of Fail Link to heading
Usually, double free
bugs are very powerful and can be quickly transformed into a strong primitive such as arbitrary write and so on…
However there are some constraints here :
- The version of the libc used in this challenge is
2.39
. Since version2.29
a check was added to prevent double frees inside thetcache
bins.- Double frees might still be exploitable inside
fastbins
, but there are only 5files
slots available. To obtain a fastbin free chunk, the correspondingtcache
freelist must have at least 7 entries.
- Double frees might still be exploitable inside
- There is no obvious way to obtain a memory leak (libc, heap, …)
- Partial overwrites could be a solution, but safe linking (introduced in libc 2.34) prevents partial overwrites on
tcache
bins andfastbins
, and force the attacker to obtain a leak before corrupting anything. - Nowadays some attacks (House of Rust, House of Water, …) have been developed to obtain an arbitrary write without leaks. However the requirements for these techniques are too high for this challenge.
- Partial overwrites could be a solution, but safe linking (introduced in libc 2.34) prevents partial overwrites on
As mentioned in the handle
function code, fopen
is used to open an arbitrary file. Internally fopen
allocates a FILE
structure on the heap. This structure is also known to be a great target to construct strong primitives (Arbitrary [Read / Write / Code Execution]).
However, there is nothing we can do during the lifetime of these FILE
objects as the file is closed just after the opening.
Seems like there is not so much classic glibc heap tricks we can do here… And by solving the challenge this way, the OOB vulnerability inside the modes
array would be totally useless, which is kinda unusual for a CTF challenge.
House of Muney Link to heading
While I was thinking about crazy heap techniques I could do to exploit the double free vulnerability, I just noticed that we could actually allocate a chunk of **any size **, that means that mmaped
chunks are also in-scope !
In the ptmalloc2
algorithm, when calling malloc
with a big size (> mp_.mmap_threshold
), the allocated chunk won’t be allocated within the main_arena
, heremmap
is actually called to obtain memory for the new chunk. In the same way, when freeing an mmaped
chunk, munmap
is called to release the chunk memory.
The exploitation of mmaped
chunks is less documented. However some techniques exists ! The first documented exploit against mmaped
chunks has been done by Qualys
while exploiting a vulnerability in QMail. Since, Maxwell Dulin documented a generic technique that take advantage of these mmaped
chunks called House of Muney
. The article is very well written, I encourage you to read it to understand more easily the next parts of this writeup.
The principle is to corrupt the header of an in-use mmaped
chunk with an arbitrary size, and then calling free
to munmap
a greater size than the original chunk, causing the unmapping of other memory regions ! These unmapped regions can then be remapped by calling mmap
once again.
This is a beautiful technique which is leakless and allows the overwrite of read-only pages ! This sounds perfect for our needs.
This technique seems clearly to be the one to use in this context, however the original House of Muney exploits the fact that the used libc
relies on lazy loading (Partial RelRO
) to corrupt the libc
GOT at runtime. It is not the case here due to the check done in the main
function, ensuring the presence of the LD_BIND_NOW
environment variable, which resolves at startup the address of every symbols.
Arbitrary fopen
mode
Link to heading
Now that we have a good idea for exploiting the double free vulnerability, let’s focus on the fopen
’s mode OOB
.
The first thing I did was opening the fopen
manual and reading the definition of the mode
parameter. There was nothing really interesting until I stumbled across this paragraph :
In addition to the above characters, fopen() and freopen() support the following syntax in mode:
,ccs=string
The given string is taken as the name of a coded character set
and the stream is marked as wide-oriented. Thereafter, internal
conversion functions convert I/O to and from the character set
string. If the ,ccs=string syntax is not specified, then the
wide-orientation of the stream is determined by the first file
operation. If that operation is a wide-character operation, the
stream is marked wide-oriented, and functions to convert to the
coded character set are loaded.
This was something I didn’t know about, so I quickly googled fopen mode ccs exploit
, and I luckily found this great article by hugeh0ge
: Getting Arbitrary Code Execution from fopen’s 2nd Argument. This seems to exactly fit to the challenge !
I won’t detail the technique in this writeup as hugeh0ge already did in his article. But the key points are :
- Controlling the
GCONV_PATH
environment variable the modification of the defaultgconv-modules
file, which is used to linkccs
module names to shared libraries on the system - An attacker controlled library is then loaded by specifying a custom
ccs
encoding (provided inside the evilgconv-modules
)- The
gconv_init
functions is called by thelibc
when after loading the library usingdlopen
system("/bin/sh")
is executed
- The
I was so excited when I discovered this article ! However there are still some problems… (else it wouldn’t be fun !)
The scenario presented in the article supposes that the attacker already has a local access to the system in order to write files, which we don’t have.
Moreover, this technique relies on the overwriting of an environment variable, in our case this would require an arbitrary write (or better, an arbitrary call to putenv
). With an arbitrary write at our disposal, this would already been game over as there are more interesting things to target inside the libc
to gain arbitrary code execution.
At this point I decided to look deeper inside the libc
code, especially how the handling of GCONV modules works.
GCONV modules cache Link to heading
I started to look where the GCONV_PATH
was used. The __gconv_load_cache
function is checking that the GCONV_PATH
environment variable is not set. If it is not set (the usual case) it starts the loading of the default gconv-modules.cache
file (located at /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
). This file is a compiled version of the ASCII gconv-modules
, and is used to be more efficient during the parsing of a ccs
parameter.
Here is a simplified snippet of the relevant code inside the libc :
int
__gconv_load_cache (void)
{
int fd;
struct __stat64_t64 st;
struct gconvcache_header *header;
/* We cannot use the cache if the GCONV_PATH environment variable is
set. */
__gconv_path_envvar = getenv ("GCONV_PATH");
if (__gconv_path_envvar != NULL)
return -1;
/* See whether the cache file exists. */
fd = __open_nocancel (GCONV_MODULES_CACHE, O_RDONLY | O_CLOEXEC, 0);
if (__builtin_expect (fd, 0) == -1)
/* Not available. */
return -1;
// ...
gconv_cache = __mmap (NULL, cache_size, PROT_READ, MAP_SHARED, fd, 0);
// ...
return 0;
}
This file is actually mapped into memory using mmap
! Does that remind you of anything ?
The principle is to corrupt the header of an in-use
mmaped
chunk with an arbitrary size, and then callingfree
tomunmap
a greater size than the original chunk, causing the unmapping of other memory regions ! These unmapped regions can then be remapped by callingmmap
once again.
Exactly ! We can abuse the mechanism of the House of Muney technique to replace the mmaped gconv-modules.cache
file by our own !
To compile my own gconv-modules.cache
file, I just used the iconvconfig
utility that takes a gconv-module
ASCII file and converts it to a valid gconv-modules.cache
file :
./iconvconfig --nostdlib --prefix='/' -o gconv-config.cache ./gconv-modules/
Basically this allows us to load any library inside the process address space, for example with the following gconv-module
file, we can load dynamically /usr/lib/x86_64-linux-gnu/libfoo.so
by calling fopen(filename, "r,ccs=pwn")
module PWN// INTERNAL usr/lib/x86_64-linux-gnu/libfoo.so 1
module INTERNAL PWN// usr/lib/x86_64-linux-gnu/libfoo.so 1
You can read the find_module
(iconv/gconv_cache.c
) / __gconv_find_shlib
(iconv/gconv_dl.c
) in the libc sources to have a better understanding of the dynamic loading of gconv modules.
At this point, I thought this was game over. By making the pwn
module pointing to /proc/self/fd/0
, I thought I could just send my fake gconv library over stdin
and a shell would pop ! But instead, I’ve got nothing but an error message from fopen
… After investigating, it seems that /proc/self/fd/0
can’t be opened if stdin
is a socket. As the remote challenge is using socat that forwards standard I/Os to socket, this might explain this. However I did not success to do this locally neither…
From that, I just fell into depression and alcoholism had some rest hoping that a brillant idea would come to me during the night.
House of Muney - Revenge Link to heading
Now, we’ve got an interesting primitive : arbitrary dlopen
, the only constraint is that the library must be already on the system.
I ran out of ideas for a while, but then I started opening the available GCONV libraries available on the provided container. All of these libraries were very similar in their structure, and the imported functions were almost always the same (to name them : malloc
, free
, strncasecmp
, strlen
). This was the moment I realized “Wait, these functions are imported from the libc ! How their address is resolved ?”.
After reading some code from the loader, the answer was “the same way as any program, by leveraging the .strtab
and .dynsym
ELF sections”.
The difference is that now, we can control when these libraries are loaded ! If we manage to apply House of Muney to corrupt libc
.dynsym
section, and then load a library, its GOT will be successfully corrupted with controlled data ! All of this by keeping the advantages of the House of Muney : leakless, data-only technique.
Now, another question arises : “How code inside the libraries is actually executed ?”
The answer is inside the libc and loader code ! During the loading of a gconv
module library, the following symbols are resolved :
gconv_init
: The initialization function of a gconv modulegconv
: The actual function used to encode / decode datagconv_end
: De-initialization function of a gconv module
When loading a module, the gconv_init
function is called (following the fopen
call). When a module is unloaded (by calling fclose
), the gconv_end
function is called. These functions actually use the imported functions listed above, which is perfect for us !
Another way to execute the code located inside the library during its loading / unloading are constructors
/ destructors
which are present inside the .init_array
/ .fini_array
sections of an ELF.
As the strncasecmp
function was called using a partially controlled string as the first parameter, I tried to modify its GOT entry by system
. However, strncasecmp
is defined as an IFUNC
from the libc symbols. This mean that this function will be called once by the loader to determine what function should be used (this is used to take a specific implementation of a function, in this case, there are many implementations of strncasecmp
which are system specific). So system
would only be called during the symbol resolution by the loader with an uncontrolled parameter.
This was the only candidate I found along with strlen
which was also an IFUNC
symbol. So my last chance was to find a one gadget that would execute a shell when calling a corrupted function.
After trying many one gadgets the reference one gadget tool found, I couldn’t find one which met every requirements.
At that moment I was a little desperate, and tried to read as much resource as I could, in order to find another idea… I ended up reading this beautiful article from Qualys where they abused the loading of libraries located under the /usr/lib*
path inside the ssh-agent
process to get remote code execution. Their approach is very interesting as they target legitimate libraries constructors to do some black magic (I won’t spoil the article :D read it !). This inspired me, so I started analyzing every library available inside the provided Docker
container, in order to find a magic constructor !
There were not as many available libraries as in the article, but I ended up finding something interesting !
The library /usr/lib/x86_64-linux-gnu/libdrop_ambient.so.0.0.0
has 2 constructors, one of them is a user-defined function init
, its code is very simple :
00001060 f30f1efa endbr64
00001064 4531c0 xor r8d, r8d {0x0}
00001067 31c9 xor ecx, ecx {0x0}
00001069 31d2 xor edx, edx {0x0}
0000106b be04000000 mov esi, 0x4
00001070 bf2f000000 mov edi, 0x2f
00001075 31c0 xor eax, eax {0x0}
00001077 e9d4ffffff jmp prctl
But there is something special here :
- Many registers are set to
0
:r8
,rcx
,rdx
- A function imported from the
libc
is called
That seems promising for a one gadget call ! Unfortunately, the conditions required for the one gadgets found by one-gadget
were not met… But at this point I was convinced I could do something from this.
Finding a one gadget Link to heading
The last step of this challenge was just finding a perfect one gadget, I just started to load the libc
inside Binary Ninja
and searched for cross references to /bin/sh
. Surprisingly, there were some functions that were not present in the results of the one-gadget
tool. After spending some time I finally found the perfect one :
000ef52b lea r11, [rel data_1cb42f] {"/bin/sh"}
000ef532 lea r10, [rbp-0x50 {var_58}]
000ef536 mov qword [rbp-0x50 {var_58}], r11 {data_1cb42f, "/bin/sh"}
000ef53a mov qword [rbp-0x48 {var_50_1}], rax ; r10 point to { /bin/sh", NULL }
000ef53e jmp 0xef431
...
000ef431 mov qword [r10+0x10], 0x0
000ef439 mov rdx, qword [rbp-0x78 {envp_1}] ; rdx = [rbp - 0x78] = 0
000ef43d mov rsi, r10 ; rsi = {"/bin/sh", NULL}
000ef440 mov rdi, r11 ; rdi = "/bin/sh"
000ef443 call __GI___execve ; execve("/bin/sh", {"/bin/sh", NULL}, NULL)
And we finally get our well deserved shell !
[*] Switching to interactive mode
$ id && cat flag.txt
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
FCSC{93aa742b341b591bb4a6cad5c1b9c63ba382ec6f8dd373ca82fd7c777443fe44}
Conclusion Link to heading
File Checker
was a wonderful challenge that made me learn a ton of things. As Super Factorizer
(another challenge of the FCSC2024 made by XeR), it’s amazing how data-only exploit can be used when classic memory corruption exploits are not usable. Congratulations to XeR for creating this challenge I really loved it !