{
	"id": "ec4a1699-0b67-4643-ad49-b73356c5f518",
	"created_at": "2026-04-06T00:10:52.862513Z",
	"updated_at": "2026-04-10T13:12:41.251347Z",
	"deleted_at": null,
	"sha1_hash": "2ca001117b5b7d8c711c9483c09f316d12b0e2a7",
	"title": "Deep Analysis of GCleaner",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 644156,
	"plain_text": "Deep Analysis of GCleaner\r\nBy Abdallah Elshinbary\r\nPublished: 2023-07-15 · Archived: 2026-04-05 20:27:11 UTC\r\nHowdy! I’m finally back with another malware deep dive report. This time we are digging into GCleaner.\r\nGCleaner is a Pay-Per-Install (PPI) loader first discovered in early 2019, it has been used to deploy other\r\nmalicious families like Smokeloader, Amadey, Redline and Raccoon.\r\nWe will be working on this sample:\r\n(SHA256: 020d370b51711b0814901d7cc32d8251affcc3506b9b4c15db659f3dbb6a2e6b )\r\nInitial TriagePermalink\r\nLet’s start by running the sample in Triage sandbox to get an overview of what it does.\r\nWe can see from the process tree that it drops and runs another binary out of \"%APPDATA%\" folder with a\r\nseemingly random name then it kills itself using \"taskkill\" and deletes the sample binary from disk.\r\nThe network tab shows communications to different IP addresses which are considered as C2 servers in Triage’s\r\nmalware config tab. Each C2 has a different URL path, we will dig deeper to find out what each of them is\r\nresponsible for.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 1 of 17\n\nRight when we open the sample in IDA we don’t have much to look at, there are some interesting strings and API\r\nimports but not very helpful to start with.\r\nWe can see a repeated pattern across the code where some values are pushed into the stack then xored with 0x2E ,\r\nso we first need to decrypt these values.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 2 of 17\n\nString DecryptionPermalink\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 3 of 17\n\nAutomating the decryption for stack strings in this sample can be a bit tricky, luckily I noticed a specific\r\ninstruction that occurs after loading the encrypted strings into stack ( cmp eax, [reg+4] ).\r\nSo we can find all occurrences of this instruction then walk back to find the mov instructions and get the\r\nencrypted values. Let’s apply this to an IDA python script.\r\n# Lowest address used in the program\r\naddr = idc.get_inf_attr(INF_MIN_EA)\r\nwhile True:\r\n # Search for \"cmp eax, [reg+4]\"\r\n addr = ida_search.find_binary(addr, idc.BADADDR, \"3B ?? 04 00 00 00\", 16, ida_search.SEARCH_NEXT | ida_searc\r\n if addr == idc.BADADDR:\r\n break\r\n enc_bytes = b''\r\n # Search for possible stack strings in the previous 12 instructions\r\n for i in range(12):\r\n ea = idc.prev_head(ea)\r\n if (idc.print_insn_mnem(ea) == \"mov\" and\r\n idc.get_operand_type(ea, 0) == idc.o_displ and\r\n idc.get_operand_type(ea, 1) == idc.o_imm):\r\n # Get the value of the second operand\r\n operand_value = idc.get_operand_value(ea, 1)\r\nThe returned operand value is an integer but we need to store it as a byte array, so we first need to figure out the\r\nsize of that operand to store it correctly.\r\n # Get the size of the second operand\r\n insn = ida_ua.insn_t()\r\n ida_ua.decode_insn(insn, ea)\r\n operand_size = ida_ua.get_dtype_size(insn.Op2.dtype)\r\n \r\n # Specify the correct data type\r\n if operand_size == 4:\r\n operand_bytes = struct.pack(\"\u003cI\", operand_value)\r\n elif operand_size == 2:\r\n operand_bytes = struct.pack(\"\u003cH\", operand_value)\r\n else:\r\n operand_bytes = struct.pack(\"\u003cB\", operand_value)\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 4 of 17\n\nenc_bytes = operand_bytes + enc_bytes\r\nOne more thing I noticed is that some strings use a combination of stack values and other values stored in the\r\n\".rdata\" section (retrieved using the XMM instruction \"movaps\" ).\r\nSo we can search for this \"movaps\" instruction after the \"cmp\" instruction, if found we can read the values\r\nstored at its operand address and append it to the encrypted bytes.\r\n # Find possible xmmword movaps\r\n xmmword_addr = ida_search.find_binary(addr, addr+50, pattern2, 16, ida_search.SEARCH_NEXT | ida_search.SEARC\r\n if xmmword_addr != idc.BADADDR:\r\n # Read the xmmword value\r\n xmmword_value = idc.get_bytes(get_operand_value(xmmword_addr, 1), 16)\r\n enc_bytes = xmmword_value + enc_bytes\r\nFinally we can xor the encrypted values with 0x2E (this key has been the same for all GCleaner samples I looked\r\nat).\r\n # Decrypt and strip encrypted bytes\r\n dec_bytes = bytes(c ^ 0x2E for c in enc_bytes)\r\n dec_str = dec_bytes.strip(b'\\x00').decode('utf-8')\r\n \r\n if len(dec_str) != 0:\r\n print(f\"{hex(addr)} --\u003e {dec_str}\")\r\n # Set a comment with the decrypted string\r\n if dec_str and comment_addr != idc.BADADDR:\r\n set_comment(comment_addr, dec_str)\r\nHere is the list of decrypted strings:\r\nExpand to see more\r\n  45.12.253.56\r\n  45.12.253.72\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 5 of 17\n\n45.12.253.98\r\n  45.12.253.75/dll.php\r\n  mixinte\r\n  mixtwo\r\nWe can now see the C2 IPs, URL paths and some other interesting strings. Let’s keep going.\r\nAnti Checks (or is it..?)Permalink\r\nGCleaner is filled with host checks but weirdly enough it doesn’t do anything them, maybe they were like test\r\nfeatures? copy-paste code? not really sure but let’s quickly go though them.\r\nChecking usernamePermalink\r\nGet the current username using \"GetUserNameA()\" and compare it to hardcoded names ( \"admin\" , \"Shah\" ,\r\n\"testBench\" ).\r\nChecking foreground windowPermalink\r\nGet the title of the foreground window using \"GetWindowTextA()\" and compare it to hardcoded strings.\r\nChecking desktop filesPermalink\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 6 of 17\n\nSearch for Desktop files with specific strings in their name ( \"CCleaner\" , \"VLC media player\" , \"Acrobat\r\nReader DC\" ).\r\nChecking locale and keyboard layoutPermalink\r\nCheck if the computer locale is Russian and compare the keyboard layout against specific values (CIS countries).\r\nDropped BinaryPermalink\r\nLooking back at the process tree we need to figure out where does that child binary with random name comes\r\nfrom. \"%APPDATA%\\{846ee340-7039-11de-9d20-806e6f6e6963}\\34LMAylZs6FixF.exe\"\r\nWe can see below that the sample reads the \"%APPDATA%\" path using \"getenv()\" then creates a random\r\ndirectory using the GUID of the current hardware profile, if retrieving the hardware profile failed it will fall back\r\nto generating a random folder name. Other possible locations for creating the random directory are \"C:\\Program\r\nFiles\" , \"C:\\Temp\" , \"C:\\ProgramData\" (fallback locations).\r\nNext it generates a random file name, appends \".exe\" extension to it then drops it to the newly created directory\r\nand runs it from there.\r\nThe binary file is hardcoded into the parent sample.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 7 of 17\n\nAll that binary child does is…well…sleep for 10 seconds, that’s it :|\r\nC2 CommunicationsPermalink\r\nThe actors behind GCleaner have been known to use BraZZZers fast flux service to hide their infrastructure, it\r\nworks more like a proxy system between the victims and the real C2 server.\r\nBefore reaching out to the C2 servers, GCleaner adds hardcoded HTTP headers (could be used for a network sig)\r\nan a custom user-agent to each C2 request.\r\nNow to figure out what each C2 request is responsible for.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 8 of 17\n\nFirst C2Permalink\r\nIP: 45[.]12.253.56\r\nUA: OK\r\nPCAP:\r\nThis C2 is likely responsible for bot registration. The sample will only continue execution if the server response is\r\n\"0\" or \"1\" , otherwise it goes to sleep and tries again.\r\nThe \"str\" and \"substr\" parameters in the C2 request above are possibly referring to the campaign ID,\r\nGCleaner has been known to use similar values in the past like \"usone\" , \"ustwo\" , \"euthree\" , \"cafive\" ,\r\n\"mixshop\" , …\r\nSecond C2Permalink\r\nIP: 45[.]12.253.72\r\nUA: OK\r\nPCAP:\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 9 of 17\n\nThe first request to this C2 is responsible for getting an AES key.\r\nThe key length must be between 10 and 100 bytes, otherwise it breaks the execution.\r\nThe second request is responsible for getting an AES encrypted PE file (notice the filename in the response\r\nheaders!), That PE file is decrypted using the key from the previous request.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 10 of 17\n\nThe decryption routine is pretty trivial, the sample first calculates the SHA256 hash of the server key then derives\r\nthe session key used for decryption (AES_128).\r\nAfter that it loads the decrypted PE file into memory (without touching disk) to get the address of an export\r\nfunction called \"GetLicInfo\" which is used in the next stage.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 11 of 17\n\nDownloaded DLLPermalink\r\nBefore going further we first need to take a look at the downloaded PE file. To be able to analyze it we can either\r\nuse the debugger to dump the decrypted file or get the encrypted response from the PCAP and decrypt it manually.\r\nWe can easily implement the decryption code in Python as follow:\r\nimport hashlib\r\nfrom Crypto.Cipher import AES\r\nenc = open(\"puk.php.bin\", \"rb\").read()\r\nkey = \"kvQoRqtcCyMtHmQyQXOUu\".encode(\"utf-16le\") # Important to encode!!\r\nsha256_hash = hashlib.sha256(key)\r\naes_key = sha256_hash.digest()[:16]\r\ncipher = AES.new(aes_key, mode=AES.MODE_CBC, IV=b\"\\x00\"*16)\r\ndec = cipher.decrypt(enc)\r\nopen(\"out.bin\", \"wb\").write(dec)\r\nNow let’s see what this export function \"GetLicInfo\" does.\r\nBasically it sends an http request to the supplied C2 server then checks the response length, if the length is greater\r\nthan 2048 bytes it creates a a new directory with a random name under \"%APPDATA%\" or \"%TEMP%\" folder then\r\ngenerates a random filename and appends \".exe\" extension to it.\r\nFinally it writes the server response to a disk file with the generated random filename and executes that file.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 12 of 17\n\nThird C2Permalink\r\nIP: 45[.]12.253.75\r\nUA: B\r\nPCAP:\r\nThis C2 is responsible for downloading further payloads, notice the user-agent used here is the one from the\r\ndecrypted strings list unlike the previous 2 C2s.\r\nThe address is supplied to the external function \"GetLicInfo\" which downloads and executes the payload as we\r\nstated above. GCleaner tries to get a payload from the server for 10 iterations with a sleep period of 2 seconds\r\nbetween every try.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 13 of 17\n\nIf no further payload is received from the server the samples kills its process and deletes the parent file from disk.\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 14 of 17\n\nForth C2Permalink\r\nIP: 45[.]12.253.98\r\nThis C2 wasn’t used in the sample we are looking at.\r\nWe can use the IDA python script we used for string decryption to build a standalone config extractor as most of\r\nthe interesting stuff are in the decrypted strings list.\r\nHere’s the output of the code after extracting the useful information:\r\nThe code can be found here.\r\n(this script is not optimized for production, it’s just for research purposes)\r\nHuntingPermalink\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 15 of 17\n\nUrlscanPermalink\r\nThe URL path of the first C2 request can be a good candidate to hunt for more C2s on urlscan.\r\nI looked at more samples and found these two URL patterns:\r\ns=NOSUB\u0026str=...\u0026substr=...\r\nsub=NOSUB\u0026stream=...\u0026substream=...\r\nSo we can use the \"page.url\" field to search for the first part of these patterns.\r\nYaraPermalink\r\nWe saw that many strings were encrypted but we can use some of the hardcoded ones to create a simple yara rule\r\nfor hunting more samples.\r\nrule GCleaner {\r\n meta:\r\n description = \"Detects GCleaner payload\"\r\n author = \"Abdallah Elshinbary (@_n1ghtw0lf)\"\r\n hash1 = \"020d370b51711b0814901d7cc32d8251affcc3506b9b4c15db659f3dbb6a2e6b\"\r\n hash2 = \"73ed1926e850a9a076a8078932e76e1ac5f109581996dd007f00681ae4024baa\"\r\n strings:\r\n // Kill self\r\n $s1 = \"\\\" \u0026 exit\" ascii fullword\r\n $s2 = \"\\\" /f \u0026 erase \" ascii fullword\r\n $s3 = \"/c taskkill /im \\\"\" ascii fullword\r\n // Anti checks\r\n $s4 = \" Far \" ascii fullword\r\n $s5 = \"roxifier\" ascii fullword\r\n $s6 = \"HTTP Analyzer\" ascii fullword\r\n $s7 = \"Wireshark\" ascii fullword\r\n $s8 = \"NetworkMiner\" ascii fullword\r\n // HTTP headers\r\n $s9 = \"Accept-Language: ru-RU,ru;q=0.9,en;q=0.8\" ascii fullword\r\n $s10 = \"Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1\" ascii fullword\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 16 of 17\n\n$s11 = \"Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0\" ascii fullword\r\n $s12 = \"Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gi\r\n \r\n condition:\r\n uint16(0) == 0x5a4d and\r\n 10 of them\r\n}\r\nReferencesPermalink\r\nhttps://medium.com/csis-techblog/gcleaner-garbage-provider-since-2019-2708e7c87a8a\r\nhttps://medium.com/csis-techblog/inside-view-of-brazzzersff-infrastructure-89b9188fd145\r\nSource: https://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nhttps://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/\r\nPage 17 of 17",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://n1ght-w0lf.github.io/malware%20analysis/gcleaner-loader/"
	],
	"report_names": [
		"gcleaner-loader"
	],
	"threat_actors": [
		{
			"id": "08c8f238-1df5-4e75-b4d8-276ebead502d",
			"created_at": "2023-01-06T13:46:39.344081Z",
			"updated_at": "2026-04-10T02:00:03.294222Z",
			"deleted_at": null,
			"main_name": "Copy-Paste",
			"aliases": [],
			"source_name": "MISPGALAXY:Copy-Paste",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434252,
	"ts_updated_at": 1775826761,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/2ca001117b5b7d8c711c9483c09f316d12b0e2a7.pdf",
		"text": "https://archive.orkl.eu/2ca001117b5b7d8c711c9483c09f316d12b0e2a7.txt",
		"img": "https://archive.orkl.eu/2ca001117b5b7d8c711c9483c09f316d12b0e2a7.jpg"
	}
}