Remember when you could figure out what bank was being targeted by a Brazilian banking Trojan just by running “strings” against it? Well, that was a while ago.
There’s this decode function widespread among most banking Trojan samples that I get my hands on, especially those written in Delphi.
Let’s take a look!
Also, please don’t mind me interchangeably saying “decode” or “decrypt” — you know what I mean by it!
First, let’s see how this function gets called:
As I said earlier, this function is pretty common among bankers written in Delphi. The example above is one of these cases. The decryption function is sub_6ba234. Since Delphi uses FASTCALL calling convention, we can see that there are two paramteres.
- EAX – String to be decrypted;
- EDX – Address of the variable where the result will be written.
Opening sub_6ba234 up, we see this:
As I did in other posts, I’ll focus on the important blocks here. If everything goes well, in the end, we’ll have enough information to build a script to decrypt these strings.
So, in this first block, we can see that the function stores in [ebp+var_14] the address where the result will be written to, and in [ebp+var_4] the string to be decrypted. At the end of the block, there’s a comparison to check if [var_4] (encrypted string) is 0, with the intention to verify if the received string is valid or not. If it’s valid, the function starts the decryption process.
Let’s rename [ebp+var_14] to [ebp+RETURN_VAR] and [ebp+var_4] to [ebp+ENCRYPTED_STRING].
Let’s start with this part:
___:006BA275 lea eax, [ebp+var_C] ___:006BA278 mov edx, offset aXmlpz_0 ; "xmlpz" ___:006BA27D call @UStrLAsg
@UStrLAsg, apparently, copies a const string to a global variable in a safe way (whatever that means). So now [ebp+var_c] holds the string “xmlpz”.
___:006BA28A mov eax, [ebp+var_C] ___:006BA28D test eax, eax ___:006BA28F jz short loc_6BA29
var_c == the address of “xmlpz”, right? If this address (now in EAX) is not 0, the following piece of code gets executed:
___:006BA291 sub eax, 4 ___:006BA294 mov eax, [eax]
The string’s address gets subtracted by ox4, and the content of the resulting address gets moved into EAX.
I know, quite confusing. Why do something like that? Well, in Delphi, every string is not just an array of characters, but actually a little data structure. The address that is being moved here, holds the length of the referenced string. EAX now holds 5, which is the length of “xmlpz”.
Onto the third block:
___:006BA296 mov [ebp+var_18], eax ___:006BA299 xor edi, edi ___:006BA29B lea eax, [ebp+var_10] ___:006BA29E mov edx, offset aD_25 ; "D" ___:006BA2A3 call @UStrLAsg ___:006BA2A8 lea edx, [ebp+var_24] ___:006BA2AB mov eax, offset aD_26 ; "D" ___:006BA2B0 call UpperCase ___:006BA2B5 mov edx, [ebp+var_24] ___:006BA2B8 mov eax, [ebp+var_10] ___:006BA2BB call @UStrEqual ___:006BA2C0 jnz loc_6BA383
The first line saves the length of “xmlpz” (as we saw previously) in [ebp+var_18]. Then, “D” gets loaded into [ebp+var_10].
UpperCase is very self-explanatory. However, “D” is already in upper case, so this basically stores “D” in [ebp+var_24]. Then @UStrEqual is executed, the parameters being passed to it are var_24 and var_10, which we know are “D” and “D”. Since they’re obviously equal, @UStrEqual will return 1, which is true.
Let’s continue.
@UStrCopy has the following signature:
function @UStrCopy(S:UnicodeString; Index:Integer; Count:Integer): UnicodeString;
So, this is what’s happening:
var_2c := UStrCopy(encrypted_string, 1, 2);
The encrypted_string parameter is “BE54AE7F947B92AD4EB651”.
This piece of code clearly instructs @UStrCopy to copy two characters, starting at position 1, to var_2c. Thus, when @UStrCopy gets called, what will var_c contain? “BE”. The first two characters of the encrypted string.
@StrCat will concatenate two strings. This is its signature:
procedure @UStrCat3(var Dest:UnicodeString; Source1:UnicodeString; Source2:UnicodeString);
So, this is what’s happening:
UStrCat3(var_28, "$", var_2c);
Now, we know that var_2c holds “BE”. We can assume that UStrCat will write “$BE” in var_28. Later, StrToInt gets called. In Delphi, hexadecimal values start with the “$” prefix. So the idea here is to transform the string “$BE” in a number. You know that a string and a number have different representation in low-level, right?
The variables var_2c and var_28 won’t be used in the next blocks, so i won’t rename them. But var_1c (which holds the return value from StrToInt) will. So let’s rename it to CURRENT_HEX_BLOCK.
Pay attention to the instruction MOV ESI, 3, as it will be important for the next block.
This is the start of a loop. Important things happen here. The first thing that you might have noticed is that the first block looks exactly like the one we saw previously (the with the StrToInt). And it really almost does the same thing, with some minor exceptions.
The first thing that is different, is the starting position being passed to @UStrCopy. Instead of copying two characters from position 1 of encrypted_string, it’s starting at position 3 (because of MOV ESI, 3 i warned you earlier, remember?). That is, this malware is extracting (in this loop iteration), from string “BE54AE7F947B92AD4EB651”, the string “54”. It’s copying two characters starting from ESI position, and not the fixed position 1 we saw in the other block. You see where this is going, don’t you? Let’s continue.
It then converts the “54” string into an actual hexadecimal value, and stores this number in var_20. Then it compares EDI (which is, at first, 0), with the size of “xmlpz”, which is 5. If EDI is smaller than XMLPZ_LENGTH, it increments EDI by 1, otherwise, it sets EDI back to 1. Again… you see where this is going, don’t you? In this first iteration, therefore, EDI is 1.
Take a look at what comes after:
___:006BA33A movzx ebx, word ptr [eax+edi*2-2] ___:006BA33F xor ebx, [ebp+var_20] ___:006BA342 cmp ebx, [ebp+CURRENT_HEX_BLOCK]
EAX holds “xmlpz”, right? EDI is 1, right? Let’s do the math.
[EAX + (1 * 2 - 2)] = [EAX + (2 - 2)] = [EAX + 0] = "x" = 0x78.
EBX now holds the first character of the “xmlpz” string. “x”, according to the ASCII table is 0x78. So EBX has? 0x78! And what happens when EDI is set to 2? Because we saw that in the next loop iteration, it’ll get incremented. If it EDI gets bigger than “xmlpz”‘s length, it’ll go back to 1, right? So let’s see what will happen when EDI is 2.
[EAX + (2 * 2 - 2)] = [EAX + (4 - 2)] = [EAX + 2] = "m" = 0x6d
Right? Right? Let’s continue.
___:006BA33A movzx ebx, word ptr [eax+edi*2-2] ___:006BA33F xor ebx, [ebp+var_20] ___:006BA342 cmp ebx, [ebp+CURRENT_HEX_BLOCK]
Do you remember what’s in var_20? 0x54, right? And from where did this value come from? It was the third and fourth character from encrypted_string (BE54AE7F947B92AD4EB651). So, what’s happening in this loop iteration is XOR EBX(0x78), 0x54. After the instruction, EBX holds 0x2c.
___:006BA33A movzx ebx, word ptr [eax+edi*2-2] ___:006BA33F xor ebx, [ebp+var_20] ___:006BA342 cmp ebx, [ebp+CURRENT_HEX_BLOCK]
CURRENT_HEX_BLOCK holds the first two characters from encrypted_string, 0xBE. So this is comparing EBX (now 0x2c) with 0xBE.
Let’s rename var_20 to NEXT_HEX_BLOCK. In the end, we’ll write a python script to do all this. It’ll be a lot more readable! But i think it’s very important to understand what’s going on in low-level.
Last part:
The last comparison was if the XOR result (current value of NEXT_HEX_BLOCK xor’d against current letter of “xmlpz”) is bigger than what’s in CURRENT_HEX_BLOCK (which is another block from the encrypted_string). If it is, the XOR result gets subtracted by CURRENT_HEX_BLOCK, otherwise, 0xFF is added to XOR result before the same subtraction. All this math gets stored in EBX, and guess what? EBX now holds the decrypted character! :).
var_38 is the pointer to the final (decrypted) string. var_8 holds 0, so this call to @UStrCat concatenates a null byte right after what was on EBX.
Then, NEXT_HEX_BLOCK gets moved to CURRENT_HEX_BLOCK. The functions adds 2 to ESI, and this determines what position of encrypted_string will be copied on loop_start, that is, NEXT_HEX_BLOCK will hold the next 2 characters after 0x54, 0xAE (remember encrypted_string is BE54AE7F947B92AD4EB651).
ESI gets compared to the length of encrypted_string. If it’s smaller, the loop goes on – with the next hex block. If it’s bigger / equal, the loop ends.
Decryption overview
This is another way to see what’s happening up there:
The string “BE54AE7F947B92AD4EB651” after being decrypted, becomes “modelo.txt”.
Decryption script
This python function wraps up everything we saw.
def decrypt_string(key, x_string): decrypted_string = '' key_index = 0 x_index = 2 current_block = x_string[0:2] current_block = int(current_block, 16) while x_index current_block: xor_result = xor_result - current_block else: xor_result = xor_result + 0xFF xor_result = xor_result - current_block decrypted_string += chr(xor_result) key_index += 1 x_index += 2 if key_index == len(key): key_index = 0 current_block = next_block return decrypted_string
Now, just pass the key and the encrypted string. This is what you should get:
That was fun!
Until the next post. 🙂