{
	"id": "7bd80a7b-e07c-4d1b-b431-a5db4f0c1c6b",
	"created_at": "2026-04-06T00:11:30.865017Z",
	"updated_at": "2026-04-10T13:12:24.187327Z",
	"deleted_at": null,
	"sha1_hash": "e0690224caa1d44810faf4a9f11e148ec1c65e61",
	"title": "Analysing ISFB – The First Loader | 0ffset Training Solutions",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 12274068,
	"plain_text": "Analysing ISFB – The First Loader | 0ffset Training Solutions\r\nBy 0verfl0w_\r\nPublished: 2019-03-13 · Archived: 2026-04-05 20:44:08 UTC\r\nI’m finally getting round to writing this post – for the past few months I have been analysing different versions of\r\nISFB/Ursnif/Gozi to gain a deeper understanding in the functionality of this specific malware. In this post, I will\r\nbe detailing how to unpack and then analyse the first stage loader executable, and then use that information to\r\nextract the second stage loader DLL, known as rpcrt4.dll, which we will analyse in a later post.\r\nIn a nutshell, ISFB is a banking trojan used to steal financial information from unsuspecting victims. It utilizes\r\nseveral methods to do so, from stealing saved passwords to injecting JavaScript into predetermined websites. This\r\nspecific sample of ISFB is version 2.14.60, and can be attributed to a specific ISFB v2 group based on the\r\ninfection routine used – specifically the macros that execute a powershell command that is simply Base64\r\nencoded. The group behind this sample also reuse the encryption key for different campaigns (the default key),\r\nmaking their samples easily identifiable compared to other large groups utilizing ISFB. I have been unable to\r\nlocate specific threat actor names, and as a result, I will be referring to this group as Group 53, based off of this\r\npresentation by FireEye.\r\nSimilarly to other groups utilizing ISFB for financial gain or simply as a stager, Group 53 gains a foothold on the\r\ntarget’s system using a malicious Word document containing embedded macros, which in turn lead to the\r\nexecution of a Powershell script responsible for downloading the first stage executable. Certain groups “partner”\r\nwith other groups that are able to distribute malicious spam (malspam) on a large scale, such as the group behind\r\nHancitor. This could potentially result in larger infection numbers, compared to those groups that are relying on\r\ntheir own distribution methods. I would assume “renting” a spot from the group behind Hancitor would be quite\r\nexpensive as a result of its enormous outreach, which is why a lot of groups, including Group 53, have to\r\ndistribute their own malicious documents. I will be focusing on the unpacker executable and the first stage\r\nexecutable loader in this post, rather than the Word Document itself, as its functionality is quite straightforward.\r\nAs always, the samples have been uploaded to VirusBay. So, let’s crack it open!\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 1 of 61\n\nBut, before I do I’d like to thank all of the people who helped me out in analysing the different samples of ISFB,\r\nincluding @VK_Intel, @Nazywam, and @Maciekkotowicz (for his great papers on ISFB). Anyway, let’s get on\r\nwith the reversing!\r\nPart 1: Unpacking the first stage executable\r\nMD5 of First Stage Executable: bc72604061732a9280edbe5e2c1db33b\r\nTypically I would open it up in PEStudio or perhaps perform some static analysis, however at this point I have\r\nalready determined that it is highly likely to be a sample of ISFB, based on the Word Macro, although we still\r\nwant to unpack the first EXE to be 100% sure that it is in fact ISFB. First, let’s open it in IDA to try and find a call\r\n(or jmp) to a memory region or register – this could possibly be a call to the unpacked stage.\r\nIt definitely looks like there is some unpacking going on here (based on the length and intricate flow shown in the\r\nimage above), and upon looking at the strings in the binary we can see that there aren’t many that are legible, or\r\nmeaningful. Normally when unpacking a sample, I start at the bottom and work my way up – most unpackers exit\r\nonce the file has been unpacked – although this depends. In this case, the unpacker performs Self-Injection, and\r\noverwrites itself with the unpacked file. This is not unusual for ISFB, and if you analyse some other samples (even\r\nfrom other groups), you will most likely find this occurring too. This means that the unpacker does not exit until\r\nthe unpacked file does, although we can assume that the last function called will transfer execution over to the\r\nunpacked executable.\r\nIn this specific file, there is no call or jmp to a register or memory region – however, there is a call to loc_42A880.\r\nIt’s the last call in WinMain, so as mentioned before, this function is most likely responsible for transferring\r\nexecution over to the EXE.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 2 of 61\n\nWhen we jump into this function, it is clear that it hasn’t been converted to a function yet, and so we are unable to\r\nview it as a graph. Sometimes, you are able to turn it into a function by pressing “P”, although it doesn’t seem to\r\nwork in this case. So, we will have to deal with the text mode. It is definitely possible to follow the jumps and\r\nconditional jumps to try and find the call or jump to our executable, but we can speed this up a bit. Locate a call\r\ninstruction and click it – this should highlight the instruction and other instances of it. This doesn’t work for every\r\nsample, but it is worth a try to speed things up. Simply scroll down or up from where you are, looking for other\r\ninstances of call. The last call I located took lpAddress as the first argument, so let’s take a look at this.\r\nlpAddress indicates that it contains an address to a region of memory, meaning it could contain the address of our\r\nunpacked executable.\r\nInside this function, if we jump straight to the end, we can see a call to ESI. Hit space to jump from the Graph\r\nview back to Text view in order to get the address of this call. This could be the call to our unpacked EXE, and so\r\nwe need the address of it to view it in x32dbg and put a breakpoint on it.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 3 of 61\n\nSo the memory address to this call is 0x0042A653, so let’s now open this up in x32dbg and jump to this address.\r\nSimply push CTRL-G in x32dbg and type in 0042A653, and then hit enter – this will jump to that address,\r\nallowing you to put a breakpoint on it. Whilst attempting to unpack a sample, I prefer to open up Process Hacker\r\nalongside it, so that if I put a breakpoint on the wrong address and the unpacked process executes, I can easily\r\ndetect it, either through the Network tab or through the Processes tab.\r\nOnce the breakpoint has been hit, step into ESI and in this sample there is what seems to be a second unpacking\r\n“stage” – this code was not here originally, in fact if we try and view it in IDA, you will simply see a lot of\r\nquestion marks and the variable unk_58FC08.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 4 of 61\n\nAs it is difficult to statically analyse this section without dumping it and opening it up in IDA, I will be jumping\r\nover functions, rather than stepping through them manually. This will also help to speed things up, but make sure\r\nyour network adapter is not attached, as one of these functions may execute the executable.\r\nIn the GIF below, you can see there is some form of loop going on, with XOR being used to XOR a byte at the\r\nmemory address stored in EAX, with the value in DL. You might also notice that the assembly is changing as each\r\nloop goes on – this is another example of the second unpacking stage. We can simply put a breakpoint on the jmp\r\nand then run the program until it hits it.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 5 of 61\n\nOnce we follow the jump, we are met with several calls to [ebx+xxxxxx]. Each of these could jump to the\r\nunpacked EXE, however as we progress further on, it is clear that these are simply calls to Windows API\r\nfunctions. Notice the call to EDI? EDI is pointing to a function that dynamically imports these APIs so that they\r\ncan be called by the unpacking stub. The result is stored in EAX, and as seen in the image below, this specific call\r\nimported the API RtlExitUserThread.\r\nScrolling down a bit, we can see a jmp eax, so let’s put a breakpoint on that and run to it, and then follow the\r\njump.\r\nThis jump takes us to a newly allocated region of memory, with more code. I didn’t want to examine each\r\nfunction, so I scrolled down until the ret instruction, and started examining the (local – not API calls) functions\r\nbackwards. The last function did not seem likely to be the executable “executor”, as there was no call or jump\r\ninstruction to a different region of memory, however the second last function had some calls to [ebp+14] and\r\n[ebp+C], so let’s put a few breakpoints on those.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 6 of 61\n\nHowever, upon executing the program and hitting the breakpoint, it is clear that they are simply calling\r\nLoadLibrary and GetProcAddress.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 7 of 61\n\nTherefore, we can simply return from this function, as it is just importing API’s, which isn’t extremely important\r\nto us yet. After exiting the function, there are several calls to API’s, in particular VirtualProtect. VirtualProtect\r\nis responsible for changing the permissions/protection of different memory regions, so that they are readable,\r\nwritable, or executable (can be one, two, or all three). In this case, we can see that VirtualProtect is being used to\r\nchange the protection of the memory region at 0x00406000, which is the .BSS section. As I mentioned before, this\r\nunpacker overwrites itself with the unpacked executable, so it is safe to say that we are extremely close to the\r\nunpacked EXE. We could dump it now, but there could be some extra unpacking going on, so let’s wait until we\r\njump to the 0x00400000 memory region.\r\nAt this point, there isn’t much going on in the function, so we can simply put a breakpoint on the ret instruction\r\nand return from this function.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 8 of 61\n\nUpon exiting the function, you’ll notice the memory address is in the 0x00401000 region. This is the unpacked\r\nprogram! Now we can dump it out, so make sure you have Process Hacker open, although you can dump it from\r\nx32dbg.\r\nTo dump it from memory, simply double click the process in Process Hacker, and right click the 0x400000 region\r\nof memory, and select Save.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 9 of 61\n\nNow we can open it up in PEBear and unmap the dumped file. Upon doing so, you can see that the imports have\r\nnot been successfully resolved by PEBear. This is why we need to unmap the file. When a program is about to be\r\nexecuted, it needs to be mapped in memory, so that it can be interpreted correctly. Therefore, PEBear is unable to\r\nresolve the imports until we unmap it.\r\nTo do so, we simply change the Raw Addr. so that it matches the Virtual Addr., and then change the Raw Size\r\naccordingly. This should result in something looking like the image below.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 10 of 61\n\nWe can check the imports and sure enough, there are 4 imported DLL’s, meaning we have the correctly unmapped\r\nfile. We can now save this to the desktop and congratulations! You have now successfully unpacked the first stage!\r\nLet’s open it up in IDA for further analysis!\r\nPart 2: Analysing the Dumped Executable\r\nMD5 of Dumped Executable: 0063316975e55c765cd12e3d91820478\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 11 of 61\n\nUpon opening the file in IDA, we can see that the main function only calls one local function and then exits, so\r\nit’s not too difficult to find the malicious code. If you’re not sure if a certain sample is ISFB or not, one telltale\r\nsign can be found in the strings window. Most, if not all, ISFB payloads (version 2 – haven’t taken a look at\r\nversion 3 yet so not sure) store a compile/campaign start date in plaintext that is used for string decryption. In this\r\nsample, the date is Jan 28 2019, so a relatively new sample at the time of writing this. I will go over the string\r\ndecryption method soon, but first let’s take a look at what happens first in the function.\r\nStrings\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 12 of 61\n\nThere are several functions called inside this function, so let’s take it section by section, so first let’s take a look at\r\nthe first four functions. From the image below, it is clear that the return value of sub_401C69 (stored in eax)\r\nneeds to match the value in esi, otherwise it will jump to the exit. The second called function – sub_401E4F –\r\nseems to do the same thing. The third function seems to be a check for something, as 1 is moved into a DWORD\r\nbased on the result of a bit-wise AND (test performs a bit-wise AND on the two values, however it just sets flags\r\nbased on the result, which is not stored) on eax. Finally, the fourth function seems to act in the same way as the\r\nfirst and second function, in the sense that the returned value is compared to the value in esi, and it will exit if the\r\nconditions are not met. Anyway, enough assuming, let’s actually take a look at these functions.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 13 of 61\n\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 14 of 61\n\nTaking a look at the first function (the main bit), we can see that the malware is opening it’s own process and\r\nstoring the handle in a DWORD, which is set to -1 if the malware failed to open the process. This then returns\r\nback to the main function, where the returned result is compared against the value in ESI.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 15 of 61\n\nThe second function is where things start to get interesting. In the image below, you’ll notice the compile date\r\nbeing moved into ESI, which, as I mentioned before, is used for string decryption. You might already know this,\r\nbut for those that don’t, ISFB contains a .BSS section, which contains multiple strings that are all encrypted using\r\na ROR-XOR algorithm. The XOR key is calculated based on a given date, and in order to decrypt the strings,\r\nISFB needs to perform the calculations again to get the correct key, allowing us to easily reverse it. But first, let’s\r\ntake a look at the two functions called beforehand.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 16 of 61\n\nLooking at the first function, you’ll notice ssb. (endian issues, as it is stored as hex – actually is .bss) being\r\ncompared to the value at [ECX], inside a loop. In order to decrypt the strings in the .BSS section, ISFB must first\r\nlocate the .BSS section, and in order to do so, it simply reads it’s own PE header and gets the size and address of\r\nthe required section. If the value at [ECX] doesn’t match .bss, 40 is added the the memory address in ECX, due to\r\nthe fact that the spacing between the section descriptors/structures (.text, .data, etc.) is 40 bytes. The loop will the\r\ncontinue. If there is a match, the length of the string is checked, making sure that it is not longer than 4 bytes. The\r\nmalware does this by checking the byte after the string, and comparing it to zero. If the string is the correct length,\r\nthe memory address pointing to “.bss” is moved into EDX. If everything is successful, ISFB will get the address\r\nand size of the .BSS section and store it in memory. In this case, the address is 0x6000 (add this to the image base\r\nand you will be able to locate it), and the size is 0x1000. If this is still difficult to understand, there is an image of\r\nwhat the section table looks like in x32dbg – this should help you to understand how it is able to get the address\r\nand size.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 17 of 61\n\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 18 of 61\n\nFunction: sub_401C0F #2\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 19 of 61\n\nWith that function analysed, let’s move onto the next one. The main purpose of this function is to add a vectored\r\nexception handler using AddVectoredExceptionHandler. The second argument is a pointer to the handler\r\nfunction that will be executed when the program runs into an exception, so lets take a look at the function\r\nException_Handle_Function.q\r\nThis function checks the value of the exception – whether it is an EXCEPTION_ACCESS_VIOLATION or\r\nEXCEPTION_SINGLE_STEP, however they both end up executing the same function, so lets move into that\r\nfunction.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 20 of 61\n\nThe important part of this function happens soon after it is executed. In the image below, you’ll be able to make\r\nout a loop, as well as two XOR instructions and a ROR instruction. This is the BSS section decryption – the XOR\r\nkey that is created later on is actually arg_8, so take note of that. So, now we know that there will be an exception\r\nthat is caused at some point, which will in turn execute the BSS string decryption function. Now we have this\r\ninformation, we can move onto reversing how the XOR key is created from the compilation/campaign date.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 21 of 61\n\nAs the XOR key is based off of the date, thats where we need to look. First, the address of the BSS section\r\n(0x6000) is moved into EAX, and the size of it (0x1000) is moved into EDX. Then, the memory address of the\r\ncompile date is moved into ESI, and a memory address (pointing to an empty section of memory) is moved into\r\nEDI. The compile date is then moved into that empty region of memory, using MOVSD, which moves a DWORD\r\nfrom the memory address in ESI to the memory address in EDI. Next, the first DWORD of the compile date is\r\nmoved into ECX, and this is XOR’ed against the second DWORD of the compile date. Then, the result of this is\r\nadded to the address of the BSS section (0x6000 here) and the value 0xE. This is then pushed as the first argument\r\nfor the next function that will be called.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 22 of 61\n\nTaking a look at the function, arg_0 (the XOR key) is used in a Rotate Left instruction. Here, we see BL being\r\nmoved into CL, which is incremented by 1 and used to rotate the XOR key left by 1. This results in the final XOR\r\nkey that is used to decrypt the BSS strings.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 23 of 61\n\nOnce we have performed these calculations, we get this as the key: 0x249d730c. Whilst it is possible to get the\r\nstrings from a debugger and copy it over to an IDA instance, I prefer to replicate custom routines using Python.\r\nI’m currently polishing up the decryption script I have been using, and when it is complete, you can get it here\r\n(my GitHub). Simply put, the algorithm decrypts the data in DWORDs, using a mixture of XOR’s and rotate right\r\n(ROR) instructions. First, we’re going to want to copy out the data in the BSS section. View it in the hex editor\r\nmode, select the entire section (where there is data), and then go Edit-\u003eExport Data, and you can copy the\r\nunspaced data. Next, we need to parse this blob of data so that we can decrypt it.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 24 of 61\n\nTo parse it, I will be using python. All we need to do is split the hex bytes by 4, and store that in a list. This means\r\neach value will be a DWORD. If you follow the commands below, you should be able to get the output seen in the\r\nimage.\r\nNext, we’re going to use CyberChef to do a bit more fixing, although we will be replacing different characters, so\r\nthat there is an 0x before every DWORD, and so Python won’t treat the list as a list of strings.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 25 of 61\n\nSo now you should have a suitable list of hex DWORDs which we can then decrypt. In order to decrypt it, you\r\nsimply have to copy it into the script and make sure the key is correct, and then it will decrypt and spit out the raw\r\ndata, as well as a list of integers, and you will see why just now. In the image below, you can see the decryption\r\npart of my script, which isn’t too complicated, so if it hasn’t gone up on my GitHub yet, it shouldn’t be too\r\ndifficult to replicate.\r\nOnce the script has decrypted the data, the output looks like this:\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 26 of 61\n\nSo now we can copy the decrypted strings over to IDA. There is an extremely basic IDA Python script in the\r\nimage below, which overwrites bytes at dest with bytes of data in data. The list data will contain the integer\r\nvalues that our script printed to the terminal, so you can simply copy it from there and paste it into the document.\r\nFrom there, we need to import it as a script/module in IDA, so you can do that either by importing the file, or by\r\ncopying the text and pasting it into the box as shown below. All you need to do then is click Run, and it should be\r\nimported – although we physically have to call the script, Run won’t execute it – at least not in this case.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 27 of 61\n\nUpon typing PatchArr() into the console and hitting enter, the script should overwrite all bytes in the BSS section\r\nwith our decrypted strings, so you should see something similar to the image below.\r\nWe can then reanalyse the payload, by going Options-\u003eGeneral-\u003eAnalysis-\u003eReanalyze Program, and it should\r\nrecognize most of the strings, although there will be the occasional error.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 28 of 61\n\nWith the newly decrypted BSS section, we should be able to analyse the rest of the payload without issues.\r\nOnce the strings have been decrypted, ISFB then calls a function that utilizes one of the decrypted strings –\r\nIsWow64Process. As you probably have guessed, this checks to see if the architecture of the system is 64 bit or\r\nnot. The result (stored in EAX – 1 if x64, 0 if not) is then tested. If the system is not 64 bit, the variable var_4 in\r\nthe graph below is used in an AND operation with EAX, which would be equal to 0, meaning the value in var_4\r\nwould be 0. If the system is 64 bit, this is skipped. Then, regardless of the system architecture, the value in var_4\r\nis moved into EAX, which is the return value.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 29 of 61\n\nIf we back out of the function, we can see EAX is tested again, and if the result is not zero, the value 1 will be\r\nmoved into the DWORD dword_405478. As the system I am running on is 64 bit, dword_405478 will contain\r\nthe value 1. If we search for cross references to this DWORD, we can find test instructions that use it. Therefore\r\nwe can determine this is an indicator of the architecture.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 30 of 61\n\nThe next function that is called is quite complex. In a nutshell, this function is responsible for getting the address\r\nto a select few API calls. To do this, it uses a list of predefined values, a “key”, and a hashing routine. First, let’s\r\ntake a look at the key and find out what it is. Looking at the image below, just before the value in hModule is\r\nmoved into ECX, EAX is XOR’d with a DWORD inside the binary, however upon viewing this DWORD, it is\r\nempty – this means it is resolved dynamically.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 31 of 61\n\nTo find out what this DWORD will contain, we need to find cross references to it – specifically, we are looking\r\nfor a reference to it being used as the destination in a MOV instruction. Luckily enough for us, it only appears\r\nonce as the destination, inside the BSS Decrypt function. From the image below, we can see that a DWORD from\r\na string is moved into ECX, which is then used in a subtraction and addition instruction. In this case, it may be\r\nmuch easier to understand what is going on by looking at it in a decompiler.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 32 of 61\n\nSo, from the looks of it, we can get the key used by performing a simple operation, as seen below:\r\nkey = (dword string[4] + dword string[0]) - dword string[12]\r\nkey = (\"ion=\" + \"vers\") - \"oft=\"\r\nWe need to convert these values to hex to perform addition and subtraction:\r\nkey = (0x696f6e3d + 0x76657273) - 0x6f66743d\r\nkey = 0x706E6C73\r\nConvert the key from hex and we get this: \"pnls\"\r\nWe can double check this by looking at the binary in a debugger, as shown in the image below.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 33 of 61\n\nNow we have the key, let’s take a look at the values used for determining which API to import. This is quite\r\nsimple to find, as it is already inside the executable. We can also see that EDI is being used as a counter, as each\r\nloop it is incremented by 4, until the value reaches 20, where it returns. This means there are 5 API’s in total that\r\nare looked up using this method. The values in the embedded list are all XOR’d by the key, which results in the\r\nhash lookup value used for comparison. So, from this, we can start examining the hashing algorithm to see how it\r\nfunctions and to locate what API’s are lookup up and stored.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 34 of 61\n\nAs the function is quite large, I will focus on the section that calls the hashing function. The three arguments\r\npassed to this function are; the address of the DLL, the value 0, and the correct hash for comparison. The function\r\nuses the base address to perform some calculations in order to get to the export table inside the DLL. From there,\r\nit loops through each of the exports and hashes the name of the export, which is then compared to the predefined\r\nhash. If the hashes match, the function retrieves the address to the exported API, and overwrites the predefined\r\nhash with the address for later use. If they do not match, the function simply continues onto the next export, until a\r\nmatch is finally discovered.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 35 of 61\n\nTaking a look at the actual hashing function, it is quite difficult to come to the conclusion (especially for a\r\nbeginner) that the function is responsible for hashing, until it is run in a debugger. There is definitely something\r\nhappening in this function from a static analysis perspective, as there are multiple logic instructions, but we can’t\r\nknow for sure until we analyse it further.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 36 of 61\n\nTaking a look in a debugger, it is clear that [edx*4+404108] and [ecx*4+404108] are values from a lookup table,\r\nas the XOR values used constantly change, however the values are repeated, and therefore we can determine that\r\nthey are not randomized. Base64 encoding/decoding use lookup tables as well, so if you have looked at that before\r\n(or at least the psuedocode), you might be able to recognize the lookup table aspect here. When we take a look at\r\nthe memory region where the lookup table is located, it is easy to see where it begins and ends. So, now we know\r\nthat there is a lookup table, how do we find out the hashing mechanism in use?\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 37 of 61\n\nTypically, custom encryption and hashing implementations are quite difficult to determine, unless you know what\r\nyou are looking for. As long as the algorithm is publicly available (such as AES or Serpent), and not a custom\r\ndeveloped one by the author, there will almost always be specific values or instructions that stick out to those who\r\nhave looked at crypto before – these are known as Constants. We will revisit these later (in the next post) when\r\nlooking at the other encryption methods used in this sample, however to sum it up, it basically refers to values that\r\nmust be used in the algorithm to achieve the correct results. In this case, let’s take one value from the lookup table:\r\n0xA00AE278. We can run a quick search for this value online, and as you can see from the image below, this is\r\ndefinitely linked to CRC-32, although it is more of a variant – we just need to find out which variant.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 38 of 61\n\nIn this specific sample, the program loops through the NTDLL.DLL export functions and hashes them all until it\r\nfinds a matching hash. What makes hashing worse is that hashes are irreversable, meaning the only way you can\r\nfind the matching hash is by brute forcing – to do this you could hash each API exported by NTDLL until you\r\nfound the matching hashes, but this is too time consuming, and it is quite simple to find the matching API anyway\r\n– all you have to do is put a breakpoint on the instruction that is hit if a match is found, and execute the program.\r\nOnce the breakpoint is hit, you’ll be able to locate the correct API. From the image below, you can see the first\r\nAPI call that matches is ZwGetContextThread. We can now use this to get the variant of CRC-32 used.\r\nIn order to do so, we can use this site, which is extremely useful. It hashes the input using several different\r\nvariants of CRC-8, CRC-16, and CRC-32, allowing us to compare the value from the debugger to the output of\r\nthe different variants. With the API name, ZwGetContextThread, and the required output, 0x5A3D66E4, we can\r\nfind the variant used: CRC-32/JAMCRC.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 39 of 61\n\nAs we can find out the matching APIs, there is no need to write a brute force script, although for families such as\r\nGootKit, this is sometimes necessary. So, the 5 APIs imported are: ZwGetContextThread, ZwSetContextThread,\r\nZwReadVirtualMemory, ZwWriteVirtualMemory, and ZwAllocateVirtualMemory. With that, we can move out of\r\nthis function, and onto the next.\r\nThe next function simply retrieves the file name to store in memory, potentially for use later on in the sample. The\r\nnext function gets much more interesting, so lets move onto that.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 40 of 61\n\nIf you’ve looked at ISFB or read anything about it before, you might be aware of an embedded FJ, F1, or JJ\r\nstructure that is located just after the section table. This structure contains pointers to appended (also known as\r\njoined) data, such as configuration information, an RSA public key, or even another executable. There are\r\ndifferences between FJ, F1, and JJ, although they are quite small changes. In this case, the sample is using an\r\nembedded JJ structure, identifiable from the magic value 0x4A4A (“JJ”).\r\nThe format of this structure is shown below:\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 41 of 61\n\nJoined Resource Structure {\r\nWORD Magic Value\r\nWORD Flags\r\nDWORD XOR Key\r\nDWORD CRC-32 Hash\r\nDWORD Address\r\nDWORD Size\r\n}\r\nThe parsed structure in this binary can be seen below:\r\nJoined Resource Structure {\r\nWORD 0x4A4A\r\nWORD 0x0001\r\nDWORD 0xD4B1C68F\r\nDWORD 0x9E154A0C\r\nDWORD 0x00008400\r\nDWORD 0x0000B600\r\n}\r\nThe function we are looking at is responsible for parsing this structure and locating the needed data. The main\r\nargument for this function is the third one, which is the result of an XOR between the “pnls” string and an\r\nembedded hex value. The first and second arguments are simply the start and end of an empty region of memory,\r\nso they are not that important right now.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 42 of 61\n\nThe first half of the parsing function traverses through the MZ and PE header until it has reached the resource\r\nstructure located just after the section table. It then checks that the magic value is in fact “JJ” (0x4A4A), otherwise\r\nit will add 0x14 to the current address and try again.\r\nOnce the structure has been located successfully, the function compares the third argument (0x9E154A0C) with\r\nthe embedded CRC-32 hash, which is also 0x9E154A0C. If these do not match, the function will return or loop\r\naround again. If they do match, the function performs a bitwise AND on the structure flag and 0x2, which must\r\nreturn 0, otherwise the function will loop or return. If 0 is returned, a heap is allocated based on the size value\r\nstored in the structure. From there, it will get the full memory address of the joined data by adding the address in\r\nthe structure to the base address of the executable, and then a bitwise AND is performed on the structure flag and\r\n0x1. If the result is 0 the joined data is not compressed and is encoded, and if the result is not 0 (which in this case\r\nit is 1), the joined data is compressed. As the joined data is compressed in this binary, we will focus on the\r\ncompression method.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 43 of 61\n\nThe two arguments pushed to the function are the location of the compressed data, and the address of the newly\r\nallocated heap.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 44 of 61\n\nLooking at an overview of the function, you can see it is quite intricate and difficult to follow. Remember what I\r\nwas saying about crypto constants earlier? They are also present in some, if not all, (de)compression routines,\r\nalthough it is much more difficult to identify the algorithm compared to encryption algorithms.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 45 of 61\n\nSearching through the graph, there are calls to the same functions several times over, but we are looking for a\r\nhardcoded value that is not stored in memory. Sure enough, after searching through the right branch, we can see a\r\nCMP instruction being used, comparing the value in esi to the value 0x7D00. Using this, we can search “0x7d00\r\ncompression” on Google and after scrolling down a bit, come across this site. Comparing this python code:\r\ndef lengthdelta(offset):\r\n if offset \u003c 0x80 or 0x7D00 \u003c= offset:\r\n return 2\r\n elif 0x500 \u003c= offset:\r\n return 1\r\n return 0\r\nTo a section of the assembly:\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 46 of 61\n\nWe can see some clear similarities. From this, we can deduce that the decompression algorithm used here is\r\nAPLib, and with this knowledge we can write a simple extraction script to extract any joined data from the binary\r\nand decompress it using the great Python library Mlib, created by Mak. You might be wondering how I was able\r\nto determine if something was decompressed before even looking at the function. I have read up on ISFB and\r\nhave also analysed it quite frequently, however it is quite easy to figure out that the joined data is compressed by\r\nsimply looking at it. Opening up the binary in a hex editor and going to the location of the joined data, you might\r\nbe able to recognize the string “This Program Cannot Be Run In DOS Mode”, although part of it is obfuscated.\r\nRecognizing compression also depends on the level of compression, however for the ISFB malware strain, APLib\r\nis the defacto compression method for now, so it is relatively simple.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 47 of 61\n\nBacking out from the decompression function, the program makes sure that the size of the decompressed\r\nexecutable is the same size as the value stored in the JJ structure. If the sizes do not match, the decompressed\r\nexecutable is cleared from memory, using HeapFree(). Then, the malware XORs the first DWORD of the\r\ndecompressed data with the XOR key stored in the structure. Taking a look at the decompressed data, we can see\r\nthat the first DWORD of the executable is not valid – it should look like 00009000, but instead looks like\r\n54205BD4. After the XOR, the valid value can be seen. The function cleans up after itself, and then returns back\r\nto the calling function. If you were to dump the executable at this stage and add “MZ” to the start, you would be\r\nunable to open it in PE Bear – this is due to the fact that not only is “MZ” missing, “PE” is also missing from the\r\nheader, so make sure you add that as well.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 48 of 61\n\nBack in the calling function, the malware then overwrites the decompressed executable using a rolling XOR\r\nalgorithm. I’m not exactly sure why it does this, as the executable is later decrypted, so if someone could let me\r\nknow that would be great! Anyway, once the executable has been encrypted, the function carries on.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 49 of 61\n\nFirst, it creates a new file mapping, and calls MapViewOfFile, and then copies it’s filename to the newly mapped\r\nregion, before calling UnmapViewOfFile. Again, I’m not too sure why this is being called, as it doesn’t seem to do\r\nanything, but if someone could drop an answer to this that would be great, and I can incorporate it into the post.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 50 of 61\n\nMoving on, the final function in this sample is called. This is quite a large function, so I will attempt to summarize\r\nthe main points. Firstly, the function begins by moving different pointers around, and then compares the first 2\r\nbytes of the encrypted executable to the value 0x5A4D (MZ) to check if the data is encrypted or not. As it is\r\nencrypted, the result will not be 0, and so it will allocate a heap based on the size seen in the embedded JJ\r\nstructure. Next, it will call another function responsible for performing another rolling XOR algorithm, that will\r\ndecrypt the data. This is a bit different to the last one, but should return the same executable that was\r\ndecompressed earlier. Then, it will locate the value in the PE header that indicates the architecture of the\r\nexecutable, which in this case is 0x14C, meaning it is x86 based.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 51 of 61\n\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 52 of 61\n\nNext, the malware will allocate a brand new section of memory using NtCreateSection(), which will be set to\r\nRead-Write-eXecute. Eventually, this will contain the decompressed executable.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 53 of 61\n\nFrom there, an address located inside the newly created section of memory will be formed, pointing to\r\n0x0022EC50, which will be used later. The created section of memory is replicated, to the address 0x00240000.\r\nNext, the program begins to copy over the executable to the new addresses, however it skips the entire MZ header\r\nand simply copies everything from the PE header.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 54 of 61\n\nThen, a function is called that imports some more API calls from NTDLL, however this time it is not done using\r\nCRC hashes, and the name is simply passed as an argument. The imported API calls are; LdrLoadDll,\r\nLdrGetProcedureAddress, and ZwProtectVirtualMemory. The pointers to the addresses are stored in the same\r\nregion of memory, split by 4 null bytes each. These addresses are then copied over to the executable at\r\n0x00220000 for usage during it’s execution. Next, a region of code is copied over to the executable in memory,\r\njust 40 bytes after the loaded APIs. This is what will be called before the next stage is completely executed.\r\nContinuing on, the next function is responsible for passing execution over to the executable. There are 2 functions\r\nthat pass over execution to the executable, however they depend on the architecture. In this case, I will be looking\r\nat the x64 version. It is quite simple, as all it does is call the function at the address 0x0022E040, which will\r\nprepare the next stage. Therefore, you can think of this stage as another “unpacker”, as all it does is unpacks the\r\nexecutable, however there are many functions that carry over to the next stage, making it much easier to analyse.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 55 of 61\n\nWhen we step into the function, there is an int3 waiting for us, which acts as a breakpoint that raises an exception,\r\nstopping us from stepping over it. To fix this, we can put a breakpoint on the previous call, restart the debugger\r\nand then run it until the breakpoint is hit. This should hopefully remove the int3.\r\nAs it didn’t remove it in my case, I will use Immunity Debugger to carry on this section. The code is responsible\r\nfor importing DLLs using the previously imported API’s – First, there is a loop which stores a DLL name in\r\nmemory that will be loaded using LdrLoadDll. Then, a list of hardcoded APIs are looped over, with each being\r\npassed to LdrGetProcedureAddress. The DLLs that are loaded are; NTDLL.DLL, KERNEL32.DLL, AND\r\nOLEAUT32.DLL.\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 56 of 61\n\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 57 of 61\n\nOnce the APIs have been imported, ZwProtectVirtualMemory is called several times in a loop to alter the\r\nprotections on several regions of memory.\r\nIn order to jump to where the execution of the payload happens, we want to put a breakpoint on a call to a register,\r\nwhich in this instance is EBX. Run to that, and then step into it, and you should find yourself in the next stage of\r\nISFB!\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 58 of 61\n\nAll we need to do now is to dump it out, add “MZ” and “PE” to the header, unmap and rebase it using PE Bear,\r\nand then we can start analysing the next stage!\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 59 of 61\n\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 60 of 61\n\nMD5 of Dumped DLL: 52b4480de6f4d4f32fba2b535941c284\r\nCongratulations! You have managed to analyse the loader and “unpack” the next stage, which I will be analysing\r\nin the next post (because this one has now amassed over 6,000 words which is much longer than I planned). So,\r\nfeel free to ask any questions you have down below, or over Twitter (@0verfl0w_) and I will be glad to answer\r\nthem! I apologize again for the lack of posts recently, I’ve been working on my course as well, so I’ve had a lot of\r\nstuff to do! Hopefully the next post on ISFB shouldn’t take too long to do, so make sure to sign up to my mailing\r\nlist to stay updated whenever I post! Thanks again 🙂\r\nSource: https://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nhttps://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/\r\nPage 61 of 61\n\nAPI call that In order to do matches is ZwGetContextThread. so, we can use this site, which We can is extremely now use this useful. It to get the variant hashes the input of CRC-32 using several used. different\nvariants of CRC-8, CRC-16, and CRC-32, allowing us to compare the value from the debugger to the output of\nthe different variants. With the API name, ZwGetContextThread, and the required output, 0x5A3D66E4, we can\nfind the variant used: CRC-32/JAMCRC.      \n   Page 39 of 61",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia",
		"ETDA"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://0ffset.net/reverse-engineering/malware-analysis/analysing-isfb-loader/"
	],
	"report_names": [
		"analysing-isfb-loader"
	],
	"threat_actors": [
		{
			"id": "d90307b6-14a9-4d0b-9156-89e453d6eb13",
			"created_at": "2022-10-25T16:07:23.773944Z",
			"updated_at": "2026-04-10T02:00:04.746188Z",
			"deleted_at": null,
			"main_name": "Lead",
			"aliases": [
				"Casper",
				"TG-3279"
			],
			"source_name": "ETDA:Lead",
			"tools": [
				"Agentemis",
				"BleDoor",
				"Cobalt Strike",
				"CobaltStrike",
				"RbDoor",
				"RibDoor",
				"Winnti",
				"cobeacon"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434290,
	"ts_updated_at": 1775826744,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e0690224caa1d44810faf4a9f11e148ec1c65e61.pdf",
		"text": "https://archive.orkl.eu/e0690224caa1d44810faf4a9f11e148ec1c65e61.txt",
		"img": "https://archive.orkl.eu/e0690224caa1d44810faf4a9f11e148ec1c65e61.jpg"
	}
}