It this post I’m going to analyse the details of CVE-2012-0711 (IBM’s security bulettin), an integer signedness bug, I’ve found in IBM DB2 Express-C a while ago.
The description of the bug:
“Integer signedness error in the db2dasrrm process in the DB2 Administration Server (DAS) in IBM DB2 9.1 through FP11, 9.5 before FP9, and 9.7 through FP5 on UNIX platforms allows remote attackers to execute arbitrary code via a crafted request that triggers a heap-based buffer overflow.”
I’m always curious how a vulnerability was found, and this details are almost never public. After I’ve analysed ZDI-11-036, I’ve learned the format of DB2 DAS communication protocol. I’ve created a simple (byte-replacer) protocol-aware fuzzer which only modified the “data” blocks during the communication, and wrote a simple client sending SysCmd requests to the database. After having a lot of crashes, it turned out, that the replaced bytes were always in the encrypted username. (You can see the dump of a communication here.) I wanted to create a client which gives me more fine-grained control above the communication (my client used db2dasutil.so through JNI, so I had only a toplevel interface). After trying to understand and implement the Diffie-Hellman key exchange for two weeks (as it turned out later, it’s not needed to control the Diffie-Hellman part), I give up this idea, and started to check the details of the crash.
Lets start with the backtrace:
(gdb) ba #0 0xb7336102 in DasHashTable::get(DasHashKey*) () from /opt/ibm/db2/V9.7/lib32/libdb2dascmn.so.1 #1 0x08057398 in validateUser(rrmRequestContext*) () #2 0x08056fd0 in authenticateRequest () #3 0x0805632c in handleServerEncryptAuthRequest(rrmRequestContext*, dasRecvParam*) () #4 0x08054c48 in handleRequest () #5 0xb73a74f6 in db2dasThreadMain () from /opt/ibm/db2/V9.7/lib32/libdb2dasapi.so.1 #6 0xb6c76e99 in start_thread (arg=0x661e2b70) at pthread_create.c:304 #7 0xb6aae73e in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:130
Lets have a look at DasHashTable::get(DasHashKey*) with some comments:
0x005cd0db <+77>: mov 0xc(%ebp),%eax 0x005cd0de <+80>: push %eax 0x005cd0df <+81>: mov (%eax),%edx 0x005cd0e1 <+83>: call *(%edx) # this is a call of UidKey::getHashCode [_ZN6UidKey11getHashCodeEv], throught vtable 0x005cd0e3 <+85>: add $0x4,%esp # eax contains the calculated hash code of the username 0x005cd0e6 <+88>: mov 0x8(%ebx),%ecx #0x64 0x005cd0e9 <+91>: mov (%ebx),%ebx 0x005cd0eb <+93>: cltd 0x005cd0ec <+94>: idiv %ecx # after this division edx will contain the remainder in the range -0x63..0x63 0x005cd0ee <+96>: mov (%ebx,%edx,4),%ebx # ebx <- *(ebx + 4 * (-0x63 .. 0x63 ))
We can see, that this part of the function takes a string from the DasHashKey structure, calculates it’s hash, and after a modulo 0x64 operation it uses the value, to index an array. The string is the supplied username, and most probably this is a hashtable implementation, with fixed (0x64) size. The problem is, that if the remainder is a negative number, we underrun the buffer.
Lets look into the implementation of UidKey::getHashCode:
0x08063b78 <+12>: mov 0x8(%ebp),%ebx #0:[ptr] 4:[username] 0x08063b7b <+15>: lea 0x4(%ebx),%ecx #ecx <- pointer to the username string #this block calculates the length of the username 0x08063b7e <+18>: xor %eax,%eax 0x08063b80 <+20>: movzbl (%ecx,%eax,1),%edx 0x08063b84 <+24>: test %edx,%edx 0x08063b86 <+26>: je 0x8063b97 <_ZN6UidKey11getHashCodeEv+43> 0x08063b88 <+28>: movzbl 0x1(%ecx,%eax,1),%edx 0x08063b8d <+33>: add $0x2,%eax 0x08063b90 <+36>: test %edx,%edx 0x08063b92 <+38>: jne 0x8063b80 <_ZN6UidKey11getHashCodeEv+20> 0x08063b94 <+40>: add $0xffffffff,%eax 0x08063b97 <+43>: mov %eax,%esi # esi <- strlen(username) [...] #after this block, eax will contain the sum of the character codes in the username string 0x08063bba <+78>: test %esi,%esi 0x08063bbc <+80>: jle 0x8063bf7 <_ZN6UidKey11getHashCodeEv+139> 0x08063bbe <+82>: mov %edi,-0xc(%ebp) 0x08063bc1 <+85>: xor %eax,%eax 0x08063bc3 <+87>: xor %edx,%edx 0x08063bc5 <+89>: movsbl 0x4(%edx,%ebx,1),%edi 0x08063bca <+94>: add %edi,%eax 0x08063bcc <+96>: add $0x1,%edx 0x08063bcf <+99>: cmp %esi,%edx 0x08063bd1 <+101>: jl 0x8063bc5 <_ZN6UidKey11getHashCodeEv+89> 0x08063bd3 <+103>: mov -0xc(%ebp),%edi
Looking into the code, we can easily see, that it first calculates the length of the specified username, then fetches it character by character and calculates the hashcode, as the sum of the charactercodes. Its interesting, that it handles the characters as unsigned (movzbl) when calculating the length, but signed (mobsbl) when calculating the sum. Later turned out, that the problem affects only Linux versions of DB2, because the Windows versions use unsigned chars when calculating the hash.
Lets get back to our “hijacked” pointer:
#if ebx is NULL we exit 0x005cd0f1 <+99>: test %ebx,%ebx 0x005cd0f3 <+101>: je 0x5cd123 <_ZN12DasHashTable3getEP10DasHashKey+149> #if *ebx is not mapped memory, a SEGFAULT happens 0x005cd0f5 <+103>: mov 0xc(%ebp),%edi 0x005cd0f8 <+106>: mov (%ebx),%eax #if *ebx is NULL, we exit 0x005cd0fa <+108>: test %eax,%eax 0x005cd0fc <+110>: je 0x5cd11c <_ZN12DasHashTable3getEP10DasHashKey+142> #if *(*(ebx)) points to nonmapped memory, we SEGFAULT 0x005cd0fe <+112>: push %edi 0x005cd0ff <+113>: push %eax 0x005cd100 <+114>: mov (%eax),%edx #we call *(*(*(ebx))+4) 0x005cd102 <+116>: call *0x4(%edx)
We can move the ebx pointer backward up to 100 words. ebx is originally pointing to an object so with this we can make the pointer to point into a different object’s memory area. If we can control a word in this area, we can build up a dereference chain, which will enable us to call an arbitrary function. I didn’t checked that controlling of one word in this area is possible or not, but I found this video, and I suppose its about the same vulnerability.
The simplest example of triggering the bug is to specify a one character username, because with one character usernames from range (128..255) we can generate all the possibilities of ebx overwrites (-0x63 .. -1).
For the test case I’ve created a simple example. The code uses the DasSysCmd command, but it is irrelevant, because the crash happens in the authentication stage, before sending the command. Every other command would be appropriate, which requires authentication. You can reach it here.