Now that we've found a malicious artifact, the next step is to understand what it's doing. Identify the file that would be created when the malicious code is run. Additionally, identify some characteristics of the communications between the malicious artifact and the LP.
Enter the IP of the LP that the malicious artifact sends data to
Enter the public key of the LP (hex encoded)
Enter the version number reported by the malware
- make - Malicious
make
binary - Dockerfile - Dockerfile to set up debugger
- pub.hex - Public key hexdump
- solution.txt - Solution
In this task we need to reverse the make
binary from the last task. I'll be using Ghidra for static analyis and GDB with GEF for dynamic analysis.
The first thing we need to do is copy the malicious make
binary from the image. You can do this by starting the image then copying from the container's filesystem:
$ docker run --detach --rm -it panic-nightly-test:latest bash
b617744dd629e8208738b255bb76ebdb2770382fd7d453788c59076e783254f8
$ docker cp b617744dd629e8208738b255bb76ebdb2770382fd7d453788c59076e783254f8:/usr/bin/make ./make
Let's see what kind of information this binary has for us:
$ file make
make: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, with debug_info, not stripped
This is great news. It means the binary still contains debugging symbols. If we're lucky, function names and comments will still be there!
Let's load this into Ghidra:
We found main()
, which is good, but it does look like some obfuscation has been applied to the function names. Hopefully that doesn't cause us too much trouble. We can use the l
key to rename functions and variables as we go.
main()
calls that gitGrabber()
function, then seems to get a string from the obfuscated qozheyshphmrq()
function which is fed into execvp()
. Knowing what we know about how this binary works, the string passed to execvp()
is probably ninja
. We can confirm this with dynamic analysis!
I tried running the make
binary in a Linux VM, but it failed. We could try to figure out why that is or we could just setup our dynamic analysis environment by extending the container we have from task 5 with a Dockerfile
:
FROM panic-nightly-test:latest
# install git, gdb, and curl
RUN apk add git gdb curl
# install gef
RUN bash -c "$(curl -fsSL http://gef.blah.cat/sh)"
CMD ["gdb", "make"]
Then we can build it with:
$ docker build -t panic-reversing .
[+] Building 0.1s (7/7) FINISHED
...
And finally run it with:
$ docker --rm -it panic-reversing
GNU gdb (GDB) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-alpine-linux-musl".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
91 commands loaded for GDB 10.1 using Python engine 3.8
[*] 5 commands could not be loaded, run `gef missing` to know why.
Reading symbols from make...
gef➤
Perfect! Now we should be able to set a breakpoint at main()
then call the obfuscated function to confirm our suspicions:
gef➤ b main
Breakpoint 1 at 0xa45c: file gitGrabber.cpp, line 239.
gef➤ run
...
gef➤ call qozheyshphmrq(0xe)
$1 = 0x7fcee99c6954 "ninja"
Nice! We were right! Looks like all of the malicious stuff will be in gitGrabber()
so it's time to start reversing!
Documenting my entire reversing process would take forever, so I'll just share a few tips:
- Rename everything. As soon as you learn a little bit about what something does, rename it.
- Check for strings. If you're stuck, try looking for strings in the Defined Strings window and clicking on the references to those strings.
- Comment everything. It helps to leave notes for yourself throughout the process. Your function and variable names might not be enough to remind you of what you learned in previous reversing sessions.
- Confirm. If you suspect something based on the decompilation or debugger output, try to confirm it. Just like we did with the
execvp('ninja')
call.
The first thing we need is the IP that this binary attempts to connect to. I tried running the binary with Wireshark listening but didn't see anything.
Let's see if we can find out where the network calls are.
Note: You can highlight all instances of a variable name in Ghidra by right-clicking the variable and picking one of the Highlight options
The binary contains references to the function send()
. All calls to send()
happen in the obfuscated function gskzpmrhehvre(int sock, void *vbuf, size_t bufLen)
. We can find the socket address by finding where the sock
argument comes from.
This leads us to the assignment of sock
from the call to ddxepwqhfwlfp(ip, port)
. Now we follow the ip
variable back to its assignment.
The ip
variable comes from a call to qozheyshphmrq(0x13)
, the same function that gave us our ninja
string. This function has variable names like ciphertext
and nonce
, so instead of messing with that we'll just call the function in GDB:
gef➤ call qozheyshphmrq(0x13)
$1 = 0x7f23b59e5954 "198.51.100.77"
We found the IP! But why didn't Wireshark show us anything? Recall that in Task 5, our strace
output showed the binary attempting to read the directory /usr/local/src/repo
but it didn't exist. What if we cloned a different git repository there by editing the Dockerfile?
RUN git clone https://github.com/hugsy/gef repo
Rebuilding and running make
while listening with Wireshark shows us this:
That worked! We can see that the binary is now trying to connect to the IP we found on port 6666.
We could take a similar approach here: search for the string "key" or "public", follow the references, and we'd eventually end up with the public key. However, that doesn't teach us much about the structure of the program. Instead we can try figuring out what's going on starting at gitGrabber()
.
I walked through some of the obfuscated functions and decoded strings until I thought I had a pretty good idea of what was going on. Here's what I ended up with:
Most of the interesting stuff and the functionality most likely to contain keys is in what's now called upload_to_lp()
(formerly piayygmoklpek()
).
upload_to_lp()
looks long and complicated, but thankfully we have many clear function and variable names. We're looking for the server's public key, so we should be on the lookout for any cryptographic calls.
While going through the obfuscated function calls in upload_to_lp()
, right at the top of enxlhwgevwtay(int sock string *fp)
, I noticed that there are several public key related variables like pubKey, public_key, and client_public
. pubKey
is the first one to be assigned with a call to get_string(0x12)
. Let's hop over to GDB to get that string:
gef➤ call qozheyshphmrq(0x12)
$2 = 0x7f43678f69b4 "3)\302\021\277\023\016\221\222\254'\343\271\323.\200b\252\203voM\a.ڼT\bjWKs"
Hm, this is probably our key, but not in the format we want. Right after the string is fetched we can see that pubKey
is placed into public_key
and has a length of 32 bytes. To get this in hex, we can use GEF's hexdump
command and then strip out the junk we don't need:
gef➤ hexdump byte 0x7f43678f69b4 --size 32
0x00007f43678f69b4 33 29 c2 11 bf 13 0e 91 92 ac 27 e3 b9 d3 2e 80 3)........'.....
0x00007f43678f69c4 62 aa 83 76 6f 4d 07 2e da bc 54 08 6a 57 4b 73 b..voM....T.jWKs
This output is copied and pasted into pub.hex
. Then in vim
, I copied out the middle section and removed the spaces/newlines to get our submission string:
3329c211bf130e9192ac27e3b9d32e8062aa83766f4d072edabc54086a574b73
We've already seen the version number. It's retrieved at the top of upload_to_lp()
with a call to get_string(0x11)
. Fetching the string in GDB gives us:
gef➤ call qozheyshphmrq(0x11)
$1 = 0x7f4928774954 "1.2.2.0-EAA"
We should have the three components we need for submission!