It’s nice to use dynamic analysis to corroborate the findings from static analysis, but what if you face an SQL attack? What if the attack caused the MySQL server to drop an executable file and pass control to it, or if the attack was exploiting a remote code execution vulnerability? I developed a Cuckoo package, misql.py, to allow me to dynamically analyse some of the effects of MySQL attacks.
The Cuckoo Sandbox is a collection of Python scripts (and a DLL) to allow for the automatic execution and dynamic analysis of malware in a controlled environment. I wanted to use Cuckoo to allow me to dynamically analyse the MySQL plugin DLL that was being dropped by a particular MySQL attack.
After extracting the cna12.dll binary from the MySQL attacks and examining it in a debugger to get an idea of what it was going to do, I wanted to perform dynamic analysis to corroborate my static analysis.
The problem with this, however, was that the attack was a series of MySQL commands, and the binary file contained within was a MySQL UDF (User Defined Function) DLL. The functions exported by the DLL are hard to run as standalone functions, as they expect to receive information from the MySQL server.
Wouldn’t it be nice if we could perform dynamic analysis on this DLL to corroborate the static analysis. With this thought in mind, I started work on a MySQL package for the Cuckoo Sandbox (version 0.4.2). This proved somewhat interesting due to an issue with the way Cuckoo works.
There are a few obstacles to overcome if we want to dynamically analyse these MySQL attacks using Cuckoo:
- The binary DLL file is hex encoded within a MySQL command. This needs extracting.
- The functions within the DLL file have to be launched within the context of a MySQL server, as the DLL functions are expecting arguments from the server.
- We need to either find, or start, the MySQL server process, and watch its behaviour while it executes the attack’s SQL commands.
On the plus side, the MySQL commands that form the attack are designed to get this malicious code to run on the server, which is just what we want. Cuckoo is already geared to notice and collect files that the monitored process(es) create. As such, simply feeding the MySQL commands from the attack in to the MySQL server will do most of the work for us.
First, though, we need to start Cuckoo monitoring the MySQL server. We have two options. Leave the MySQL server process running as a service and get our Cuckoo package to find the PID of the mysqld process and watch it, or, disable the MySQL service and get our Cuckoo package to start it manually and watch it. The two options have different implications, the main one being the user which the server process runs as.
If the MySQL server process runs as a service, it runs as the NETWORK SERVICE user, whereas if a Cuckoo package starts the MySQL server process, it runs as the same user as the Cuckoo agent process (in the later versions of Cuckoo Sandbox which use an agent). Also, I found that if I run the MySQL server process as a service, I have to change the NTFS permissions on the plugin directory so that the server (NETWORK SERVICE user) has permission to write to a DUMPFILE in the plugins directory. This is good as it suggests that by default the MySQL server doesn’t have write permissions to its own binary directories, which helps to prevent attacks like this one from succeeding.
Personally, I’m preferring the method of using the Cuckoo package to start the MySQL server process, as that means that the MySQL server isn’t running when using the Cuckoo VM to analyse other files. Plus, if the MySQL server process is not running as the NETWORK SERVICE user, it may have more permissions than if it were, which will potentially allow more attacks to succeed. However it may also taint results if you are using Cuckoo to test whether or not an attack would be successful on your production server. Having said that, it is reasonably easy to switch between the two approaches without having to change my package script, but it does need changes to the guest VM.
The first step is to prepare the Cuckoo VM guest by installing MySQL Server. On Windows XP at least, I had to install both service pack 3 and the .NET Framework before I could install the MySQL server.
The next step was to install the MySQL Connector for Python. This will enable our package to open a MySQL connection to the MySQL server, to execute the MySQL commands from the attack.
Right, let’s script again like we did last summer, and create a Cuckoo package. After figuring out why my import mysql.connector statement kept failing from within my Cuckoo package, despite working otherwise, I decided to call my package misql.py and not mysql.py.
Cuckoo zips up its cuckoo/analyzer/windows/ directory, and sends it to the agent.py script on the guest, which unpacks it to c:\analyzer\. It adds this directory to the Python module search path before the standard Python site-packages directory. If I call my package mysql.py, then the import mysql.connector command fails to find connector, presumably because it is finding c:\analyzer\packages\mysql.py before finding c:\python27\lib\site-packages\mysql\.
My MySQL Cuckoo package is written as per the Cuckoo package documentation.
Some of the more interesting parts of it:
try: import mysql.connector except Exception as e: raise CuckooError("my.py failed to import mysql.connector: " + e.message)
This exception handling code was there while I was trying to figure out why import mysql.connector wasn’t working. I figured it was worth leaving in.
The doqry() method uses connection.cmd_query to send an SQL query, query, over the MySQL connection connection. It then reads each row of the results using connection.get_row(), and logs it to the log file.
The start() method opens a log file, which the package script writes to at various points. This was mainly used so that I could try to figure out why it wasn’t working, as I wasn’t getting any error messages or useful output from Python/Cuckoo. Since my package logs the SQL commands and responses to this log file, I figured it was worth leaving in.
My package will handle either a guest with the MySQL server running as a service, or a guest where my package needs to start the MySQL server by running mysqld.exe. To determine whether the MySQL server is running, it looks for its PID file (to save messing around enumerating processes and the like).
The configuration variables section near the start of the script has various variables which you may want to change, most notably the mysqldbuser and mysqldbpass variables which contain the credentials for the MySQL user account:
### # configuration variables ### mysqldpid = "C:\\Documents and Settings\\All Users\\Application Data\\MySQL\\MySQL Server 5.5\\data\\" + socket.gethostname() + ".pid" mysqldexe = "C:\\Program Files\\MySQL\\MySQL Server 5.5\\bin\\mysqld.exe" mysqldini = "C:\\Documents and Settings\\All Users\\Application Data\\MySQL\\MySQL Server 5.5\\my.ini" mysqldbuser = "" mysqldbpass = "" procmon = "C:\\processmonitor\\procmon.exe" procmonfile = "C:\\cuckoo\\logs\\events" misqllog = "C:\\cuckoo\\logs\\misql.log"
Note the part that either starts (if the pid file wasn’t found or couldn’t be read), or attaches to (if a process ID could be read from the pid file), the MySQL server process:
if (pid == 0): self.logfile.write("Starting MySQL server... ") p = Process() ok = p.execute(path = self.mysqldexe,args = "--defaults-file=\"" + self.mysqldini + "\"",suspended = False) self.logfile.write("%s\n" % ok) else: p = Process(pid = pid)
The Process.execute() method is used with suspended = False. This is because while we actually want to watch the MySQL server process, we want to do so while it is running the SQL commands, not while it is starting up. This is a known good (I hope) process so we don’t need to know what it is doing before we start feeding it the SQL commands. Not suspending the process on creation, and injecting cuckoomon.dll just before running the SQL commands, stops a lot of non-malicious behaviour from being logged.
This is where things got interesting. Cuckoo was failing to generate an analysis report when I set suspended = False, and the cuckoo.py script was giving me the following messages:
WARNING: Log access error for analysis #157: [Errno 2] No such file or directory: '/usr/local/cuckoo-master/storage/analyses/157/logs' [modules.processing.behavior] ERROR: Analysis results folder does not exist at path "/usr/local/cuckoo-master/storage/analyses/157/logs".
I figured that it was cuckoomon.dll that was responsible for creating the log files in c:\cuckoo\logs\ on the guest, which would then be copied across to storage/analyses/<n>/logs/ on the host. Consequently, it was starting to look like the DLL injection wasn’t working as well as cuckoo.py‘s log messages had led me to believe.
I launched Process Explorer (from Sysinternals) and used it to tell me which DLLs mysqld.exe had loaded (press CTRL-D) and sure enough, cuckoomon.dll wasn’t there, not even as a random six letter string (as Process.inject() renames it). I even commented the dll = randomize_dll(dll) line out in process.py, to stop it from randomising the DLL name — still no luck. It was loading if I ran Process.execute() with suspended = True, but not with suspended = False.
Eventually, I booted the guest VM outside of Cuckoo so that I could play with it without Cuckoo shutting it down. I was just going to try to do a straight DLL injection, as I couldn’t see why it shouldn’t work. The difference here though, was that I was using a different DLL which I had just copied to a nice short path of c:\, so now I had to call Process.inject() with dll = … to specify a DLL other than the default dll\cuckoomon.dll string that it uses, and I still had the dll = randomize_dll(dll) line commented out. That worked.
I’m wondering what the difference was, then it twigged — I’d started using an absolute path instead of a relative one. So I tried specifying dll = c:\analyzer\dll\cuckoomon.dll and that also worked. Why? It should have been reasonably obvious thinking about it afterwards.
Using the CreateRemoteThread() method of DLL injection, the path to the DLL is passed to the LoadLibrary() Win32 API function. Checking which directories LoadLibrary() uses to search for DLLs led me to Dynamic-Link Library Search Order (Windows):
If SafeDllSearchMode is enabled [which is the default on the version of Windows I was using], the search order is as follows: 1. The directory from which the application loaded. 2. The system directory. Use the GetSystemDirectory function to get the path of this directory. 3. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched. 4. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory. 5. The current directory. 6. The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.
If Cuckoo starts the process in a suspended state, then it queues an Asynchronous Procedure Call to run LoadLibrary(). Otherwise it uses CreateRemoteThread() to run LoadLibrary(). The queued Asynchronous Procedure Call will presumably run pretty shortly after the process is resumed, while the current working directory is still c:\analyzer\.
If, however, the process is not started in a suspended state, then it is free to change its working directory to something else before Cuckoo runs the CreateRemoteThread() call, and if it does then LoadLibrary() can no longer find dll\cuckoomon.dll.
You can’t just call Process.inject(dll = ‘c:\\analyzer\\dll\\cuckoomon.dll’) instead, because randomize_dll() turns it back in to a relative path again by setting the new DLL path to dll\<sixrandomletters>.dll.
At this point I modified the Process.inject() method in cuckoo/analyzer/windows/lib/api/process.py, and changed the following lines:
new_dll_path = os.path.join("dll", "%s.dll" % new_dll_name) ... def inject(self, dll=os.path.join("dll", "cuckoomon.dll"), apc=False):
to
new_dll_path = "c:\\analyzer\\dll\\%s.dll" % new_dll_name ... def inject(self, dll="c:\\analyzer\\dll\\cuckoomon.dll", apc=False):
accepting that I had just removed the operating system portability afforded by the use of os.path.join() and presumably by the use of a relative DLL path.
So that is the fun that I got up to when developing my MySQL package for Cuckoo. I tested it with the newer ‘autocommit’ attacks that I’ve seen and it seemed to go ok (as long as the MySQL server has permission to write to its plugin directory).
One of the older MySQL attacks that I’ve captured, didn’t work so well. It looks like it is targetting a pre-5 version of MySQL, as it tries to specify a full path to the DLL in the CREATE FUNCTION MySQL statement, and as of version 5 of MySQL that is not allowed, as can be seen in misql.py‘s log file:
Q: create function cmdshelv returns string soname 'C:\\WINDOWS\\system32\\amd.dll' DatabaseError: 1124 (HY000): No paths allowed for shared library
I tweaked the SQL a bit to overcome this problem, as I couldn’t find version 4 of MySQL to download (at least not from a reputable looking site). That seemed to work, although highlighted another issue with Cuckoo — it fails to capture dropped files if they are deleted before the end of the analysis.
I am keen for other analysts to use my Cuckoo package, misql.py, as I have only been able to test it with two different MySQL attacks. I’m hoping the recently disclosed MySQL vulnerabilities CVE-2012-5611 through CVE-2012-5615 will result in me receiving some new test material. Bugs, suggestions, and general feedback are welcome, and can be provided via comments.
Note that I am currently experiencing issues using misql.py with Cuckoo v0.5, in that I am not getting the results that I’m expecting.
I will work on troubleshooting this as version 0.5 introduces some nice features which I’d like to have a play with.