How SQL Server hackers cover their tracks

Learn how a SQL Server attacker can cover their tracks in this excerpt from "The Database Hacker's Handbook: Defending Database Servers" by David Litchfield, Chris Anley, John Heasman and Bill Grindlay.

The following excerpt, courtesy of Wiley Publishing, is from Chapter 22 of the book "The Database Hacker's Handbook: Defending Database Servers" written by David Litchfield, Chris Anley, John Heasman and Bill Grindlay. Click for the complete book excerpt series or purchase the book.

Covering tracks

Once an attacker has broken into a SQL Server, his efforts will turn to both ensuring that his intrusion is not detected and to making future attacks easier. The first goal is achieved by deleting access log entries and minimizing obvious changes to data; the second is commonly accomplished by means of subtle changes to the database software and structure that remove security checks, known as backdoors. This section describes techniques used to compromise a SQL Server's security controls and also details detection and defense methods.

Three-Byte Patch

Perhaps the subtlest of SQL Server backdoors is the three-byte patch as described by Chris Anley in his whitepaper "Violating Database-Enforced Security Mechanisms".

This method utilizes an existing attack vector, such as a buffer overflow exploit, to patch the SQL Server process in memory — an approach known as runtime patching. When patching bytes in memory the Windows SDK function VirtualProtect() must first be called on the region in order to mark it as writable. To determine the bytes to patch, a debugger, such as the one included with Microsoft Visual C++ .NET, is attached to the sqlservr.exe process. After logging on to the SQL Server as a low-privileged user using Microsoft Query Analyzer, a query attempting to access a prohibited table is executed:

select * from sysxlogins

By default only members of the dbo database administrators group can view this table, which contains usernames and password hashes for all database users. Running this query causes the SQL Server process to throw a C++ exception in the debugger; after allowing execution to continue the expected message is produced in Query Analyzer:

SELECT permission denied on object 'sysxlogins', database 'master',
owner 'dbo'.

Logging into the server as the sa user, which does have select permission on the table, and running the query displays the table and does not produce the C++ exception. Clearly the access control mechanism throws an exception when access is denied to a table. A great help when debugging SQL Server is the symbols file (sqlservr.pdb), which is provided by Microsoft in the MSSQLBinnexe directory. This provides the original function names to the debugger and allows inference of the general purpose of large chunks of assembler. A case in point here is the function FHasObjPermissions, which after setting breakpoints on all functions containing the word "permission" is executed after the original select query is run. A static disassembly of the main SQL Server binary using DataRescue's excellent IDA Pro can be used to divine the behavior of this function. In this case the function is called from within the CheckPermissions function:

0087F9C0 call FHasObjPermissions
0087F9C5 add esp, 14h
0087F9C8 test eax, eax
0087F9CA jnz short loc_87F9DC
0087F9CC push 17h
0087F9CE push 19h
0087F9D0 push 2
0087F9D2 push 24h
0087F9D4 call ex_raise

FHasObjPermissions is called, and after it returns, the stack-pointer (esp) is increased by 0x14 to remove the arguments that were passed to the function. The eax register is then compared with itself using the test operator; the effect of this operation is to set the CPU's zero flag only if eax is zero. So if eax is set to zero by FhasObjPermission, the following jnz (jump if not zero) operator will not cause a jump and execution will continue on to the call to ex_raise. To avoid the exception being raised, the jump to the code that carries out the query should always occur. A quick way to achieve this would be to patch the conditional jump (jnz) to a non-conditional jump (jmp), however this may not bypass further checks; if the code is investigated further a neater patch can be found.

Looking at the code for FHasObjPermissions, an interesting section is

004262BB call ExecutionContext::Uid(void)
004262C0 cmp ax, 1
004262C4 jnz loc_616F76

The call to the Uid method in the ExecutionContext object places the current user's uid into the ax register (the 16-bit version of the eax register, effectively the lower 16 bits of this 32-bit register). SQL Server uids (user IDs) are listed in the sysxlogins table, and the uid with a value of 1 is associated with the database administrators group dbo. Because the code is comparing the uid returned by the Uid() call to 1, the best approach would be to patch ExecutionContext::Uid() to always return 1. Examining the function, the assignment takes place at the end, just before it returns:

00413A97 mov ax, [eax+2]
00413A9B pop esi
00413A9C retn

Changing the mov ax, [eax+2] assignment to mov ax, 1 requires patching three bytes. The bytes 66 8B 40 02 should be changed to 66 B8 01 00. Any user now has permissions on all objects in the database and any user can view the password hashes in sysxlogins. Attempting to execute the stored procedure xp_cmdshell as a non-admin user, however, results in

Msg 50001, Level 1, State 50001
xpsql.cpp: Error 183 from GetProxyAccount on line 604

This is because of a security feature in SQL Server that prevents nonadministrators from executing commands unless a proxy account is specified. SQL Server is attempting to retrieve this proxy information and failing because it is not set by default. Loading up SQL Server's Enterprise Manager, and selecting the Job System tab under SQL Server Agent properties, invalid proxy account information was entered. The error now produced when xp_cmdshell is run with low privileges is

Msg 50001, Level 1, State 50001
xpsql.cpp: Error 1326 from LogonUserW on line 488

Using APISpy32 to watch for calls to the Windows API function LogonUserW(PWSTR, PWSTR, PWSTR, DWORD, DWORD, PDWORD) when xp_cmdshell is executed, the output shows the function being called from within xplog70.dll. This DLL can be debugged by launching the sqlservr.exe process from within a debugger such as Microsoft's WinDbg or from IDA's internal debugger. After setting multiple breakpoints in the code and stepping through the code-path taken when xp_cmdshell is successfully and unsuccessfully executed, the divergence point can be established. This point on SQL Server 2000 with no service packs turns out to be a conditional jump (jnz):

42EA56D3 add esp, 0Ch
42EA56D6 push eax
42EA56D7 call strcmp
42EA56DC add esp, 8
42EA56DF test eax, eax
42EA56E1 jnz loc_42EA5A98

Patching the 2-byte op-code for jnz (0F 85) to the 2-byte op-code for a nonconditional jump jmp (90 E9) results in execution of xp_cmdshell being allowed for all users. Both this patch and Chris Anley's original patch require existing attack vectors for deployment such as a buffer overflow vulnerability. The decision on whether to patch bytes in memory (run-time patching) or to patch the actual SQL Server system files on the hard-drive depends on two factors; if the target is running software that offers file baselining features such as TripWire, and the SQL Server binaries are patched, this will be detected. However, if the SQL Server code is patched in memory, any backdoors will be removed on reboot of the server. A call to the function VirtualProtect is needed first in order to make the code segment writable.

XSTATUS Backdoor

Another tactic, known as the xstatus backdoor, uses a modification to the xstatus column of the master.dbo.sysxlogins table to permit users to login with system administrator privileges and no password. The xstatus column contains a smallint (2 byte) value that describes the user's role memberships together with the method of authentication to use. If the third bit of the number is set to zero, this denotes that the account authenticates using SQL Server's native authentication; a 1 means that Windows authentication is used. The default SQL Server account used with Windows authentication (BUILTINAdministrators) has a null password, which becomes a problem if the xstatus bit is changed to zero, giving an effective denary value of 18. This results in allowing anyone to log on to the server using native authentication, a username of BUILTINAdministrators, and a blank password.

The Windows .NET Server adds the NT AUTHORITYNETWORK SERVICE group as a SQL login and this account is also prone to xstatus changes in the same way as BUILTINAdministrators.

Start-Up Procedures

If the SQL Server is set up to use replication, the stored procedure sp_MSRepl_startup will be executed every time the server is restarted, in the security context of the SQL Server process. This makes it a target for Trojanning — all procedures that are run automatically should be examined for malicious instructions. The presence of the stored procedures sp_addlogin, sp_addrolemember, sp_addsrvrolemember,or xp_cmdshell in startup procedures may indicate that the server has been attacked and should be investigated.

Click for the complete book excerpt series.

Dig Deeper on SQL Server Security