{
	"id": "c1c412ad-302a-4cae-8dbc-a9d502c94f03",
	"created_at": "2026-04-06T15:53:32.762328Z",
	"updated_at": "2026-04-10T13:12:08.34539Z",
	"deleted_at": null,
	"sha1_hash": "40983a020e4f12b5737fc915e1d7d4b18d81c802",
	"title": "Win32/Upatre.BI - Part One - Unpacking",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 129821,
	"plain_text": "Win32/Upatre.BI - Part One - Unpacking\r\nArchived: 2026-04-06 15:27:24 UTC\r\nThis is the first part of the four-part series on \"Win32/Upatre.BI\". Check out the other parts here:\r\nPart 2: Config (June 14, 2015)\r\nPart 3: Main Loop (June 16, 2015)\r\nPart 4: Payload Format (June 20, 2015)\r\nWin32/Upatre.BI is a recent member of the Upatre downloader family. The malware is usually spread by email\r\nattachments. It can steal user information and download a variety of other malicious software such as Zeus,\r\nRovnix, Dyzap or Cutwail. In this multi-part blog post I analyze the inner workings of Upatre.BI by reversing the\r\nfollowing sample:\r\nFilename\r\nOGdbzU.exe\r\nFilesize\r\n48128 bytes\r\nMD5\r\n99807df2c2b2cdbff9e373611b07cf48\r\nSHA-256\r\n08acd0c50dbb6d712c54d25a5ccd99c1d6faf9ad2031e56884a0af991e3fda78\r\nMalwr\r\nhttps://malwr.com/analysis/YThlYTU5MmE4OTAyNDEzNTllYjdkZThmODQ5ZTRkYjA/\r\nThe sample is well detected by all popular antivirus scanners:\r\nKaspersky\r\nTrojan-Downloader.Win32.Upatre.aacs\r\nMcAfee\r\nRDN/Upatre-FABV!a\r\nMicrosoft\r\nWin32/Upatre.BI\r\nSophos\r\nTroj/Upatre-LD\r\nSymantec\r\nDownloader.Upatre!gen9\r\nTrendMicro\r\nTROJ_UPATRE.SMC1\r\nThis first part of the series focuses on the initial step in reversing the malware: unpacking. The Upatre sample is\r\nnot technically packed — in fact the unpacked payload is about a quarter the size — but it is protected by two\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 1 of 17\n\ndecryption stages. The first stage uses a simple XOR and ROL encryption, while the second stage uses a variant of\r\nRC4. The following illustration gives an overview of the two unpacking stages.\r\n1. The first unpacking stub modifies part of the memory with ROL and XOR.\r\n2. At the end of the first stage, the tail jump enters the unpacked memory.\r\n3. The second stage copies the unpacking stub code region to a newly allocated page.\r\n4. A call is made to the copied region.\r\n5. Most of the loaded executable, including the PE header, is zeroed out.\r\n6. The encrypted payload is loaded from the executable file and decrypted.\r\n7. The decrypted plain text contains the new executable. It is copied to the image base.\r\n8. The unpacking stub handles relocation and resolving the imports.\r\n9. The tail jump into the decrypted executable concludes the second unpacking stage.\r\nThe remainder of the blog posts provides details about the individual unpacking steps.\r\nStage 1\r\nThe first unpacking stub is very simple. It first checks the size of a Windows DLL before decrypting the second\r\nstage and jumping to it.\r\nCatSrv.dll\r\nUpatre.BI checks the catsrv.dll in the Windows system directory. The system directory is determined with the\r\nfollowing call:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 2 of 17\n\n0040103F call ds:GetSystemDirectoryA\r\n00401045 call append_catsrv_dll\r\nThe routine append_catsrv_dll then appends the name \\catsrv.dll . The DLL name is hard-coded with some\r\nslight obfuscation:\r\n004011E1 mov edx, 'lld'\r\n004011E6 push edx\r\n004011E7 mov edx, '.vru'\r\n004011EC sub edx, 2 ; -\u003e .vrs\r\n004011EF push edx\r\n004011F0 mov edx, 'tac[' ; -\u003e tac\\\r\n004011F5 inc edx\r\n004011F6 push edx\r\nThe same obfuscation method — shifting letters — is also used in the unpacked payload. This might indicate that\r\nthe paylaod and the protector were written by the same authors.\r\nImage Size of CatSrv.dll\r\nThe sample reads the contents of c:\\windows\\system32\\catsrv.dll with fopen . The address of the API\r\nfunction is manually determined by reading the import address table, whose location is hard-coded:\r\n00401006 mov ebx, 65758h\r\n...\r\n00401021 add ebx, 39E8A8h ; -\u003e 0x404000 (.idata)\r\nThe offsets 0x404000 points to the import directory if the .idata section is placed there:\r\nIf the import directory is loaded somewhere else, then a memory reference error will occur and the process\r\ncrashes. The sample retrieves the import at offset 0x7C:\r\n0040105E sub eax, eax\r\n00401060 add eax, 7Ch\r\n00401065 add eax, ebx ; -\u003e eax = fopen\r\n...\r\n0040107C call dword ptr [eax]\r\nwhich is fopen :\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 3 of 17\n\n.idata:0040407C extrn fopen:dword\r\nIt then uses this MS VC++ routine to read the SizeOfCode value of the catsrv.dll by reading field 0x1C into the\r\nPE header:\r\n0040109E call ds:fread\r\n004010A4 mov eax, esi\r\n004010A6 add eax, 3Dh\r\n004010AB dec eax ; -\u003e offset 3C = real PE header\r\n004010AC mov eax, [eax]\r\n004010AE add eax, esi\r\n004010B0 push 1Eh\r\n004010B2 pop edi\r\n004010B3 dec edi\r\n004010B4 dec edi ; -\u003e edi = 1C\r\n004010B5 mov eax, [eax+edi] ; -\u003e size of code\r\nThe SizeOfCode needs to be 768 or larger than 131581, otherwise the unpacking step is skipped and the malware\r\nwill crash.\r\nUnpacking\r\nUpatre arrives at this unpacking loop if the DLL size is in order:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 4 of 17\n\nThe two XOR instructions at the initialization calculates 0x3E00, which is the size of the packed region. The start\r\nof the packed region is pointed to by esi, which was set to 0x405200 at the entry point:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 5 of 17\n\n00401000 push 83h\r\n...\r\n0040101B add esi, 40517Dh\r\nThe unpacking routine decompiles to the following simple routine:\r\nfor i from 0x405200 to 0x408FFF\r\n Byte(i) = ROL(Byte(i), 1) ^ 0x12\r\nendfor\r\nWhere ROL denotes the rotate on left instruction.\r\nTail Jump\r\nThe first unpacking stage concludes by jumping to 0x405604 inside the decrypted area:\r\n0040119C mov edi, 40520Ah\r\n004011A1 xor eax, eax\r\n004011A3 add esp, 3Ch\r\n004011A6 jmp return\r\n...\r\n00401375 return:\r\n00401375\r\n00401375 add edi, 3FAh\r\n0040137B push edi\r\n0040137C retn -\u003e 0x405604\r\nStage 2\r\nThe second unpacking stage is slightly more complicated than the first. Instead of using simple XOR/ROL\r\nobfuscation, it uses an RC4 variant as protection. It also features manual resolving of API addresses.\r\nFind Kernel32\r\nThe first step of the second stage is to laod the base address of the kernel32 library:\r\n0040560C call get_kernel32\r\nThe routine performs the following steps:\r\nmov ebx, large fs:30h -\u003e TIB -\u003e PEB\r\nmov ebx, [ebx+0Ch] -\u003e PEB -\u003e PEB_LDR_DATA\r\nmov ebx, [ebx+0Ch] -\u003e PEB_LDR_DATA -\u003e InLoadOrder\r\nmov ebx, [ebx] -\u003e second element (ntdll)\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 6 of 17\n\nmov ebx, [ebx] -\u003e third element (kernel32)\r\nmov eax, [ebx+18h] -\u003e LDR_MODULE -\u003e DllBase\r\nThe first three lines get the InLoadOrder ListEntry, located at offset 12 into the PEB_LDR_DATA, which is\r\nlocated at offset 12 into the PEB at offset 0x30 into the TIB. The code then follows the forward link of the doubly\r\nlinked list InLoadOrder twice to get to the third element. The third module that is loaded after the process and\r\nntdll is kernel32. The code finally retrieves the DllBase at offset 0x18 into the LDR_MODULE for kernel32.\r\nModify the Entry Point\r\nWhile the malware relies on the kernel32 being the third loaded module, it uses a more robust method to locate the\r\ncurrent process. The malware first determines it’s image base with the following instructions:\r\n00405614 call $+5\r\n00405619 pop eax\r\n0040561A xor ax, ax\r\nThese lines round the current EIP down to the image base. As in get_kernel32 , the code looks up the\r\nInLoadOrder list. It traverses this list until it finds the module that matches the image base, i.e., the current\r\nprocess:\r\n0040562C push large dword ptr fs:30h\r\n00405633 pop eax\r\n00405634 loc_405634:\r\n00405634 mov eax, [eax+0Ch]\r\n00405637 mov eax, [eax+0Ch]\r\n0040563A loc_40563A:\r\n0040563A cmp [eax+18h], ebx\r\n0040563D jz short loc_405643\r\n0040563F mov eax, [eax]\r\n00405641 jmp short loc_40563A\r\nThe malware then sets the image base of the current process to zero (will be fixed later), and the entry point at\r\noffset 0x1C to the original entrypoint 0x127A. Both values will later be overwritten.\r\n00405627 mov esi, 127Ah\r\n00405643 mov [eax+18h], edi\r\n00405646 mov [eax+1Ch], esi\r\nResolve API addresses\r\nUpatre.BI imports all system APIs manually by calling a routine getProcByHash . It has the following prototype:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 7 of 17\n\ngetProcByHash(dll, hash)\r\nThe first parameter dll is the address of the kernel32 DLL resolved earlier. The second argument hash is 32\r\nbit value tied to a specific procedure in kernel32. The disassembly of the routine looks as follows.\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 8 of 17\n\nThis constitutes a very common way of finding imports by malware, as illustrated by the following image:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 9 of 17\n\n1. The offset to the PE header is read from the DOS header.\r\n2. The offset to the export directory is read from the PE header.\r\n3. The offset to the export names table is read from the export table.\r\n4. Every name in the names table is hashed and the result compared to the desired hash.\r\n5. When a name produces the correct hash (in the example the routine GetProcAddress, then the\r\ncorresponding row is looked up in the export ordinals table, e.g., 0x244.\r\n6. In the export address table the row that corresponds to the ordinal is read. This is the RVA to the desired\r\nprocedure.\r\n7. To get the absolute address, the base address of the DLL is added (in our example 0x75200000).\r\nThe hashing routine is simple, yet not trivial to reverse:\r\nfor letter in string\r\n hash = 0\r\n hash = ROL(hash, 7)\r\n hash = hash ^ letter\r\nendfor\r\nIt is used to conceal the names of the following kernel32 functions:\r\nGetProcAddress\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 10 of 17\n\nVirtualProtect\r\nLoadLibraryExA\r\nGetModuleHandleA\r\nVirtualAlloc\r\nGetModuleFileNameA\r\nCreateFileA\r\nSetFilePointer\r\nReadFile\r\nVirtualFree\r\nCloseHandle\r\nGetModuleHandleA\r\nCopy Unpacking Stub\r\nThe second unpacking stage overwrites large parts of the loaded code. It therefore first saves the remaining code\r\nby copying it to newly allocated pages. The size is set to the image size of the loaded exe (which is more than\r\nenough):\r\n00405709 mov [ebp+image_base], eax\r\n0040570C mov eax, [eax+pe.e_lfanew]\r\n0040570F add eax, [ebp+image_base]\r\n00405712 mov [ebp+pe_header_], eax\r\n00405715 mov eax, [eax+pe.SizeOfImage]\r\n00405718 mov [ebp+size_of_image], eax\r\n0040571B push 40h\r\n0040571D push 1000h\r\n00405722 push [ebp+size_of_image]\r\n00405725 push 0\r\n00405727 call [ebp+kernel32_VirtualAlloc]\r\n0040572A mov [ebp+unpacking_stub], eax\r\nThen 0x694 bytes starting from 0x4057F (0x40573A + 0x15) are copied to the newly allocated space:\r\n00405732 mov [ebp+hThisProcess], eax\r\n00405735 call $+5\r\n0040573A pop eax\r\n0040573B add eax, 15h\r\n0040573E push 694h\r\n00405743 push [ebp+unpacking_stub]\r\n00405746 push eax ; 0040574f\r\n00405747 call copy_\r\nFinally, the copied code is run:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 11 of 17\n\n0040574C call [ebp+unpacking_stub]\r\nThe copied code follows right after the previous call. In the following I report the offsets at their original location.\r\nUnpacking\r\nBefore the payload is unpacked, Upatre cleans any trace of the current image by zeroing out the memory from the\r\nimage base.\r\n00405828 mov edi, [ebp+image_base]\r\n0040582B mov ecx, [ebp+SizeOfImage]\r\n0040582E shr ecx, 2\r\n00405831 xor eax, eax\r\n00405833 rep stosd\r\nThe original payload lies encrypted at a fixed location into the executable file. The encryption method is VMPC\r\nRC4 with a 64 bit IV and 64 bit key, generated by a modified LCG. Starting at 0x5576 bytes into the exe, Upatre\r\nfirst reads 64 bytes as the initialization vector. Following that, it reads the 0x1A44 bytes long ciphertext:\r\nGetModuleFileNameA(-1, \u0026path_to_exe, ...)\r\nh = CreateFileA(\u0026path_to_exe, ...)\r\nSetFilePointer(h, 0x5576, 0, 0)\r\nReadFile(h, \u0026iv, 0x40, ...)\r\nReadFile(h, \u0026ciphertext, 0x1A44, ...)\r\nUpatre then creates the decryption key:\r\n00405838 push [ebp+key]\r\n0040583B call generate_key\r\nThe key generation routine decompiles to the following pseudo-code:\r\nprocedure generate_key(key[64], seed)\r\nfor i from 0 to 63\r\n ix = 16807 * ((seed ^ 123456789) % 12773) - 2836 * ((seed ^ 123456789) / 12773)\r\n if ix \u003c 0 then\r\n ix += 0x7FFFFFFF\r\n end if\r\n key[i] = ix\r\n seed = ix ^ 123456789\r\nend for\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 12 of 17\n\nThis resembles the LCG-rand implementation in C, except:\r\n1. the additional encryption of the seed with XOR 123456789\r\n2. 12773 is used instead of 127773.\r\nThe second modification makes no sense, because the number can’t be freely chosen but rather depends on the\r\nmodulus and factor of the LCG. Chosing a different values breaks Schrage’s trick.\r\nThe next step of RC4 is the initialization of the permutation S:\r\nprocecedure vmpc_initialization(S[256])\r\nfor i from 0 to 255\r\n S[i] = i\r\nend for\r\nThe permutation is then scrambled with the following algorithm:\r\nprocedure vmpc_scrambling(S[256], vec, veclen, j)\r\nfor m from 0 to 3*256 - 1\r\n i = m % 256\r\n j = S[j + vec[m % veclen] + S[i]]\r\n swap( S[i], S[j] )\r\nend for\r\nreturn j\r\nThe scrambling routine is first called with the key as the second parameter, then again with the IV. After these\r\nKSA steps, the decryption of the ciphertext is carried out using the PRGA variant of VMPC:\r\nprocedure vmpc_prga(S[256], ciphertext, ciphertext_len, j)\r\nfor a from 0 to ciphertext_len -1\r\n i = m % 256\r\n j = S[(S[i] + j) % 256]\r\n ciphertext[m] = c[m] ^ S[(S[S[j]] + 1) % 256]\r\n swap( S[i], S[j] )\r\nreturn j\r\nAs seen earlier, the key for the decryption is calculated by a modified LCG based on a seed. This seed stored\r\nnowhere in the binary, but rather brute-forced by the unpacking stub: at first, the decryption is carried out for the\r\nfirst four bytes of the ciphertext for every seed starting with 0 until the plaintext reads “AMPC” (for my sample\r\nwith the seed 0x45). The key is then used to decrypt the remainder of the ciphertext. The following pseudo-code\r\nsummarizes the decryption of the payload.\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 13 of 17\n\nseed = 0\r\nIV = exe_file[0x5576 : 0x557A]\r\nc = exe_file[0x557A : 0x6FBE]\r\nwhile True\r\n generate_key(key, seed)\r\n vmpc_initialization(S)\r\n j = vmpc_scrambling(S, key, 64, 0)\r\n j = vmpc_scrambling(S, iv, 64, j)\r\n magic = c[0:4]\r\n j = vmpc_prga(S, magic, 4, j)\r\n if magic = \"AMPC\"\r\n break\r\n else\r\n seed = seed + 1\r\n end if\r\nend while\r\nvmpc_prga(S, c[4 : 0x6FBE], 0x1A44, j)\r\nCopy\r\nThe decrypted payload is written to the current image base. First the PE headers protection is changed to be\r\nwritable:\r\n004058CD mov eax, [ebp+plaintext]\r\n004058D0 mov eax, [eax+pe.e_lfanew]\r\n004058D3 add eax, [ebp+plaintext]\r\n004058D6 mov [ebp+pe_header], eax\r\n004058D9 mov eax, [eax+pe.nr_of_sections]\r\n004058DC mov word ptr [ebp+nr_of_sections], ax\r\n004058E0 mov ebx, [ebp+pe_header]\r\n004058E3 mov ebx, [ebx+pe.SizeOfHeaders]\r\n004058E6 mov eax, ebp\r\n004058E8 sub eax, 48h\r\n004058EB push eax ; prev header protectiong\r\n004058EC push PAGE_EXECUTE_READWRITE\r\n004058EE push ebx\r\n004058EF push [ebp+image_base]\r\n004058F2 call [ebp+kernel32_VirtualProtect]\r\nthen the header is overwritten by the header of the payload:\r\n004058F5 push ebx ; size of headers\r\n004058F6 push [ebp+image_base] ; dst\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 14 of 17\n\n004058F9 push [ebp+plaintext] ; src\r\n004058FC call copy\r\nUpatre then iterates over all sections of the payload — .text and .idata — and also copies them to the current\r\nimage.\r\nRelocation and Imports\r\nAfter the header and the sections are copied, the unpacker performs relocation if necessary:\r\n00405959 mov ebx, [ebp+image_base]\r\n0040595C call relocate\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 15 of 17\n\nThe payload is position independent and therefore has no relocation information, so the relocation call does\r\nnothing even if the image base should not be 0x400'000. In this case, however, the first stage of unpacking would\r\nhave crashed already.\r\nFinally, the imports are manually resolved the same way the loader would resolve imports:\r\n1. The offset to the PE header is read from the DOS header.\r\n2. The offset to the import directory is read from the PE header.\r\n3. For each element in the import directory, the name at offset 0x0C is retrieved and the corresponding DLL is\r\nloaded with LoadLibraryExA .\r\n4. The array of hint names is visited by following OriginalFirstThunk .\r\n5. For each thunk in the HintNames array, the pointer is followed to the IMAGE_IMPORT_BY_NAME structure.\r\n6. At offset 2, the name is read and the corresponding procedure is loaded by calling GetProcAddress()\r\n(using the DLL address from step 3).\r\n7. The resolved address is stored in the IAT, which is pointed to by the FirstThunk value at offset 0x10 into\r\nthe import directory.\r\nThe second stage finally jumps to the unpacked payload. The unpacking stubs tries to veil the jump by setting the\r\noriginal entry point as the return address for an API call VirtualFree:\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 16 of 17\n\n00405A57 mov eax, [esp+14h+image_base_]\r\n00405A5E add eax, 127Ah\r\n00405A63 mov [esp], eax\r\n00405A67 pop eax\r\n00405A68 jmp [esp+10h+VirtualFree]\r\nThe unpacked exe can then be dumped. No import reconstruction or other fixes are necessary. Interestingly, the\r\nunpacked executable has a lower detection rate than the packed version. Sophos, Symantec and TrendMicro for\r\nexample classify the payload as clean (as of June 9th):\r\nKaspersky\r\nTrojan-Downloader.Win32.Upatre.aasc\r\nMcAfee\r\nUpatre-FABV!7347D2130AB5\r\nMicrosoft\r\nWin32/Upatre.BI\r\nSophos\r\nclean\r\nSymantec\r\nclean\r\nTrendMicro\r\nclean\r\nPart 2 of this blog post series will document the data structures used by Upatre and their content.\r\nSource: https://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nhttps://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/\r\nPage 17 of 17",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia",
		"ETDA"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://johannesbader.ch/2015/06/Win32-Upatre-BI-Part-1-Unpacking/"
	],
	"report_names": [
		"Win32-Upatre-BI-Part-1-Unpacking"
	],
	"threat_actors": [],
	"ts_created_at": 1775490812,
	"ts_updated_at": 1775826728,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/40983a020e4f12b5737fc915e1d7d4b18d81c802.pdf",
		"text": "https://archive.orkl.eu/40983a020e4f12b5737fc915e1d7d4b18d81c802.txt",
		"img": "https://archive.orkl.eu/40983a020e4f12b5737fc915e1d7d4b18d81c802.jpg"
	}
}