Threat Intelligence & APTs • Windows

Bangladesh GPCA: Targeted Cyber Espionage and Custom Payload Decoding

Decode the custom-encrypted gpca.dat payload used in the Bangladesh cyber heist, analyzing how the nroff b.exe loader interacts with financial systems.

Initial Context:

▪The Bangladesh (central) bank heist, happened in 2016
▪The attackers tried to transfer funds with SWIFT transactions
▪Artifacts: nroff b.exe (1d0e79feb6d7ed23eb1bf7f257ce4fee) and gpca.dat
▪We need to decrypt gpca.dat, that is known to be used by nroff b.exe

The executable reads .dat file that is encrypted and it is supposedly some configurations.

that we do need to recover and decrypt.

File: gpca.dat

Toool: 010 editor

This looks like heavily encrypted.

Screenshot 2026-01-05 at 11.08.51 AM.png
Figure: Screenshot 2026-01-05 at 11.08.51 AM.png Click to zoom ↗

File: 1d0e79feb6d7ed23eb1bf7f257ce4fee.

This is a exe file we have MZ header, DOS stun and we have the PE magic.

Screenshot 2026-01-05 at 11.15.55 AM.png
Figure: Screenshot 2026-01-05 at 11.15.55 AM.png Click to zoom ↗

IDA CODE Analysis

Start with main. IDA recognize that it is a C compiled binary.

Screenshot 2026-01-05 at 12.31.51 PM.png
Figure: Screenshot 2026-01-05 at 12.31.51 PM.png Click to zoom ↗

Nothing interesting let’s follow in the call to function 0x403130

Screenshot 2026-01-05 at 12.33.14 PM.png
Figure: Screenshot 2026-01-05 at 12.33.14 PM.png Click to zoom ↗

looks like code nothing that requires immediate attention.

Screenshot 2026-01-05 at 12.35.21 PM.png
Figure: Screenshot 2026-01-05 at 12.35.21 PM.png Click to zoom ↗

Our goal: Decrypts the content of the file called gpca.dat

Bangladesh GPCA - Tracking the value

Data flow: tracking values at various points in the program "gpca.dat" - obvious string value to look for

▪read/write/offset cross-references
▪"what reads this value?”
▪"What writes this value?”
▪"what uses the address of this value?”

Code (control) flow: determining possible sequences of execution

▪call/jump/offset cross-references
▪"what calls this code?”
▪"What jumps to this code?”
▪"what uses the address of this code?”

Our starting point is finding a strings containing “gpca.dat” and then further to any string that was generated by using this string. By using all these references we find the code that is using this file name.

Search string in IDA

Screenshot 2026-01-05 at 12.48.41 PM.png
Figure: Screenshot 2026-01-05 at 12.48.41 PM.png Click to zoom ↗

1st - Found “gpca.dat” this is an argument to a function.

Screenshot 2026-01-05 at 12.49.15 PM.png
Figure: Screenshot 2026-01-05 at 12.49.15 PM.png Click to zoom ↗

Navigate to the location of the string. Look at the cross-references.

Screenshot 2026-01-05 at 12.51.30 PM.png
Figure: Screenshot 2026-01-05 at 12.51.30 PM.png Click to zoom ↗

2nd Time we find the same string.

Screenshot 2026-01-05 at 12.54.42 PM.png
Figure: Screenshot 2026-01-05 at 12.54.42 PM.png Click to zoom ↗

So this is the only location in the whole binary. That mentions our file name.

Screenshot 2026-01-05 at 12.55.42 PM.png
Figure: Screenshot 2026-01-05 at 12.55.42 PM.png Click to zoom ↗

Analysing

We see that it is used as an argument to the function called _makepath.

we don’t know what is the meaning of the arguments. we will us ida’s definition of the function.

So, in our case, gpca.dat is the argument Filename.

MSDN Refrences.

Screenshot 2026-01-05 at 1.13.53 PM.png
Figure: Screenshot 2026-01-05 at 1.13.53 PM.png Click to zoom ↗
Screenshot 2026-01-05 at 1.13.16 PM.png
Figure: Screenshot 2026-01-05 at 1.13.16 PM.png Click to zoom ↗

Our file name is used as fname here.

So this function will compile a full path to a file name from it’s components. and store it in the buffer pointed by the first argument.

In our case this buffer was important to us because it will also be used as a longer path to the same file name.

Screenshot 2026-01-05 at 4.10.19 PM.png
Figure: Screenshot 2026-01-05 at 4.10.19 PM.png Click to zoom ↗

let’s rename this buffer to for us to fullPathToGpca trace it’s value. this new fullpath is what we are going to trace for both code and data analysis.

Screenshot 2026-01-05 at 4.16.39 PM.png
Figure: Screenshot 2026-01-05 at 4.16.39 PM.png Click to zoom ↗

follow the fullPathToGpca .

Screenshot 2026-01-05 at 4.21.33 PM.png
Figure: Screenshot 2026-01-05 at 4.21.33 PM.png Click to zoom ↗

Now let’s use this fullPath Buffer at 0x00405FD0

We will use back references generated by IDA to track the places in the code where this value is used.

Press x to see back references. There are 2 places in the code that references this address.

inspect code at both the addresses.

Screenshot 2026-01-05 at 4.30.29 PM.png
Figure: Screenshot 2026-01-05 at 4.30.29 PM.png Click to zoom ↗

The 1st location we have seen already.

Screenshot 2026-01-05 at 4.32.22 PM.png
Figure: Screenshot 2026-01-05 at 4.32.22 PM.png Click to zoom ↗

The 2nd one is at 0x0040313A The address of the buffer is passed on the stack.

Screenshot 2026-01-05 at 4.33.23 PM.png
Figure: Screenshot 2026-01-05 at 4.33.23 PM.png Click to zoom ↗

look’s like it’s used in this argument to another function.

Screenshot 2026-01-05 at 4.36.12 PM.png
Figure: Screenshot 2026-01-05 at 4.36.12 PM.png Click to zoom ↗

Into the function 0x4013B0

Screenshot 2026-01-05 at 4.47.11 PM.png
Figure: Screenshot 2026-01-05 at 4.47.11 PM.png Click to zoom ↗

It copies to ecx then again passed as an argument function 0x401290

Screenshot 2026-01-05 at 4.49.38 PM.png
Figure: Screenshot 2026-01-05 at 4.49.38 PM.png Click to zoom ↗

In 0x401290

Again it was renamed to lpFileName and copied to eax. and it is passed to function CreateFileA.

Screenshot 2026-01-05 at 4.50.58 PM.png
Figure: Screenshot 2026-01-05 at 4.50.58 PM.png Click to zoom ↗

We can see that the opened file handle is used to get the file size.

Screenshot 2026-01-05 at 4.53.39 PM.png
Figure: Screenshot 2026-01-05 at 4.53.39 PM.png Click to zoom ↗

Then there is a Memory Allocation. for that file size+1

Screenshot 2026-01-05 at 5.01.18 PM.png
Figure: Screenshot 2026-01-05 at 5.01.18 PM.png Click to zoom ↗

Then the file is read. And the buffer is the result of the memory allocation. The number of byte read is the file size.

Screenshot 2026-01-06 at 11.25.00 AM.png
Figure: Screenshot 2026-01-06 at 11.25.00 AM.png Click to zoom ↗
Screenshot 2026-01-06 at 11.26.19 AM.png
Figure: Screenshot 2026-01-06 at 11.26.19 AM.png Click to zoom ↗

So the whole file is read into a newly allocated buffer.

and if something goes wrong the buffer is free.

Screenshot 2026-01-06 at 11.27.53 AM.png
Figure: Screenshot 2026-01-06 at 11.27.53 AM.png Click to zoom ↗

If not another argument is used as a pointer to the result. and the result is esi . In our case esi is the file size.

Screenshot 2026-01-06 at 11.28.39 AM.png
Figure: Screenshot 2026-01-06 at 11.28.39 AM.png Click to zoom ↗

EAX, the standard return of the function is EDI that is the result of allocation of the memory.

Screenshot 2026-01-06 at 11.31.01 AM.png
Figure: Screenshot 2026-01-06 at 11.31.01 AM.png Click to zoom ↗

So, the function is reading the the whole file. The file size is passed as a result in the pointer that is the second argument. And the buffer just returned in EAX.

Going back and renaming this function to something like ReadFile.

Screenshot 2026-01-06 at 11.47.11 AM.png
Figure: Screenshot 2026-01-06 at 11.47.11 AM.png Click to zoom ↗

This function reading the contents of the gpca.dat

Screenshot 2026-01-06 at 12.12.22 PM.png
Figure: Screenshot 2026-01-06 at 12.12.22 PM.png Click to zoom ↗

And it’s address is 0x401290

Screenshot 2026-01-06 at 12.21.13 PM.png
Figure: Screenshot 2026-01-06 at 12.21.13 PM.png Click to zoom ↗

Now we know where the content’s of the file is read.

go one level up

we need to track the value of the file to find the location where it is decoded or decrypted.

NOTE: - The result, the contents of the file is returned in EAX. Which is then immediately copied in in EBX.

Screenshot 2026-01-06 at 12.25.42 PM.png
Figure: Screenshot 2026-01-06 at 12.25.42 PM.png Click to zoom ↗

let’s track EBX

It is used as a argument to another function.

Screenshot 2026-01-06 at 12.27.58 PM.png
Figure: Screenshot 2026-01-06 at 12.27.58 PM.png Click to zoom ↗

before calling the function the code is verifying. if var_4 is exactly 0x8438

Screenshot 2026-01-06 at 12.29.32 PM.png
Figure: Screenshot 2026-01-06 at 12.29.32 PM.png Click to zoom ↗

var_4 is used as a second argument for ReadFileCompletely. So it is the size of the file.

there’s a file size check.

Screenshot 2026-01-06 at 12.30.50 PM.png
Figure: Screenshot 2026-01-06 at 12.30.50 PM.png Click to zoom ↗

Convert the value of 0x8438 to decimal. highlight the value and press h to convert hexadecimal to decimal. we can see it’s 33848 bytes.

Screenshot 2026-01-06 at 12.45.12 PM.png
Figure: Screenshot 2026-01-06 at 12.45.12 PM.png Click to zoom ↗

Verifying with the file size of gpca.dat it’s 33,848 bytes. So we are in the right path.

Screenshot 2026-01-06 at 12.56.56 PM.png
Figure: Screenshot 2026-01-06 at 12.56.56 PM.png Click to zoom ↗

we have another function call sub_4032F0

Screenshot 2026-01-06 at 1.22.03 PM.png
Figure: Screenshot 2026-01-06 at 1.22.03 PM.png Click to zoom ↗

that takes some pointer to unknown value. 16 or (0x10), ebx that is our encrypted file. and eax that is the size of the file. So it’s look like it has to operate on this buffer somehow. so let’s go into that function.

Screenshot 2026-01-06 at 1.23.20 PM.png
Figure: Screenshot 2026-01-06 at 1.23.20 PM.png Click to zoom ↗

Function: sub_4032F0

Screenshot 2026-01-06 at 1.28.44 PM.png
Figure: Screenshot 2026-01-06 at 1.28.44 PM.png Click to zoom ↗

let’s rename two last arguments to from:

arg_8 → argGpcaBuffer

arg_c → argGpcaLen length of the buffer.

NOTE: arguments load in the memory in the reverse order.

Screenshot 2026-01-06 at 1.34.38 PM.png
Figure: Screenshot 2026-01-06 at 1.34.38 PM.png Click to zoom ↗

These arguments are used in another function call.

Screenshot 2026-01-06 at 1.36.13 PM.png
Figure: Screenshot 2026-01-06 at 1.36.13 PM.png Click to zoom ↗

So, these are used in another function call. But that one is the second in the function.

we have one function with two different calls. and first one is using only the first and the second argument that we have not recognized yet.

Screenshot 2026-01-06 at 1.38.50 PM.png
Figure: Screenshot 2026-01-06 at 1.38.50 PM.png Click to zoom ↗

Analyzing the first function.

Screenshot 2026-01-06 at 1.43.14 PM.png
Figure: Screenshot 2026-01-06 at 1.43.14 PM.png Click to zoom ↗

Function: 0x4031E0

we have some loop for 256 iterations filling a buffer.

Screenshot 2026-01-06 at 1.43.56 PM.png
Figure: Screenshot 2026-01-06 at 1.43.56 PM.png Click to zoom ↗

Another loop that is operating on the buffer. nothing looks like encryption.

Screenshot 2026-01-06 at 1.47.23 PM.png
Figure: Screenshot 2026-01-06 at 1.47.23 PM.png Click to zoom ↗

Analysing another

function: sub_403250

This one also operates in a loop using index 0x100 and 0x101.

Screenshot 2026-01-06 at 1.49.55 PM.png
Figure: Screenshot 2026-01-06 at 1.49.55 PM.png Click to zoom ↗

We have a xor loop that is xoring a buffer.

there’s a xor cl, bl → that is not just cleaning a register but actually xoring two different values from buffers.

Screenshot 2026-01-06 at 1.53.09 PM.png
Figure: Screenshot 2026-01-06 at 1.53.09 PM.png Click to zoom ↗

The destination of the xor loop is esi, [esp+0Ch+arg_8]. In our case it will be our GpcaBuffer .

Screenshot 2026-01-06 at 1.57.47 PM.png
Figure: Screenshot 2026-01-06 at 1.57.47 PM.png Click to zoom ↗

So the function sub_403250 is definitely xoring somehow our input buffer.

It’s look like it is decryption.

Screenshot 2026-01-06 at 1.59.48 PM.png
Figure: Screenshot 2026-01-06 at 1.59.48 PM.png Click to zoom ↗

we need to figure out which algorithm.

We will use templates (source code and combine it with the disassembly that we have in the IDA to recognize) the algorithm.

Template for Rc4

JSX
RC4 (ARC4)
/*
 * ARC4 key schedule
 */
void mbedtls_arc4_setup( mbedtls_arc4_context *ctx, const unsigned char *key,
				 unsigned int keylen )
{
	int i, j, a;
	unsigned int k;
	unsigned char *m;
	ctx->x = 0;
	ctx->y = 0;
	m = ctx->m;
	for( i = 0; i < 256; i++ )
		m[i] = (unsigned char) i;
	j = k = 0;
	for( i = 0; i < 256; i++, k++ )
	{
		if( k >= keylen ) k = 0;
		a = m[i];
		j = ( j + a + key[k] ) & 0xFF;
		m[i] = m[j];
		m[j] = (unsigned char) a;
	}
}
Screenshot 2026-01-07 at 12.12.26 PM.png
Figure: Screenshot 2026-01-07 at 12.12.26 PM.png Click to zoom ↗

Renaming sub_403250 —> RC4

RC4 cipher needs a key and key length. We need to track the key and the length.

Function: RC4SetKey

Screenshot 2026-01-07 at 12.13.21 PM.png
Figure: Screenshot 2026-01-07 at 12.13.21 PM.png Click to zoom ↗

arg_0 is a S-box context

renaming arg_0 → argCtx

We have 2 arguments arg_4 and arg_8.

Screenshot 2026-01-07 at 12.40.10 PM.png
Figure: Screenshot 2026-01-07 at 12.40.10 PM.png Click to zoom ↗

arg_4 is used as a buffer. So it is the key buffer.

arg_4 → argKeyBuffer

arg_4 is modulo, So this is the length.

arg4 → argkeyLength

Screenshot 2026-01-07 at 12.42.55 PM.png
Figure: Screenshot 2026-01-07 at 12.42.55 PM.png Click to zoom ↗

So we have 4 arguments.

KeyBuffer, KeyLength, argGpcsBuffer, argGpcaLen

They go in reverse order.

Screenshot 2026-01-07 at 12.58.38 PM.png
Figure: Screenshot 2026-01-07 at 12.58.38 PM.png Click to zoom ↗

The first argument is going to be a key

renaming it to key

Screenshot 2026-01-07 at 1.03.39 PM.png
Figure: Screenshot 2026-01-07 at 1.03.39 PM.png Click to zoom ↗

Key length is 0x10 or 16 bytes.

Screenshot 2026-01-07 at 1.04.21 PM.png
Figure: Screenshot 2026-01-07 at 1.04.21 PM.png Click to zoom ↗

EBX is our value of gpca.dat, and EAX - the length of that buffer.

Template for Decryption

JSX
#!/usr/bin/python3
# - common decryption
# - identifying the crypto algorithm
# - using a decoding framework 

from decoder_core import *
from Crypto.Cipher import AES, Salsa20, ARC2, ARC4, Blowfish, DES3, DES

class Decoder(TrainingDecoder):
    def __init__(self):
        # MD5 hash of the correctly decrypted file
        super().__init__()

    def decode(self):
        # self.data is a bytearray with the contents of the file

        # 1. Insert the key as a hexadecimal string:
        key = bytes.fromhex(REPLACE_ME_HERE)

        # 2. Identify the decryption algorithm and use it
        # examples:
        # cipher = AES.new(key, AES.MODE_EAX)
        # cipher = ARC4.new(key)
        # cipher = Salsa20.new(key=key)
        # cipher = ARC2.new(key, ARC2.MODE_CFB)
        # cipher = Blowfish.new(key, Blowfish.MODE_CBC)
        # cipher = DES3.new(key, DES3.MODE_CFB)
        # cipher = DES.new(key, DES.MODE_OFB)
        cipher = REPLACE_ME_HERE # <-- Create a proper decryption class here

        # If you use the correct algorithm and the key, the decrypted data will be written
        # in a .dec file
        self.data = cipher.decrypt(bytes(self.data))

        # Check if the results are correct
        self.check_results()

# Create the decryptor object. Automation happens in __init__()
Decoder()

Extraction of the key.

Screenshot 2026-01-07 at 4.40.43 PM.png
Figure: Screenshot 2026-01-07 at 4.40.43 PM.png Click to zoom ↗

Or we can create an array. Ida automatically recognize the boundaries.

Screenshot 2026-01-07 at 4.42.53 PM.png
Figure: Screenshot 2026-01-07 at 4.42.53 PM.png Click to zoom ↗

press ok there will be now an array. copy the key.

Screenshot 2026-01-07 at 4.44.12 PM.png
Figure: Screenshot 2026-01-07 at 4.44.12 PM.png Click to zoom ↗

Extract data hex string

Screenshot 2026-01-07 at 4.49.04 PM.png
Figure: Screenshot 2026-01-07 at 4.49.04 PM.png Click to zoom ↗

key:

JSX
4E381FA77F08CCAA0D56EDEFF9ED08EF

Decryption:

Screenshot 2026-01-08 at 12.07.02 PM.png
Figure: Screenshot 2026-01-08 at 12.07.02 PM.png Click to zoom ↗

Opening the decrypted file in hex editor.

We have a readable strings and these were swift transactions.

Screenshot 2026-01-08 at 1.13.33 PM.png
Figure: Screenshot 2026-01-08 at 1.13.33 PM.png Click to zoom ↗

Some more Interesting strings.

Screenshot 2026-01-08 at 1.16.55 PM.png
Figure: Screenshot 2026-01-08 at 1.16.55 PM.png Click to zoom ↗
Screenshot 2026-01-08 at 1.17.35 PM.png
Figure: Screenshot 2026-01-08 at 1.17.35 PM.png Click to zoom ↗
Screenshot 2026-01-08 at 1.17.45 PM.png
Figure: Screenshot 2026-01-08 at 1.17.45 PM.png Click to zoom ↗

First bytes are D0 C0 B0 A0

Screenshot 2026-01-08 at 1.18.39 PM.png
Figure: Screenshot 2026-01-08 at 1.18.39 PM.png Click to zoom ↗

If we go back to IDA.

After calling RC4. The code reads the first DWORD of the decrypted buffer at address 004013FC and it verifies if there is a particular magic value. that is exactly D0 C0 B0 A0 or just if you rotate in little-endian.

This assures we have Decrypted the whole file correctly.

Screenshot 2026-01-08 at 1.20.54 PM.png
Figure: Screenshot 2026-01-08 at 1.20.54 PM.png Click to zoom ↗

Decryption Program:

JSX
#!/usr/bin/python3

from decoder_core import *
from Crypto.Cipher import AES, Salsa20, ARC2, ARC4, Blowfish, DES3, DES

class Decoder(TrainingDecoder):
    def __init__(self):
        # MD5 hash of the correctly decrypted file
        super().__init__()

    def decode(self):
        # self.data is a bytearray with the contents of the file

        # 1. Insert the key as a hexadecimal string:
        key = bytes.fromhex(REPLACE_ME)

        # 2. Identify the decryption algorithm and use it
        # examples:
        # cipher = AES.new(key, AES.MODE_EAX)
        # cipher = ARC4.new(key)
        # cipher = Salsa20.new(key=key)
        # cipher = ARC2.new(key, ARC2.MODE_CFB)
        # cipher = Blowfish.new(key, Blowfish.MODE_CBC)
        # cipher = DES3.new(key, DES3.MODE_CFB)
        # cipher = DES.new(key, DES.MODE_OFB)
        # cipher = ARC4.new(key) # <-- Create a proper decryption class here

        # If you use the correct algorithm and the key, the decrypted data will be written
        # in a .dec file
        self.data = cipher.decrypt(bytes(self.data))

        # Check if the results are correct
        self.check_results()

# Create the decryptor object. Automation happens in __init__()
Decoder()
Copied