TWSL2016-005: Memory corruption in a third-party component: how to find what's wrong

In continuation of this post: https://www.trustwave.com/Resources/SpiderLabs-Blog/Debugging-SAP-ASE--NET-Provider-Issues/

Recently we stumbled upon an issue in DevArt dotConnect for Oracle component (8.5.583): supplying an overly long Oracle Database username results in a crash with memory corruption message. This doesn't happen on every run however. We decided to investigate what goes on. First thing is writing simple minimal C# application that connects to Oracle Database to reproduce the issue.

Minimal C# app:

Min_app

It will throw this exception:

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
at Devart.Data.Oracle.a3.a(cl A_0, t A_1)
at Devart.Data.Oracle.OracleInternalConnection..ctor(cl connectionOptions, OracleInternalConnection proxyConnection)
at Devart.Data.Oracle.a1.a(ae A_0, Object A_1, DbConnectionBase A_2)
at Devart.Common.DbConnectionFactory.a(DbConnectionPool A_0, ae A_1, DbConnectionBase A_2)
at Devart.Common.DbConnectionPool.a(DbConnectionBase A_0)
at Devart.Common.DbConnectionPool.GetObject(DbConnectionBase owningConnection)
at Devart.Common.DbConnectionFactory.b(DbConnectionBase A_0)
at Devart.Common.DbConnectionClosed.Open(DbConnectionBase outerConnection)
at Devart.Common.DbConnectionBase.Open()
at Devart.Data.Oracle.OracleConnection.Open()
at Demo.Program.Main(String[] args)

From the stacktrace we know that possible candidate for examination Is Devart.Data.Oracle.a3.a(cl A_0, t A_1) since it's closest to the exception. The name suggests it is obfuscated but nevertheless we fire up Red-Gate Reflector on Devart.Data.Oracle.dll and navigate to that method. After simple lookup of username-setting OCI function references (namely we look for OCIAttrSet with OCI_ATTR_USERNAME parameter) we find a few occurrences of this pattern:

if (destination == IntPtr.Zero){ destination = Marshal.AllocHGlobal(100); } byte[] buffer4 = Utils.GetMaxBytes(encoding, cl.u(), out num); Marshal.Copy(buffer4, 0, destination, buffer4.Length); this.e(this.b.OCIAttrSet(this.g, 9, destination, num, 0x16, this.d)); // Constants 9 and 0x16 here are OCI_HTYPE_SESSION and OCI_ATTR_USERNAME respectively

From the above we see that a buffer of fixed size (100 bytes) is allocated from the heap no matter how long actual username is. Obviously it is a problem. Let's confirm what comes from Utils.>GetMaxBytes method for a string of 34 characters passed in UTF8 encoding:

public static byte[] GetMaxBytes(Encoding encoding, string s, out int byteCount) { int length = s.Length; byte[] bytes = new byte[encoding.GetMaxByteCount(length) + 1]; byteCount = encoding.GetBytes(s, 0, length, bytes, 0); return bytes; }

Simple program using this snippet results in 106 bytes – this means that subsequent Marshal.Copy will overwrite adjustent heap structures. Depending on what specific structures are overwritten it may lead to application crash, code execution (possible) or simply go unnoticed.

Now let's use WinDbg to confirm our finding: run the application under WinDbg debugger and issue the following commands to set a breakpoint on Marshal.AllocHGlobal:

.symfix sxe ld clrjit g .loadby sos clr !name2ee mscorlib.dll System.Runtime.InteropServices.Marshal.AllocHGlobal

Notice the MethodDesc values: select the one that corresponds to the AllocHGlobal(IntPtr) overload since the Int32 overload just wraps IntPtr one. use the next command to set a breakpoint on that value:

!bpmd -md 703b8a84

Resume execution and examine CLR stack on each break via:

!CLRStack -p

Eventually it will break on:

0:007> !CLRStack -p
OS Thread Id: 0x8f4 (7)
Child SP IP Call Site
05a8f7a4 70ad8a20 System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr) PARAMETERS: cb (<CLR reg>) = 0x00000064 05a8f7a8 0061e238 Devart.Data.Oracle.a3.b(System.Object) PARAMETERS: this (0x05a8f7c4) = 0x02337b74 A_0 = <no data>

In blue we highlighted 100 bytes argument (0x64), in red our culprit method. Stepping out (Shift+F11) lands us in this disassembly:

// Marshal.AllocHGlobal 0061e233 e8e8a74b70 call mscorlib_ni+0x368a20 (70ad8a20) 0061e238 8945b4 mov dword ptr [ebp-4Ch],eax ss:002b:05a8f8b4=00000000 0061e23b 8b85c0feffff mov eax,dword ptr [ebp-140h] 0061e241 8b5014 mov edx,dword ptr [eax+14h] 0061e244 8d45c4 lea eax,[ebp-3Ch] 0061e247 50 push eax 0061e248 8b8db8feffff mov ecx,dword ptr [ebp-148h] 0061e24e ff15c40d6200 call dword ptr ds:[620DC4h] 0061e254 8945ac mov dword ptr [ebp-54h],eax 0061e257 ff75b4 push dword ptr [ebp-4Ch] 0061e25a ff7004 push dword ptr [eax+4] 0061e25d 8bc8 mov ecx,eax 0061e25f 33d2 xor edx,edx // Marshal.Copy 0061e261 e8ea67af70 call mscorlib_ni+0x9a4a50 (71114a50)

Now on 0061e261 examine the object pointed to by the ecx register with the do command:

0:007> !do ecx Name: System.Byte[] MethodTable: 70b44588 EEClass: 707d229c Size: 164(0xa4) bytes Array: Rank 1, Number of elements 152, Type Byte (Print Array) Content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Fields: None

It is the source Byte array containing 164 elements. Register edx holds zero (second argument to the Copy), in esp there is 0xa4 (164 decimal) – source array length, at esp+4 is pointer to destination. Before the Marshal.Copy the destination is:

0:007> d poi(esp+4) 0033e458 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e468 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e478 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e488 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e498 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e4a8 0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba ................ 0033e4b8 0d f0 ad ba ab ab ab ab-ab ab ab ab ee fe ee fe ................ 0033e4c8 00 00 00 00 00 00 00 00-4c e7 9c 31 49 46 00 00 ........L..1IF..

After return from Marshal.Copy:

0:007> d 0033e458 0033e458 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e468 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e478 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e488 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e498 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e4a8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e4b8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0033e4c8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA

As we see it changed memory beyond allowed 100 bytes. Again, depending on what structures are corrupted we may observe different behavior of the app. This buffer overflow situation can be used to inject code via a well-crafted username or password string.

Conclusion

We see that the perils of using a fixed size buffer to hold a user-entered value still exist, even in a managed environment if it wraps native code. The problem is avoidable by checking the size of the data to be copied, which should be taken as common programming practice.

For more information please see the Trustwave security advisory: https://www.trustwave.com/Resources/Security-Advisories/Advisories/TWSL2016-005/?fid=7652

This blog post was co-authored by Martin Rakhmanov and Ernesto Cullen.

Trustwave reserves the right to review all comments in the discussion below. Please note that for security and other reasons, we may not approve comments containing links.