© Noiche

char nick[] = "N0iche";

printf("https://www.root-me.org/%s\n", nick);

printf("%s\x40protonmail\x2ecom\n", nick);

[FCSC2020] - PWN - Why not a Sandbox ?

May 4, 2020 • Write-Up,FCSC-2020,PWN

Challenge details :

CTF Challenge Category Value Solves
FCSC2020 Why not a sandbox PWN 474 50

Description

Your goal is to call the print_flag function in order to show the flag
Service : nc challenges1.france-cybersecurity-challenge.fr 4005

TL;DR

That challenge was an ELF binary running a python jail, using Audit Hook to prevent us from escaping.

The print_flag function was part of the lib_flag.so included in the jail.

We had to leak the binary so we could examine the GOT.PLT section and find an entry for the welcome

Since welcome was also in the lib_flag.so library, we could find our way to the print_flag function.

Let’s get a shell ;)

I started this challenge with some basic fuzzing, trying to import some modules or to call the print_flag function :

enum

I noticed something very interesting to me in that output : Audit Hook.

Why ? Because I didn’t know anything about the so called Audit Hook, so I searched for some documentation and here is was I found :

https://www.python.org/dev/peps/pep-0578/

The Why not a Sandbox section caught my eyes, since the challenge had exactly the same name, so I digged deeper :

https://python-security.readthedocs.io/security.html#sandbox

If you read these articles, you will find mentions to CPython which is basically the default python interpreter.

To keep it simple, actions taken by the python runtime are visible to auditing tools thanks to Cpython and thanks to the python API.

Audit Hook is the feature that makes it possible, and it also makes it possible to trigger actions based on events.

But that feature is not meant to be used for sandboxing, as you can read in the first link I shared :

This proposal does not attempt to restrict functionality, but simply exposes the fact that the functionality is being used.
[…]
Since audit hooks have the ability to safely prevent an operation occurring, this feature does enable the ability to provide some level of sandboxing. In most cases, however, the intention is to enable logging rather than creating a sandbox.

Well, it looked like the author of the challenge decided to create a sandbox using Audit Hook anyway :)

I searched for documentation again, and I saw that ctypes was often used to bypass that kind of sandbox.

Since import os was forbidden due to a hook on the os import event, I decided to find os somewhere in order to get a shell.

I ended up with 2 options :

ctypes._os.popen("bash command").read()

and :

ctypes._os.execl("/bin/bash", "-i").read()

The second one provided me an interactive bash, and tadaaa :

ctypes_shell

Finding the way to the flag

As you could see on the previous screenshot, there was 2 files in the directory : spython and lib_flag.so

At this point, I noticed that spython was the jail and that a shared object library was here, but I wasn’t able to read it, damn :/

Running ldd spython told me that the library was loaded in spython (note that spython was actually an ELF embedding python code)

Based on the name of the library (lib_flag), I thought that the print_flag function was probably part of the lib.

Since ctypes provide C data type implementation and the ability to load DLL and shared object library, here is what I tried :

ctypes_cdll

But unfortunatly, as you can see, that was also forbidden due to a hook…

After some hours reading documentation and searching into ctypes using the dir command, I finally decided to get the binary so then I could analyse it using radare2 (I encoded it using base64 so i could just copy past it on my machine and decode it).

Because the binary was loading some shared objects, I took a look at the GOT/PLT sections, and noticed that there was an entry in the GOT.PLT section for a function named welcome… That function wasn’t common and seemed to be a part of the lib_flag library:

welcome_in_GOT-PLT

As you can see, the address of the welcome entry in the GOT section was 0x40a8. But that’s a relative address, an offset, since the binary wasn’t mapped into memory yet. That would be a good start to call the welcome function, uh ?

Here is the idea i had in mind :

  1. Find the base address of the spython binary mapped into binary
  2. Create a int_64 pointer on base_addr + welcome_GOTentry_offset using ctypes
  3. Dereference that pointer to get the welcome function address.
  4. Create a c-function object using the address and call it.

In order to understand, we have to know what is the GOT/PLT, and how to wrap C code with python using ctypes

So here is a reminder :

PLT stands for Procedure Linkage Table which is, put simply, used to call external functions whose address isn’t known in the time of linking, and will be resolved by the dynamic linker at runtime.

GOT stands for Global Offsets Table and is also used to resolve addresses. When an address as already been resolved, it’s stored directly in the GOT.PLT section

ctypes provide the ctypes.c_int64 that can be used with the ctypes.POINTER constructor. It also provide the ctypes.CFUNCTYPE function that can be used to wrap c function.

Last trick : the basee address can be found in /proc/self/maps… BUT the open function was caught by a hook… Hopefully, ctypes struck again ! Here is the way I opened /proc/self/maps and got the base_addr + welcome_GOTentry_offset :

gotplt = int(ctypes.__loader__.get_data("../../../../proc/self/maps").decode().split("-")[0], 16) + 0x40a8

Then, it was possible to cast that address into a int64 pointer:

p = ctypes.cast(gotplt, ctypes.POINTER(ctypes.c_int64))

And bam, I was ready to call the function using :

ctypes.CFUNCTYPE(ctypes.c_void_p)(p.contents.value)()

I tried… and… It worked just as expected :D

welcome_call

OK, OK, but the function I wanted to call at the very beginning was print_flag

The hard part was done at this point, I decided to bruteforce the offset between the two functions, and finally got the precious flag !

The offset was 0x14, here is the final payload (oneliner) :

ctypes.CFUNCTYPE(ctypes.c_void_p)(ctypes.cast(int(ctypes.__loader__.get_data("../../../../proc/self/maps").decode().split("-")[0], 16) + 0x40a8, ctypes.POINTER(ctypes.c_int64)).contents.value+0x14)()

print_flag_call

Note : It was also possible to find the flag using memory leak with ctypes

I hope you enjoyed !

FCSC{55660e5c9e048d988917e2922eb1130063ebc1030db025a81fd04bda75bab1c3}