{
	"id": "24eb8c73-4696-43be-bf83-586e0acda60f",
	"created_at": "2026-04-06T00:18:14.809222Z",
	"updated_at": "2026-04-10T03:21:13.72923Z",
	"deleted_at": null,
	"sha1_hash": "bbe2189646401528639e5ca7b243de9ef5b09a15",
	"title": "Semplificare l’analisi di Emotet con Python e iced x86",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3904601,
	"plain_text": "Semplificare l’analisi di Emotet con Python e iced x86\r\nArchived: 2026-04-05 16:55:40 UTC\r\nIn questi giorni l’Italia è interessata da campagne Emotet che fanno uso di payload offuscati che rendono l’analisi\r\nstatica piuttosto tediosa. In questo caso infatti l’uso di un debugger non si rileva molto utile.\r\nLa variante di Emotet analizzata impiega varie tecniche di offuscazione del codice, una di queste è nota come\r\nCFO (Control Flow Obfuscation); l’analisi con il debugger permette di ricostruire stringhe ed API utilizzate ma\r\nnon aiuta ad avere una visione d’insieme del codice eseguito.\r\nIn questo articolo vedremo come semplificare l’analisi di Emotet utilizzando Python e la libreria iced x86 per\r\nprocessare il codice offuscato di Emotet.\r\nMa prima, introduciamo un po’ di contesto sul campione analizzato.\r\nMacro, powershell e packer\r\nIl campione che abbiamo deciso di analizzare risale a qualche giorno fa.\r\nLa catena di infezione ha inizio con un documento Word (pre 2007) con una macro malevola.\r\nQuesta si presenta, come spesso accade, offuscata tramite l’utilizzo di linee di codice irrilevante.\r\nSi riporta di seguito un estratto.\r\nFunction S619csvpd1v4xzk5kc(Xoyqcbzwjyi6tqiw0z)\r\n GoTo GKsgQaAGE\r\nDim NmmcJMB As String 'POyDeJ\r\nOpen \"dVMtDJ.ecCLuZ.vNWxUB\" For Binary As 154\r\nOpen \"GmQlB.gLlkBCq.ohnmP\" For Binary As 154\r\nOpen \"asHdBA.RNUGfJo.UEIiMmoM\" For Binary As 154\r\nPut #154, , NmmcJMB\r\nClose #154\r\nGKsgQaAGE:\r\nGoTo fIjVkJj\r\nDim jFUMUmIIJ As String 'NskblDD\r\nOpen \"fRHrGnFp.uWltAIHCI.WYWvIWr\" For Binary As 146\r\nOpen \"qQeaRICAm.KgqZFRWRC.cuPrnUFxk\" For Binary As 146\r\nOpen \"ShUECDIR.otrtDOGBA.OugaBFHlJ\" For Binary As 146\r\nPut #146, , jFUMUmIIJ\r\nClose #146\r\nfIjVkJj:\r\nGoTo hTTQEJEAC\r\nDim OybSq As String 'kEafA\r\nOpen \"umMOXxmA.SfYuGDN.ueONFAEFD\" For Binary As 227\r\nOpen \"eIQhLAGS.forvJhMB.LGyFI\" For Binary As 227\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 1 of 47\n\nOpen \"TifoEDtFB.fukVJAvIS.dlciFGDA\" For Binary As 227\r\nPut #227, , OybSq\r\nClose #227\r\nhTTQEJEAC:\r\nHBYVV = \"\"\r\nS619csvpd1v4xzk5kc = HBYVV + VBA.Replace _\r\n(Xoyqcbzwjyi6tqiw0z, \"qq\" + \")(s2)\" + \"(\", W5ya1q1z48ltq3z_)\r\n GoTo mJsZBCEFo\r\nDim jUDsXM As String 'gtpnJOwLd\r\nOpen \"myDIGCFHC.cgXWyuEFC.OybuGU\" For Binary As 131\r\nOpen \"EnJMG.KCVSIHB.BJiWBGLWG\" For Binary As 131\r\nOpen \"kfSFYoEHi.aXUIAvAP.dswKhikA\" For Binary As 131\r\nPut #131, , jUDsXM\r\nClose #131\r\nmJsZBCEFo:\r\nGoTo BOzmWI\r\nDim CJeaFB As String 'jtrvFEWLD\r\nOpen \"dfOYHJLF.uBXVkGE.ghpJGB\" For Binary As 124\r\nOpen \"MTfEVUDIQ.DlrvrPEB.PgggwwMD\" For Binary As 124\r\nOpen \"YHUtVQCI.AyvDaAH.JsZULCUu\" For Binary As 124\r\nPut #124, , CJeaFB\r\nClose #124\r\nBOzmWI:\r\nGoTo kPMjtUB\r\nDim eVbTfoFi As String 'xTUBS\r\nOpen \"eXoWdB.HSupDA.oXRxAS\" For Binary As 149\r\nOpen \"nmuAl.yeRQHDs.UqyoFI\" For Binary As 149\r\nOpen \"nzFmWEVE.ZFvEGsIFD.mjIMGVD\" For Binary As 149\r\nPut #149, , eVbTfoFi\r\nClose #149\r\nkPMjtUB:\r\nEnd Function\r\nFunction Tujor4m47ob()\r\nOn Error Resume Next\r\nsh2v = T6dwlv_ivpoiq2.StoryRanges.Item(1)\r\n GoTo aektCnFI\r\nDim jaJUkAFeG As String 'cwxgFSS\r\nOpen \"DbnKMvMAH.jHcdBADv.EGxUCAADs\" For Binary As 201\r\nOpen \"gQEGCB.HVmcrDI.zGpVIUABC\" For Binary As 201\r\nOpen \"shyujG.RFwdH.VPRoIX\" For Binary As 201\r\nPut #201, , jaJUkAFeG\r\nClose #201\r\naektCnFI:\r\nGoTo RtfzGtt\r\nDim WWCACxG As String 'mRJNaEGtF\r\nOpen \"vATeCIgJI.FpiaIJIiJ.MmplJ\" For Binary As 153\r\nOpen \"MOIhAmCn.UAJXCE.BwsiJS\" For Binary As 153\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 2 of 47\n\nOpen \"NpVFCB.MCDxG.UpDmKPxpp\" For Binary As 153\r\nPut #153, , WWCACxG\r\nClose #153\r\nRtfzGtt:\r\nGoTo QSISC\r\nEstratto del codice della macro di Emotet.\r\nCodice superfluo\r\nSoffermandoci a leggere il codice è possibile osservare la ripetizione di un pattern in particolare: tutte le istruzioni\r\nper la scrittura di file (Open, Put, Close) sono inutili.\r\nStesso discorso vale per i GoTo che puntano sempre verso l’etichetta successiva, risultando quindi inutili.\r\nAnche le dichiarazioni di variabile (Dim) sono inutili.\r\nPossiamo quindi semplificare il codice mediante una semplice sostituzione che rimuova le righe con le istruzioni\r\nOpen, Put, Close, GoTo e le etichette.\r\nLa seguente regex svolge proprio questo compito:\r\n^\\s(Open |Put |Close |GoTo |Dim |[A-Za-z]+:).$\r\n(gli spazi nella regex sono rilevanti)\r\nIl risultato, una volta rimosse le linee superflue, è più compatto ed attaccabile.\r\nFunction S619csvpd1v4xzk5kc(Xoyqcbzwjyi6tqiw0z)\r\nHBYVV = \"\"\r\nS619csvpd1v4xzk5kc = HBYVV + VBA.Replace _\r\n(Xoyqcbzwjyi6tqiw0z, \"qq\" + \")(s2)\" + \"(\", W5ya1q1z48ltq3z_)\r\nEnd Function\r\nFunction Tujor4m47ob()\r\nOn Error Resume Next\r\nsh2v = T6dwlv_ivpoiq2.StoryRanges.Item(1)\r\nsng2 = \"qq)(\" + \"s2)(pq\" + _\r\n \"q)(s2)(\"\r\nF7_if4svnte = \"qq)(s\" + _\r\n \"2)(roqq\" + \")(s2)(qq)(s2)(ceqq)(s2)\" + _\r\n \"(sqq)(s2)(sqq)(s2)(qq)(s2)(\"\r\nVbzhqcqh1pqco1e2_ = \"qq)(s2)(\" + \":wqq)(s2)(qq)(s\" + _\r\n \"2)(inqq)(s2)(3qq)(s\" + _\r\n \"2)(2qq)(s2)(_qq)(s2)(\"\r\nR67uawfvzvw = \"wqq)(s2\" + _\r\n \")(inqq)(s2)(mqq)(s\" + \"2)(gmqq)(s2)(tqq)(\" + \"s2)(qq)(s2)(\"\r\nKz1yuitvz3qu6xai = Kfo_8qx2w7l7x71 + ChrW(Hvsf68urunanusc + wdKeyS + A08llnuiz59xyw7) + Pgjdd1yrw8qt\r\nNi1wsg2ja20x23qpzl = R67uawfvzvw + Kz1yuitvz3qu6xai + Vbzhqcqh1pqco1e2_ + sng2 + F7_if4svnte\r\nKltqgnwd4i8 = C0d4mc619_eaiuirzl(Ni1wsg2ja20x23qpzl)\r\nSet Bx9ystsny9ej4ynfne = CreateObject(Kltqgnwd4i8)\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 3 of 47\n\nWb0zemdl5ow9 = Mid(sh2v, (5), Len(sh2v))\r\nBx9ystsny9ej4ynfne.Create C0d4mc619_eaiuirzl(Wb0zemdl5ow9), Gge416y0ol9ajq, Z2vzndsnblr9xje7s\r\nEnd Function\r\nFunction C0d4mc619_eaiuirzl(Hcmfukntlsj04fj5x3)\r\nOn Error Resume Next\r\nH4k01s90g3qjf9v7e = (Hcmfukntlsj04fj5x3)\r\nIxl3ey6k7oiq4qmw8 = S619csvpd1v4xzk5kc(H4k01s90g3qjf9v7e)\r\nC0d4mc619_eaiuirzl = Ixl3ey6k7oiq4qmw8\r\nEnd Function\r\nIl codice della macro di Emotet, una volta rimosse le parti inutili.\r\nA questo punto è immediato osservare che il codice della macro si basa su:\r\neliminazione della stringa qq)(s2)(\r\npayload contenuto nel documento ( T6dwlv_ivpoiq2.StoryRanges.Item(1) )\r\ncomando di esecuzione di quest’ultimo (comando generato concatenando gli string literal contenuti nella\r\nmacro).\r\nIl payload è uno script powershell (codificato in base64), inserendo un carattere di fine linea, dopo ogni punto e\r\nvirgola, otteniamo un codice già abbastanza leggibile.\r\nIl codice del payload Powersell, di facile interpretazione.\r\nA questo punto risultano facilmente individuabili:\r\nle modalità di download ( Net.WebClient.DownloadFile );\r\ni C2 (è interessante notare come i C2 fossero tutti siti WordPress compromessi);\r\nla modalità di esecuzione (tramite rundll32).\r\nIl file scaricato, come ci si aspetta, è una DLL che esportata una routine chiamata RunDLL.\r\nUna ricognizione con CFF Explorer rileva la presenza di una risorsa con dati binari e l’analisi con IDA mostra la\r\npresenza delle stringhe LdrAccessResource e LdrFindResource_U e di una chiamata a VirtualAlloc.\r\nRisulta quindi facile dedurre che abbiamo a che fare con un packer. Infatti, tenendo traccia del buffer allocato con\r\nVirtualAlloc si ottiene rapidamente il payload. Una nuova DLL.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 4 of 47\n\nI componenti del packer. In alto a sinistra il codice che rileva l’accesso alla risorsa. In alto a destra il\r\ncodice che alloca il buffer per il payload e lo decodifica. In basso la risorsa contente il payload\r\ncodificato.\r\nLa DLL ottenuta contiene il codice del malware Emotet e come vedremo presenta varie tecniche di offuscazione.\r\nL’offuscazione di Emotet\r\nNavigando la DLL di Emotet con IDA si notano alcune tecniche di offuscazione, consideriamo ad esempio, la\r\nprocedura riportata nella figura sotto.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 5 of 47\n\nLa procedura principale di Emotet. Da questa visione d’insieme è evidente la presenza di CFO.\r\nL’immagine sopra riportata racchiude quasi tutti i blocchi di codice della procedura principale di Emotet, dato\r\nl’elevato numero di blocchi non è possibile leggere il codice di ognuno, ma due di essi sono stati appositamente\r\ningranditi per metterli in evidenza.\r\nDalle istruzioni presenti in questi due blocchi, le quali confrontano il valore del registro ECX con una costante, si\r\nintuisce che siamo in presenza di CFO.\r\nIl valore del registro ECX – nota che il registro usato varia da procedura a procedura – contiene un numero\r\n(chiamiamolo token per comodità) che indica quale blocco di codice eseguire.\r\nSi tratta di un enorme switch implementato con tanti salti condizionali, anzichè con un jump table (che renderebbe\r\nl’analisi più semplice).\r\nDopo l’esecuzione di ogni blocco di codice, un nuovo token viene messo in ECX per proseguire al prossimo\r\nblocco di codice utile.\r\nUno dei nostri obiettivi è quello di trasformare la funzione sopra nella sua forma originale, mostrata qui sotto.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 6 of 47\n\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 7 of 47\n\nLa procedura principale di Emotet una volta rimossa l’offuscazione CFO.\r\nOltre a CFO, questo campione di Emotet non importa nessuna API e la sezione dei dati è codificata.\r\nInfine, le costanti sono tutte create nello stack con una sequenza di istruzioni variabile ma più o meno fa uso delle\r\nstesse operazioni.\r\nEsempio di creazione della costante 513 nella variabile var_198 (poi mai usata).\r\nVedremo come rimuovere, in parte, queste forme offuscazione.\r\nIl lavoro sporco: PE, decodifica delle istruzioni e basic block\r\nPer poter manipolare il codice di Emotet è necessario effettuare un processo di reificazione del codice x86.\r\nIl primo obiettivo consiste nel poter manipolare le istruzioni x86. La decodifica e la codifica delle istruzioni x86\r\nnon è particolarmente complessa in sè, ovviamente è richiesta esperienza con il linguaggio assembly x86 e con la\r\nconsultazione dei manuali Intel (di interesse è solo il volume 2), tutti prerequisiti base per affrontare l’analisi.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 8 of 47\n\nIl formato generale delle istruzioni x86.\r\nLa vera complessità nello scrivere codice per la decodifica (e codifica) delle istruzioni x86 sta nel fatto che il\r\nformato è variabile, i campi ModRM ed Immediate dipendono dal valore dell’Opcode (questo a sua volta ha\r\nlunghezza variabile) e di Opcode ve ne sono migliaia.\r\nIl formato delle istruzioni x86 è vecchio di 41 anni e la sua continua espansione da parte di Intel (e talvolta AMD)\r\nha portato ad un zoo di istruzioni codificate tramite una serie di hack, al punto che la decodifica delle istruzioni è\r\nuno dei principali colli di bottiglia delle CPU x86 (il front-end richiede addirittura un passo di pre-decodifica per\r\npoter essere super-scalare).\r\nLa decodifica può essere effettuata facilmente con delle tabelle di lookup, a patto di voler trascrivere le mappe\r\nfornite in appendice al volume 2 da Intel. Parliamo di centinaia di istruzioni, ognuna con più varianti (es: mov\r\nr32, rm32 e mov rm32, imm32 sono documentate sotto la voce MOV), per un totale che supera il migliaio.\r\nAbbiamo quindi deciso di usare una libreria già pronta. Questa deve permettere non solo la decodifica ma anche la\r\ncodifica e l’alterazione delle istuzioni.\r\nLa scelta è ricaduta su iced x86, una libreria (per lo più generata automaticamente) Rust, .NET e Python\r\n(linguaggio scelto dal Cert-AgID perchè gia predisposto).\r\nIced x86 ha molte funzionalità utili, come la possibilità di determinare il modo in cui una funzione altera il flusso\r\ndi esecuzione e vari metodi helper per la creazione e l’introspezione delle istruzioni.\r\nCon iced x86 le istruzioni x86 diventano oggetti python che possiamo mettere in una lista e modificare a piacere.\r\nLa classe Encoder permette di ricodificare le istruzioni a qualsiasi indirizzo, permettendoci di lavorare con le\r\nistruzioni senza dover pensare a come sono codificate (la codifica di alcune istruzioni, tipo un salto near relativo,\r\ndipende dall’indirizzo dell’istruzione e dal target del salto).\r\nPrima di poter decodificare le istruzioni, è necessario trovarle. Per fare ciò ci serve processare il file PE della DLL\r\ndi emotet e poi decodificare le istruzioni utilizzando la tecnica di descending.\r\nEsistono varie librerie Python per il parsing dei file PE (quella più famosa è pefile) ma più o meno tutte soffrono\r\ndel tipico problema delle librerie Python: assenza di documentazione o documentazione limitata ad esempi che\r\ndescrivono come muovere i primi passi.\r\nDato che la struttura PE è molto semplice, abbiamo preferito scrivere una funzione per il parsing dei campi PE di\r\ninteresse.\r\nIn particolare ci serve conoscere: le varie sezioni, i vari allineamenti (su file ed in memoria), i nomi esportati, la\r\nsezione di codice e quella di dati (ricordate appositamente data la loro importanta), il base address dell’immagine\r\ned infine l’ultimo RVA e offset usato (per aggiungere nuove sezioni) ed il minimo offset delle sezioni (per vedere\r\nse c’è posto per aggiungere nuove entry nella tabella delle sezioni).\r\ndef read_dw(bytes, off=0):\r\n return bytes[off] | (bytes[off+1] \u003c\u003c 8) | (bytes[off+2] \u003c\u003c 16) | (bytes[off+3] \u003c\u003c 24)\r\ndef read_w(bytes, off=0):\r\n return bytes[off] | (bytes[off+1] \u003c\u003c 8)\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 9 of 47\n\ndef write_dw(bytes, off=0, val=0):\r\n bytes[off] = val \u0026 0xff\r\n bytes[off+1] = (val \u003e\u003e 8) \u0026 0xff\r\n bytes[off+2] = (val \u003e\u003e 16) \u0026 0xff\r\n bytes[off+3] = (val \u003e\u003e 24) \u0026 0xff\r\ndef write_w(bytes, off=0, val=0):\r\n bytes[off] = val \u0026 0xff\r\n bytes[off+1] = (val \u003e\u003e 8) \u0026 0xff\r\ndef load_pe(filename, export = \"RunDLL\"):\r\n with open(filename, \"rb\") as f:\r\n dll = f.read()\r\n if dll[0:2] != b'MZ':\r\n raise ValueError(f\"{filename} doesn't have a valid MZ header.\")\r\n pe_off = read_dw(dll, 0x3c)\r\n if dll[pe_off:pe_off+4] != b'PE\\0\\0':\r\n raise ValueError(f\"{filename} doesn't have a valid PE header.\")\r\n \r\n n_sec = read_w(dll, pe_off+6)\r\n size_opt = read_w(dll, pe_off + 0x14)\r\n sec_table = pe_off + size_opt + 0x18\r\n first_off = len(dll)\r\n last_off = 0\r\n last_rva = 0\r\n code_rva = data_rva = None\r\n secs = []\r\n for x in range(n_sec):\r\n cur_sec = sec_table + 0x28*x\r\n cur_rva = read_dw(dll, cur_sec + 0xc)\r\n cur_off = read_dw(dll, cur_sec + 0x14)\r\n cur_size = read_dw(dll, cur_sec + 0x10)\r\n cur_vsize = read_dw(dll, cur_sec + 0x8)\r\n if dll[cur_sec : cur_sec+8] == b'.text\\0\\0\\0':\r\n code_rva = cur_rva\r\n code_off = cur_off\r\n code_size = cur_size\r\n code_vsize = cur_vsize\r\n elif dll[cur_sec : cur_sec+8] == b'.data\\0\\0\\0':\r\n data_rva = cur_rva\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 10 of 47\n\ndata_off = cur_off\r\n data_size = cur_size\r\n data_vsize = cur_vsize\r\n secs.append({\r\n \"name\" : dll[cur_sec:cur_sec+8],\r\n \"rva\": cur_rva,\r\n \"off\": cur_off,\r\n \"size\": cur_size,\r\n \"vsize\": cur_vsize,\r\n })\r\n if cur_off + cur_size \u003e last_off:\r\n last_off = cur_off + cur_size\r\n \r\n if first_off \u003e cur_off:\r\n first_off = cur_off\r\n if cur_rva + cur_vsize \u003e last_rva:\r\n last_rva = cur_rva + cur_vsize\r\n image_base = read_dw(dll, pe_off + 0x34)\r\n entry_point = read_dw(dll, pe_off + 0x28)\r\n file_align = read_dw(dll, pe_off + 0x3c)\r\n sec_align = read_dw(dll, pe_off + 0x38)\r\n if code_rva is None:\r\n raise ValueError(f\"Cannot find the .text section in {filename}.\")\r\n if data_rva is None:\r\n raise ValueError(f\"Cannot find the .data section in {filename}.\")\r\n def rva_to_off(rva):\r\n for s in secs:\r\n if rva \u003e= s[\"rva\"] and rva \u003c s[\"rva\"] + s[\"vsize\"]:\r\n return rva - s[\"rva\"] + s[\"off\"]\r\n return None\r\n def read_cstr(data, off=0):\r\n res = \"\"\r\n while data[off] != 0:\r\n res += chr(data[off])\r\n off += 1\r\n return res\r\n n_dirs = read_dw(dll, pe_off + 0x74)\r\n exp_rva = exp_size = exp_off = None\r\n if n_dirs \u003e 0:\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 11 of 47\n\nexp_rva = read_dw(dll, pe_off + 0x78)\r\n exp_size = read_dw(dll, pe_off + 0x7c)\r\n exp_off = rva_to_off(exp_rva)\r\n exports = []\r\n if exp_off is not None:\r\n n_names = read_dw(dll, exp_off + 0x18)\r\n names = rva_to_off(read_dw(dll, exp_off + 0x20))\r\n ordinals = rva_to_off(read_dw(dll, exp_off + 0x24))\r\n addresses = rva_to_off(read_dw(dll, exp_off + 0x1c))\r\n for i in range(n_names):\r\n name = read_cstr(dll, rva_to_off(read_dw(dll, names + i * 4)))\r\n ordinal = read_w(dll, ordinals + i * 2)\r\n rva = read_dw(dll, addresses + ordinal * 4)\r\n exports.append({\r\n \"name\" : name,\r\n \"rva\" : rva,\r\n \"off\" : rva_to_off(rva)\r\n })\r\n return {\r\n \"dll\": dll[:],\r\n \"pe_off\": pe_off,\r\n \"n_sec\": n_sec,\r\n \"size_opt\": size_opt,\r\n \"sec_table\": sec_table,\r\n \"first_off\": first_off,\r\n \"last_rva\": last_rva,\r\n \"last_off\": last_off,\r\n \"secs\": secs,\r\n \"code\": {\r\n \"rva\": code_rva,\r\n \"off\": code_off,\r\n \"size\": code_size,\r\n \"vsize\": code_vsize,\r\n \"data\": dll[code_off:code_off+code_size],\r\n },\r\n \"data\": {\r\n \"rva\": data_rva,\r\n \"off\": data_off,\r\n \"size\": data_size,\r\n \"vsize\": data_vsize,\r\n \"data\": dll[data_off:data_off+data_size],\r\n },\r\n \"exp\": {\r\n \"rva\": exp_rva,\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 12 of 47\n\n\"size\": exp_size,\r\n \"off\": exp_off,\r\n },\r\n \"image_base\" : image_base,\r\n \"entry_point\" : entry_point,\r\n \"file_align\" : file_align,\r\n \"sec_align\" : sec_align,\r\n \"exports\": exports,\r\n }\r\nL’orribile codice utilizzato per il parsing PE. Notare che si tratta di codice prototipo, non usare in produzione a\r\nmeno di non voler essere licenziati!\r\nDopo il parsing PE abbiamo a disposizione tutte le informazioni per la decodifica delle istruzioni: il base address\r\ndell’immagine può essere sommato all’RVA della routine RunDLL (anch’esso recuperato nel parsing) per ottenere\r\nl’indirizzo in memoria del codice, l’RVA stesso può essere usato insieme alle informazioni sulla sezione di codice\r\nper ottenere l’offset all’interno di questa dell’inizio della funzione.\r\n#Load PE\r\ndll = load_pe(\"emotet.dll\")\r\n#Decode\r\nstart = dll[\"exports\"][0][\"rva\"] + dll[\"image_base\"]\r\nstart_f = descend_func(start, dll[\"code\"][\"data\"], start - dll[\"image_base\"] - dll[\"code\"][\"rva\"])\r\nParsing del PE di emotet e chiamata alla funzione di descending del codice del primo nome esportato.\r\nChi programma in assembly sa bene che il codice è dati ed i dati sono codice, non vi è differenza sintattica tra i\r\ndue.\r\nLa decodifca delle istruzioni non può quindi procedere da un indirizzo in avanti perchè potremmo incorrere nella\r\nseguenti situazioni.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 13 of 47\n\nDue situazioni problematiche per la decodifica sequenziale. Le frecce indicano i salti del codice.\r\nDobbiamo quindi adottare l’approccio dei dissassemblatori descending (tipo IDA), i quali decodificano una serie\r\ndi istruzioni fino alla prima istruzione di salto (escluse chiamate ma inclusi ritorni al chiamante, ovvero\r\nl’istruzione ret ).\r\nQuesta serie di istruzioni, che per loro natura contengono al massimo un salto e solo come ultima istruzione, si\r\nchiama basic block (BB in breve).\r\nUn BB necessità di: una lista di istruzioni, un’indirizzo (un VA) di inizio che lo identifica univocamente (e che\r\naiuta nel debug se posto uguale a quello reale determinato dal PE) e una lista di successori.\r\nIl nostro codice prototipo non gestisce salti indiretti e quindi si avranno, 0, 1 o 2 successori per ogni BB.\r\nOltre a queste caratteristiche, torna utile salvare tutte le istruzioni chiamata individuate. Queste serviranno per\r\nscoprire altro codice da decodificare.\r\nInfine, per la ricodifica sono utili altri due campi: uno con il codice macchina generato dalla lista di istruzioni ed\r\nuno con l’offset (all’interno della nuova sezione di codice) in cui è rigenerato il codice del BB.\r\nclass BasicBlock(object):\r\n CACHE = {}\r\n START_ADDRESS_FOR_DUPLICATES = -1\r\n def __init__(self, start_address, insts = None):\r\n self.start_address = start_address\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 14 of 47\n\nself.insts = insts or []\r\n self.nexts = []\r\n self.update_len()\r\n self.calls = []\r\n self.splitted = False\r\n self.encoded = None\r\n self.offset = None\r\n self._cfo_last_registers = None\r\n BasicBlock.CACHE[self.start_address] = self\r\n def duplicate(self):\r\n dup = BasicBlock(BasicBlock.START_ADDRESS_FOR_DUPLICATES, self.insts.copy())\r\n dup.nexts = self.nexts.copy()\r\n dup.calls = self.calls.copy()\r\n dup.splitted = self.splitted\r\n dup.encoded = dup.offset = None\r\n BasicBlock.START_ADDRESS_FOR_DUPLICATES -= 1\r\n return dup\r\n def update_len(self):\r\n tot = 0\r\n addr = self.start_address\r\n encoder = Encoder(32)\r\n for i in self.insts:\r\n if i.len == 0:\r\n ilen = encoder.encode(i, addr)\r\n i.len = ilen\r\n tot += i.len\r\n addr += i.len\r\n self.len = tot\r\n #The address \"item\" is in this BB (but not at the very beginning or end)?\r\n def __contains__(self, item):\r\n return item \u003e self.start_address and item \u003c (self.start_address + self.len)\r\n def add_inst(self, i):\r\n self.insts.append(i)\r\n self.len += i.len\r\n def split_at(self, address):\r\n old_len = address - self.start_address\r\n if old_len == 0:\r\n raise ValueError(\"Invalid split! Cannot split at the beginning of a BB.\")\r\n if old_len == self.len:\r\n raise ValueError(\"Invalid split! Cannot split at the end of a BB.\")\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 15 of 47\n\n#print(f\"Splitting {hex(self.start_address)} at {hex(address)} (len: {hex(self.len)})\")\r\n #print(\"Nexts before split: \" + str([hex(x.start_address) for x in self.nexts]))\r\n tot = 0\r\n for j, i in enumerate(self.insts):\r\n if tot == old_len:\r\n break\r\n elif tot \u003e old_len:\r\n #print(f\"{hex(self.start_address)} + {hex(self.start_address + self.len)}\")\r\n raise ValueError(f\"Invalid split address {hex(address)}!\")\r\n tot += i.len\r\n new_bb = BasicBlock(address, self.insts[j:])\r\n self.len = old_len\r\n self.insts = self.insts[:j]\r\n new_bb.nexts = self.nexts\r\n new_bb.splitted = self.splitted\r\n self.splitted = True\r\n self.nexts = [new_bb]\r\n #print(f\"After: (len: {hex(self.len)})\")\r\n #print(\"Nexts after split: \" + str([hex(x.start_address) for x in self.nexts]))\r\n return new_bb\r\n #Add a next to this BB, but if it's splitted, traverse its successors until the first\r\n # non split block (this is necessary because during the descending of blocks with two\r\n # successor, one branch may split the current BB and the other would attach to the end of the\r\n # now splitted block insead to the end of the block left after the splitting address.)\r\n def add_next(self, item):\r\n #print(f\"Adding next {hex(item.start_address)} to {hex(self.start_address)}\")\r\n last_bb = self\r\n while last_bb.splitted:\r\n #print(\"Skipping to next BB, this is splitted\")\r\n last_bb = last_bb.nexts[0]\r\n if item not in last_bb.nexts:\r\n last_bb.nexts.append(item)\r\n def iterate(self, cb, start):\r\n bbs = [self]\r\n done = []\r\n while bbs:\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 16 of 47\n\nbb = bbs.pop(0)\r\n start += cb(bb)\r\n done.append(bb)\r\n bbs += [x for x in bb.nexts if x not in bbs and x != bb and x not in done]\r\n return start\r\n def __str__(self):\r\n \r\n def show_bb(bb):\r\n formatter = Formatter(FormatterSyntax.NASM)\r\n s = f\"loc_{hex(bb.start_address)}:\\n\"\r\n for i in bb.insts:\r\n s += f\"\\t{formatter.format(i)}\\n\"\r\n s+= \"\\n\".join(map(lambda x: f\"\\t\\t-\u003eloc_{hex(x.start_address)}\", bb.nexts)) + \"\\n\"\r\n return s\r\n return self.iterate(show_bb, \"\")\r\n def _show(self):\r\n for n, i in enumerate(self.insts):\r\n print(f\"({n}) {hex(i.ip)} {i:ns}\")\r\nLa classe che rappresenta un BB. Anche qui il codice è stato scritto senza considerare i principi OOP o di stile. Il\r\ncodice è molto grezzo ma intuitivo.\r\nRimane aperta una questione spinosa, evidenziata dall’attributo splitted e dal metodo split_at della classe\r\nsopra.\r\nA volte un BB va diviso in due parti perchè un altro BB contiene un salto che atterra proprio nel mezzo del nostro\r\nBB.\r\nQuesto succede quando i compilatori riusano del codice già generato.\r\nSotto è evidenziato un esempio di un BB che è stato diviso (da IDA) a seguito di questa problematica. Si noti\r\ncome i primi due BB non terminino con un’istruzione di salto.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 17 of 47\n\nEsempio di due BB, splitted, che non terminano con un’istruzione di salto. Essendo la destinazione\r\ndi salti in altri BB, devono essere separati dal BB che li conteneva (quello in alto in questo caso).\r\nLa gestione dello “splitting” dei BB è necessaria non solo per creare un corretto grafo di esecuzione (come fa\r\nIDA) ma anche perchè durante il descending di un BB con due successori è possibile che uno di questi finisca per\r\ndividere proprio il blocco in analisi. Quando l’altro ramo di esecuzione è processato, per determinare l’altro\r\nsuccessore, senza adeguata gestione, questo viene attaccato al BB sbagliato.\r\nQuando viene chiesto di attaccare un successore ad un BB che risulta diviso, è necessario attaccarlo invece\r\nall’unico successore di questi (che a sua volta può essere diviso).\r\nFatte queste considerazioni, il descending è poi piuttosto semplice: si decodifica ogni istruzione, inserendola nel\r\nBB corrente. Se si incontra un’istruzione ret (o un errore) terminiamo la procedura, se si incontra un’istruzione di\r\nsalto, richiamiamo ricorsivamente il descending sulla destinazione del salto e attacchiamo il BB così ottenuto\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 18 of 47\n\ncome successore di quello attuale.\r\nNel caso di salti condizionali si hanno due possibili successori:\r\nuno è il fallthrough (ovvero la prossima istruzione, quando il salto non è preso);\r\nl’altro è il target del salto.\r\nPer convenzione, che va rispettata durante la ricodifica, il fallthrough è il primo successore della lista di successori\r\ndi un BB.\r\ndef descend_bb(start, code, offset):\r\n #Already processed?\r\n if start in BasicBlock.CACHE:\r\n return BasicBlock.CACHE[start]\r\n #In the middle of another BB (but not at its beginning)?\r\n for _, bb in BasicBlock.CACHE.items():\r\n if start in bb:\r\n return bb.split_at(start)\r\n #Descend the BB\r\n decoder = Decoder(32, code[offset:])\r\n decoder.ip = start\r\n cur_bb = BasicBlock(start)\r\n #print(f\"BB START: {hex(start)} (offset {hex(offset)})\")\r\n #For each instruction\r\n for i in decoder:\r\n #Add it to the BB\r\n cur_bb.add_inst(i)\r\n #These instructions end the BB with no next BBs\r\n if i.flow_control in [FlowControl.RETURN, FlowControl.EXCEPTION, FlowControl.INDIRECT_BRANCH\r\n #print(f\"BB END: {hex(i.ip + i.len-1)}\")\r\n return cur_bb\r\n #These instructions are remembered for convenience but they don't influence the building of t\r\n elif i.flow_control in [FlowControl.CALL]:\r\n #print(f\"FOUND CALL at {i.ip} to {i.near_branch_target}\")\r\n cur_bb.calls.append(i)\r\n #These instructions are ignored\r\n elif i.flow_control in [FlowControl.NEXT, FlowControl.XBEGIN_XABORT_XEND, FlowControl.INTERRU\r\n pass\r\n #These instructions end the BB with a single next BB\r\n elif i.flow_control in [FlowControl.UNCONDITIONAL_BRANCH]:\r\n target = i.near_branch_target\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 19 of 47\n\ntarget_off = target-start+offset\r\n #print(f\"BB END (UB): {hex(i.ip + i.len-1)}\")\r\n \r\n #Descend from the target\r\n cur_bb.add_next(descend_bb(target, code, target_off))\r\n return cur_bb\r\n #These instructions end the BB with two next BBs\r\n elif i.flow_control in [FlowControl.CONDITIONAL_BRANCH]:\r\n target = i.near_branch_target\r\n target_off = target-start+offset\r\n target_next = i.ip + i.len\r\n target_next_off = target_next-start+offset\r\n #print(f\"BB END (CB): {hex(i.ip + i.len-1)} - {len(cur_bb.insts)}\")\r\n #First the fallthrough BB\r\n cur_bb.add_next(descend_bb(target_next, code, target_next_off))\r\n #Then the branching BB\r\n cur_bb.add_next(descend_bb(target, code, target_off))\r\n return cur_bb\r\n else:\r\n raise ValueError(\"Unhandled flow!\")\r\nLa funzione di descending di un BB. A partire da un VA e relativo offset nella sezione di codice, crea il grado di\r\nesecuzione e ritorna il BB di inizio del suddetto grado.\r\nSi può notare, nel codice, l’utilizzo di un dizionario di cache. Questo dizionario mappa ogni indirizzo con\r\nl’eventuale BB a quell’indirizzo. Questa cache è necessaria per evitare di processare lo stesso indirizzo più volte\r\n(evitando cicli infiniti).\r\nE’ inoltre utile per scorrere tutti i BB processati (un’operazione che servirà in seguito).\r\nCon il descending abbiamo quasi finito. La procedura descend_bb (che forse era meglio includere nella class\r\nBasicBlock ) se utilizzata all’indirizzo di inizio di una procedura genera il grafo di esecuzione di questa e la lista\r\ndi chiamate effettuate.\r\nE’ conveniente quindi creare una classe Function che tiene traccia del BB di inizio del grafo (detto head) e delle\r\nfunzioni chiamate.\r\nclass Function(object):\r\n CACHE = {}\r\n def __init__(self, address, head = None):\r\n self.address = address\r\n self.head = head\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 20 of 47\n\nself.callees = []\r\n Function.CACHE[address] = self\r\n def next_calls(self):\r\n def collect_calls(bb):\r\n calls = []\r\n for c in bb.calls:\r\n target = c.near_branch_target\r\n if target not in calls: #target not in Function.CACHE and\r\n calls.append(target)\r\n return calls\r\n return list(set(self.head.iterate(collect_calls, [])))\r\n def linearize_bbs(self):\r\n return list(set(self.head.iterate(lambda x: [x], [])))\r\n def __str__(self):\r\n return f\"sub_{hex(self.address)}:\\n\" + str(self.head)\r\n \r\ndef descend_func(start, code, offset):\r\n if start in Function.CACHE:\r\n return Function.CACHE[start]\r\n f = Function(start, descend_bb(start, code, offset))\r\n #print(f\"Next calls: {len(f.next_calls())}.\")\r\n for c in f.next_calls():\r\n f.callees.append(descend_func(c, code, c - start + offset))\r\n return f\r\nIl codice per la reificazione delle funzioni si basa sul codice dei BB.\r\nUn’ultima nota: la classe BasicBlock contiene un metodo iterate che elenca tutti i BB raggiungibili da quello su\r\ncui è chiamato, rompendo i cicli infiniti (in pratica ogni BB è presente una ed una sola volta).\r\nA questo punto abbiamo uno strumento per l’analisi dell’esecuzione di codice x86.\r\nPossiamo modificare il codice processato inserendo e modificando istruzioni (una chiamata a update_len è\r\nnecessaria dopo ogni modifica).\r\nPossiamo anche creare nuovi BB o ridefinire i collegamenti tra BB.\r\nNon possiamo però rigenerare il codice a partire da un’oggetto BasicBlock o Function.\r\nLa generazione del codice (che abbiamo ironicamente chiamato ascending) è più o meno speculare all’operazione\r\ndi descending.\r\nL’idea è di avere una classe che gestica lo spazio fin ora usato per la ricodifica dei BB.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 21 of 47\n\nQuesto si può fare un contatore che viene ogni volta incrementato della dimensione, in byte, di un BB.\r\nUsando il valore del contatore come offset (e quindi anche come RVA) a cui mettere il codice dei BB, si garantisce\r\nche questi non si sovrappongono e siano continui.\r\nUn’accorgimento da avere è quello di convertire tutti gli operandi dei salti in modo che siano codificati con 32 bit.\r\nPer rendere il codice più denso il formato x86 supporta operandi a 8 e 16 bit, oltre che quelli a 32 bit.\r\nUtilizzando quest’ultimi si risolvono i problemi di raggiungibilità del target (ad esempio se si trovasse oltre 128\r\nbyte dal salto e si usassero operandi ad 8 bit).\r\nQuando si trova un’istruzione di chiamata, è importante memorizzare a quale offset è stata incontrata, dove inizia\r\ne dove finisce il suo operando.\r\nQuesti dati saranno necessari per le rilocazioni (fixup), effettuate una volta generato il codice di tutte le funzioni.\r\nPer il resto l’algoritmo di ascending è ricordivo: per ogni BB codifichiamo le sue istruzioni (opportunamente\r\nconvertite) e poi codifichiamo ricorsivamente ogni successore e modifichiamo l’ultima istruzione di salto del BB\r\nper puntare all’offset del successore di competenza.\r\nNel caso di salti condizionali, il successore fallthrough protrebbe già essere stato codificato ad un offset che non è\r\nsuccessivo all’istruzione di salto, in tal caso inseriamo un salto incondizionale artificiale.\r\nIl codice di ascending è riportato qui.\r\nclass Ascender(object):\r\n def __init__(self, new_code_va):\r\n self.va = new_code_va\r\n self.next_offset = 0\r\n self.bbs = []\r\n self.fixups = []\r\n #We keep track of the space allocated with an offset.\r\n #Each BB requires a specific amount of continuous space\r\n def _alloc_space(self, size):\r\n offset = self.next_offset\r\n self.next_offset += size\r\n return offset\r\n #Compute the size of BB, each branch is transformed to use a 32-bit relative immediate\r\n #So we don't have to care about out of reach targets.\r\n def _bb_size(self, bb):\r\n size = 0\r\n for i in bb.insts:\r\n if i.flow_control in [FlowControl.CALL, FlowControl.UNCONDITIONAL_BRANCH]:\r\n size += 5\r\n elif i.flow_control in [FlowControl.CONDITIONAL_BRANCH]:\r\n size += 6\r\n else:\r\n size += i.len\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 22 of 47\n\nreturn size\r\n #A BB can request more space if no other call to ascend_bb was made between _alloca_space\r\n #and the call to _more_space.\r\n def _more_space(self, amount):\r\n self.next_offset += amount\r\n #Alloc space for the BB and record it\r\n def alloc_bb(self, bb):\r\n self.bbs.append(bb)\r\n return self._alloc_space(self._bb_size(bb))\r\n #Transform a conditional branch that uses 8 or 16-bit immeditates to one that uses a 32-bit immed\r\n def _cond_br_rel32(self, i):\r\n return {\r\n Code.JO_REL8_32: Code.JO_REL32_32, Code.JO_REL16: Code.JO_REL32_32, Code.JO_REL32_32: Cod\r\n Code.JNO_REL8_32: Code.JNO_REL32_32, Code.JNO_REL16: Code.JNO_REL32_32, Code.JNO_REL32_32\r\n Code.JB_REL8_32: Code.JB_REL32_32, Code.JB_REL16: Code.JB_REL32_32, Code.JB_REL32_32: Cod\r\n Code.JAE_REL8_32: Code.JAE_REL32_32, Code.JAE_REL16: Code.JAE_REL32_32, Code.JAE_REL32_32\r\n Code.JE_REL8_32: Code.JE_REL32_32, Code.JE_REL16: Code.JE_REL32_32, Code.JE_REL32_32: Cod\r\n Code.JNE_REL8_32: Code.JNE_REL32_32, Code.JNE_REL16: Code.JNE_REL32_32, Code.JNE_REL32_32\r\n Code.JBE_REL8_32: Code.JBE_REL32_32, Code.JBE_REL16: Code.JBE_REL32_32, Code.JBE_REL32_32\r\n Code.JA_REL8_32: Code.JA_REL32_32, Code.JA_REL16: Code.JA_REL32_32, Code.JA_REL32_32: Cod\r\n Code.JS_REL8_32: Code.JS_REL32_32, Code.JS_REL16: Code.JS_REL32_32, Code.JS_REL32_32: Cod\r\n Code.JNS_REL8_32: Code.JNS_REL32_32, Code.JNS_REL16: Code.JNS_REL32_32, Code.JNS_REL32_32\r\n Code.JP_REL8_32: Code.JP_REL32_32, Code.JP_REL16: Code.JP_REL32_32, Code.JP_REL32_32: Cod\r\n Code.JNP_REL8_32: Code.JNP_REL32_32, Code.JNP_REL16: Code.JNP_REL32_32, Code.JNP_REL32_32\r\n Code.JL_REL8_32: Code.JL_REL32_32, Code.JL_REL16: Code.JL_REL32_32, Code.JL_REL32_32: Cod\r\n Code.JGE_REL8_32: Code.JGE_REL32_32, Code.JGE_REL16: Code.JGE_REL32_32, Code.JGE_REL32_32\r\n Code.JLE_REL8_32: Code.JLE_REL32_32, Code.JLE_REL16: Code.JLE_REL32_32, Code.JLE_REL32_32\r\n Code.JG_REL8_32: Code.JG_REL32_32, Code.JG_REL16: Code.JG_REL32_32, Code.JG_REL32_32: Cod\r\n }[i.code]\r\n #Encode this BB and every BB reachable from it.\r\n #Save them in the bbs field.\r\n def ascend_bb(self, bb, fixups):\r\n #Already ascended?\r\n if bb.encoded is not None:\r\n return\r\n #Alloc space in the data\r\n offset = self.alloc_bb(bb)\r\n cur_va = self.va + offset\r\n encoder = Encoder(32)\r\n bb.encoded = b''\r\n bb.offset = offset\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 23 of 47\n\nfor i in bb.insts:\r\n #If the instruction is call, add a new entry in the fixups.\r\n #This entry contains:\r\n # the offset of the first byte of the immediate (to patch it)\r\n # The VA target of the call (to find which Function object represent it)\r\n # The offset of the end of the call instruction (to be used to compute the delta)\r\n if i.flow_control in [FlowControl.CALL]:\r\n fixups.append((cur_va + 1 - self.va, i.near_branch_target, cur_va+5-self.va))\r\n i = Instruction.create_branch(Code.CALL_REL32_32, 0x1000000)\r\n \r\n #This is an unconditional branch, just ascend the next block and change the last instr\r\n elif i.flow_control in [FlowControl.UNCONDITIONAL_BRANCH]:\r\n if i != bb.insts[-1] or len(bb.nexts) != 1:\r\n raise ValueError(\"Bug! Unhandled case: UNCB.\")\r\n #Ascend the next block\r\n self.ascend_bb(bb.nexts[0], fixups)\r\n #Recompute the branch to point to the right target\r\n i = Instruction.create_branch(Code.JMP_REL32_32, self.va + bb.nexts[0].offset)\r\n elif i.flow_control in [FlowControl.CONDITIONAL_BRANCH]:\r\n if i != bb.insts[-1] or len(bb.nexts) != 2:\r\n raise ValueError(\"Bug! Unhandled case: CONB.\")\r\n #If the fallthrough is already encoded, we need to put a jump at the end of this BB\r\n #to jump where the fallthough was encoded.\r\n #NB. This is buggy, it seems to always put a jump\r\n artificial_jump = False\r\n if bb.nexts[0].encoded is not None:\r\n #Require more space for this BB\r\n self._more_space(5)\r\n artificial_jump = True\r\n else:\r\n #The fallthrough was not encoded, ascend it right after this BB (as it should be\r\n self.ascend_bb(bb.nexts[0], fixups)\r\n \r\n #Ascend the conditional branch target\r\n self.ascend_bb(bb.nexts[1], fixups)\r\n #Recreate the target instruction\r\n i = Instruction.create_branch(self._cond_br_rel32(i), self.va + bb.nexts[1].offset)\r\n #Make the artificial jump to the fallthrough\r\n if artificial_jump:\r\n #print(\"Making artificial jump.\")\r\n cur_va += encoder.encode(i, cur_va)\r\n i = Instruction.create_branch(Code.JMP_REL32_32, self.va + bb.nexts[0].offset)\r\n \r\n cur_va += encoder.encode(i, cur_va)\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 24 of 47\n\n#Encoded instruction\r\n bb.encoded = encoder.take_buffer()\r\n #In case of splitted BB we ascended no successor, so we handle that case here\r\n if bb.splitted:\r\n self.ascend_bb(bb.nexts[0], fixups)\r\n #Heper method, just like ascend_bb but also ascend the callees\r\n def ascend_func(self, f):\r\n if f.head.encoded is not None:\r\n return\r\n self.ascend_bb(f.head, self.fixups)\r\n for c in f.callees:\r\n self.ascend_func(c)\r\n #Write the BB code into a bytearray\r\n def write(self):\r\n result = bytearray(self.next_offset)\r\n #print(f\"Ascended BBs: {len(self.bbs)}.\")\r\n for bb in self.bbs:\r\n #print(f\"BB at offset {hex(bb.offset)}.\")\r\n result[bb.offset:bb.offset+len(bb.encoded)] = bb.encoded\r\n #Fixup the calls immediate operands\r\n for offset, orig_target, off_rel_to in self.fixups:\r\n if orig_target not in Function.CACHE:\r\n raise ValueError(\"Something is wrong. Found function not ascended.\")\r\n to_off = Function.CACHE[orig_target].head.offset\r\n delta = to_off - off_rel_to\r\n #print(f\"Applying fixup to offset {hex(offset)} (delta is {hex(delta)}).\")\r\n result[offset:offset+4] = struct.pack(\"\r\nLa classe che ricodifica gli oggetti Function e BasicBlock.\r\nPossiamo ora creare una nuova sezione di codice e salvare il PE.\r\nQueste sono operazioni sul formato PE e riportiamo quindi solo il codice.\r\ndef align(val, alignment):\r\n return ((val + alignment - 1) // alignment) * alignment\r\ndef make_sec(dll, name, size, exec=True):\r\n rva = align(dll[\"last_rva\"], dll[\"sec_align\"])\r\n dll[\"last_rva\"] = rva + size\r\n return {\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 25 of 47\n\n\"name\": name,\r\n \"rva\": rva,\r\n \"size\": size,\r\n \"exec\": exec,\r\n }\r\ndef save_pe(filename, dll, extra_secs, import_data=None):\r\n pe_off = dll[\"pe_off\"]\r\n data = bytearray(dll[\"dll\"])\r\n write_w(data, pe_off+0x6, dll[\"n_sec\"] + len(extra_secs))\r\n write_dw(data, pe_off+0x50, read_dw(data, pe_off+0x50) + sum(map(lambda x: x[\"size\"], extra_secs\r\n if import_data:\r\n write_dw(data, pe_off+0x80, import_data[\"rva\"])\r\n write_dw(data, pe_off+0x84, import_data[\"size\"])\r\n delta = dll[\"first_off\"] - dll[\"sec_table\"] - dll[\"n_sec\"]*0x28\r\n amount = 0\r\n if delta \u003c 0x28 * len(extra_secs):\r\n amount = align(0x28 * len(extra_secs) - delta, dll[\"file_align\"])\r\n for x in dll[\"n_sec\"]:\r\n ptr = dll[\"sec_table\"] + x*0x28 + 0x14\r\n write_dw(data, read_dw(data, ptr) + amount)\r\n with open(filename, \"wb\") as f:\r\n f.write(data[0:dll[\"sec_table\"]+ dll[\"n_sec\"]*0x28])\r\n cur_off = align(dll[\"last_off\"], dll[\"file_align\"])\r\n for ns in extra_secs:\r\n f.write(ns[\"name\"].encode('utf-8') + (b'\\0'*8)[len(ns[\"name\"]):])\r\n f.write(struct.pack('\r\nLa classe che ricodifica gli oggetti Function e BasicBlock.\r\nIl codice seguente aggiunge la sezione .text2 al PE e vi scrive il codice ottenuto discendendo la routine RunDLL.\r\n#Reencode\r\nnew_code = make_sec(dll, \".text2\", 0, True) #First use 0 as the size, this gives us the RVA of the ne\r\nascender = Ascender(dll[\"image_base\"] + new_code[\"rva\"])\r\nascender.ascend_func(start_f)\r\nencoded_code = ascender.write()\r\n#New code section\r\nnew_code = make_sec(dll, \".text2\", len(encoded_code), True)\r\nnew_code[\"data\"] = encoded_code\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 26 of 47\n\n#Save\r\nsave_pe(\"emotet.proc.dll\", dll, [idata, data_sec, new_code], idata)\r\nDeoffuscare le API usate\r\nDopo questa lunga digressione sulle tecniche di reficazione del codice x86, siamo in possesso di uno strumento,\r\nprimitivo e un po’ sgangherato, ma molto potente nella teoria: possiamo modificare il codice x86 di un eseguibile\r\nPE lavorando ad alto livello.\r\nVediamo come poterlo usare.\r\nQuando si vuole fare luce su un malware offuscato può tornare utile partire dalle funzioni più piccole.\r\nCon IDA è possibile ordinare le funzioni per dimensione crescente.\r\nSe adottiamo questo approccio con il campione offuscato di emotet, notiamo che la quarta funzione (in ordine) è\r\nusata per ottenere l’indirizzo del PEB.\r\nLa funzione di accesso al PEB.\r\nQuesta funzione è usata solo in un altro punto del codice, mostra qui sotto.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 27 of 47\n\nCosa farà questa funzione? Gli offset usati parlano chiaro.\r\nL’occhio esperto riconosce immediatamente gli offset usati nella struttura PEB, questa funzione scorre la lista dei\r\nmoduli (DLL) caricati in memoria.\r\nPossiamo quindi ipotizzare (dall’istruzione di xor successiva) che la chiamata evidenziata in giallo sia verso una\r\nfunzione che fa l’hashing del nome della DLL.\r\nAnalizzando la chiamata in questione si scopre essere proprio questo il caso.\r\nL’algoritmo di hashing è molto semplice e riportato qui sotto.\r\ndef hash(string, ci=False):\r\n res = 0\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 28 of 47\n\nfor x in string:\r\n res = (ord(x.lower() if ci else x) + (res \u003c\u003c 0x10) + (res \u003c\u003c 6) - res) \u0026 0xffffffff\r\n return hex(res)\r\nLa funzione di hashing di Emotet.\r\nLa funzione mostrata nella figura sopra non fa altro che recuperare il base address di una DLL, dato l'hashing del\r\nsuo nome.\r\nAnche questa funzione è utilizzata in un unico punto, insieme ad un'altra routine per recuperare un simbolo\r\nesportato da un PE.\r\nAbbiamo quindi trovato la funzione che importa le API.\r\nLa funzione che importa le API.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 29 of 47\n\nQuesta è usata sempre nel solito modo da Emotet: gli hash del modulo e della funzione da importare sono passati\r\na getAPI, la quale ritorna un puntatore all'API ottenuta.\r\nCome viene usata la funzione getAPI. Si notino i due hash passatogli.\r\nDa questo come possiamo trovare le API importate?\r\nEsattamente come abbiamo fatto con IDA. Il bello dello strumento che abbiamo macchinosamente creato è che ci\r\nindica per ogni funzione quali altre funzioni sono chiamate (tramite l'attributo callees ) e che ci dà la possibilità\r\ndi analizzare e modificare le istruzioni.\r\nPossiamo identificare getPEB come l'unica funzione che contiene mov eax, [fs:30h] e quindi vedere chi la\r\nchiama per ottenere getModule.\r\nRipetiamo quest'ultimo passo per ottenere getAPI.\r\nA questo punto possiamo scorrere il codice di tutti i BB per vedere se c'è una chiamata a getAPI , in caso\r\npositivo cerchiamo gli ultimi 4 push (qui occorre stare attenti al fatto che a volte sono presenti delle istruzioni pop,\r\nche se non gestite falserebbero il conteggio) e recupere gli hash.\r\nUna volta ottenuti gli hash, dobbiamo trovare a quali moduli ed API corrispondono.\r\nAbbiamo creato uno script che fa proprio questo e salva il risultato in un file JSON.\r\nimport sys\r\nimport json\r\nimport pefile\r\nimport os\r\nfrom collections import defaultdict\r\ndef hash(string, ci=False):\r\n res = 0\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 30 of 47\n\nfor x in string:\r\n res = (ord(x.lower() if ci else x) + (res \u003c\u003c 0x10) + (res \u003c\u003c 6) - res) \u0026 0xffffffff\r\n return hex(res)\r\nhashes = defaultdict(dict)\r\nfor filename in sys.argv[1:]:\r\n basename = os.path.basename(filename)\r\n module_hash = hash(basename, True)\r\n \r\n try:\r\n pe = pefile.PE(filename)\r\n except pefile.PEFormatError:\r\n continue\r\n hashes[module_hash][\"name\"] = basename\r\n hashes[module_hash][\"exports\"] = {}\r\n if not hasattr(pe, \"DIRECTORY_ENTRY_EXPORT\"):\r\n continue\r\n for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:\r\n if exp.name is None:\r\n continue\r\n export_hash = hash(exp.name.decode('ascii'))\r\n hashes[module_hash][\"exports\"][export_hash] = exp.name.decode('ascii')\r\n print(basename)\r\nwith open(\"exports.json\", \"w\") as f:\r\n json.dump(hashes, f)\r\nLo script casareccio per l'associazione tra hash ed API. Processa i PE passati da riga di comando e crea un file\r\nexports.json.\r\nGrazie al file export.json generato tramite lo script, possiamo finalmente ottenere, per ogni chiamata a getAPI ,\r\nl'effettiva API importata.\r\nNon ci rimane che modificare il codice affinchè chiami l'API direttamente.\r\nQuesto richiede la creazione di una directory di import nel file PE, questa operazione non verrà discussa in quanto\r\npiuttosto semplice.\r\nDato che Emotet è stato compilato usando la convenzione di chiamata C, possiamo semplicmente rimuovere la\r\nchiamata a getAPI e sostituire call eax con call [import] , dove import è il VA dell'entry opportuna\r\nnell'Import Address Table.\r\nIl risultato è mostrato di seguito.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 31 of 47\n\nIl sample di Emotet con le API importate.\r\nUna miglioria possible è quella di rimuovere interamente la funzione wrapper intorno alla chiamata API (quella\r\nche nella figura sopra abbiamo chiamato VirtualAllocAPI ).\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 32 of 47\n\nIl codice per l'importazione delle API è riportato sotto.\r\nLa procedura ritorna una sezione da passare a save_pe , è la sezione che contiene i dati della directory di import.\r\ndef import_apis(pe_dll):\r\n def _is_mov_eax_fs_30(inst):\r\n return (\r\n inst.code == Code.MOV_EAX_MOFFS32 and inst.op0_register == Register.EAX and inst.op_kind\r\n and inst.memory_segment == Register.FS and inst.memory_displacement == 0x30)\r\n def _is_retn(inst):\r\n return inst.code == Code.RETND\r\n def _is_call_to(inst, target):\r\n return inst.flow_control == FlowControl.CALL and inst.near_branch_target == target\r\n def is_call_eax(inst):\r\n return inst.flow_control == FlowControl.INDIRECT_CALL and inst.op_kind(0) == OpKind.REGISTER\r\n def _is_push_imm(inst):\r\n return inst.code in [Code.PUSHD_IMM8, Code.PUSHD_IMM32]\r\n #First we find the getPEB proc\r\n \"\"\"\r\n mov eax, large fs:30h\r\n retn\r\n \"\"\"\r\n get_peb_fun = None\r\n for _, f in Function.CACHE.items():\r\n if len(f.head.nexts) != 0 or len(f.head.insts) != 2:\r\n continue\r\n if not _is_mov_eax_fs_30(f.head.insts[0]) or not _is_retn(f.head.insts[1]):\r\n continue\r\n \r\n get_peb_fun = f\r\n break\r\n if get_peb_fun is not None:\r\n print(f\"Found getPEB at {hex(get_peb_fun.head.start_address)}\")\r\n else:\r\n return\r\n #Now the only function that call getPEB\r\n get_module_fun = None\r\n for _, f in Function.CACHE.items():\r\n if get_peb_fun in f.callees:\r\n get_module_fun = f\r\n break\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 33 of 47\n\nif get_module_fun is not None:\r\n print(f\"Found getModule at {hex(get_module_fun.head.start_address)}\")\r\n else:\r\n return\r\n #Now the only function that call getModule\r\n get_api_fun = None\r\n for _, f in Function.CACHE.items():\r\n if get_module_fun in f.callees:\r\n get_api_fun = f\r\n break\r\n \r\n if get_api_fun is not None:\r\n print(f\"Found getAPI at {hex(get_api_fun.head.start_address)}\")\r\n else:\r\n return\r\n xrefs = []\r\n #Now get the XREF to get API\r\n for _, bb in BasicBlock.CACHE.items():\r\n for n, inst in enumerate(bb.insts):\r\n if _is_call_to(inst, get_api_fun.head.start_address):\r\n xrefs.append((bb, bb.insts[n-1::-1]))\r\n break\r\n \r\n if len(xrefs) \u003e 0:\r\n print(f\"Found {len(xrefs)} XREFS to getAPI\")\r\n else:\r\n return\r\n #Now get the module and export hashes\r\n api_hashes = []\r\n for bb, insts in xrefs:\r\n module_hash = export_hash = None\r\n n_push = 0\r\n for inst in insts:\r\n if inst.flow_control == FlowControl.CALL:\r\n break\r\n if inst.mnemonic == Mnemonic.POP:\r\n n_push -= 1\r\n elif inst.mnemonic == Mnemonic.PUSH:\r\n if n_push == 0:\r\n if not _is_push_imm(inst):\r\n break\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 34 of 47\n\nmodule_hash = inst.immediate(0)\r\n elif n_push == 3:\r\n if _is_push_imm(inst):\r\n export_hash = inst.immediate(0)\r\n break\r\n n_push += 1\r\n if module_hash is not None and export_hash is not None:\r\n api_hashes.append((bb, module_hash ^ 0x39CEFC97, export_hash ^ 0x5E3043F1))\r\n else:\r\n print(f\"API NOT PROCESSED: {hex(bb.start_address)}\")\r\n print(f\"Found {len(api_hashes)} API hashes\")\r\n \r\n #Resolve the hashes\r\n import json\r\n with open(\"exports.json\") as f:\r\n exports = json.load(f)\r\n apis = []\r\n for bb, mod, exp in api_hashes:\r\n if hex(mod) in exports:\r\n apis.append((bb, exports[hex(mod)][\"name\"], exports[hex(mod)][\"exports\"][hex(exp)]))\r\n #Make the import section\r\n idata, api_patch = make_import_sec(apis, pe_dll)\r\n \r\n #Now, nop the call to getAPI and replace the call eax after it\r\n for bb, dll, exp in apis:\r\n i = -1\r\n while i + 1 \u003c len(bb.insts):\r\n i += 1\r\n inst = bb.insts[i]\r\n if _is_call_to(inst, get_api_fun.head.start_address):\r\n \r\n bb.insts.remove(inst)\r\n for j in range(i+1, len(bb.insts)):\r\n inst_2 = bb.insts[j]\r\n if is_call_eax(inst_2):\r\n bb.insts.remove(inst_2)\r\n #print(f\"Patching API to {hex(pe_dll['image_base'] + api_patch[dll][exp])}\")\r\n bb.insts.insert(j, Instruction.create_mem(Code.CALL_RM32,\r\n MemoryOperand(displ = pe_dll[\"image_base\"] + api_patch[dll][exp]) ))\r\n bb.update_len()\r\n break\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 35 of 47\n\nreturn idata\r\nIl codice per ripristinare l'import delle API.\r\nRipristino delle stringhe\r\nSeguendo Emotet con un debugger, si nota che le stringhe sono deoffuscate al volo.\r\nDa analisi manuale è possibile riconoscere due metodi per la decodifca delle stringhe, entrambi identici se non nel\r\nformato di output (uno unicode, l'altro no).\r\nIl reverse engineering dell'algoritmo è piuttosto semplice, riportiamo direttamente il formato delle stringhe.\r\nLa chiave XOR è, appunto, xorata con la lunghezza ed i byte della stringa.\r\nPer identificare i metodi di decodifica possiamo:\r\n1. Trovare tutte le istruzioni mov e push che hanno un operando che fa riferimento ad una stringa dentro la\r\nsezione .data.\r\n2. Provare a decodificare ogni singola stringa e scartare le istruzioni che non portano a stringhe valide (troppo\r\nlunghe o con caratteri \u003e= 0x80).\r\n3. Delle restanti istruzioni, trovare la chiamata successiva più prossima. Otteniamo un insieme di funzioni\r\ncandidate ad essere quelle di decodifica.\r\n4. Ridurre la lista di candidati eliminando quelli che sono chiamati da altri candidati (questo succede quando\r\nla stringa è passata ad una funzione wrapper che chiama quella di decodifica).\r\n5. Se si ottengono solo due candidati, quello con più chiamate è la funzione di decodifica unicode, l'altra\r\nquella ANSI.\r\nDi questi passi, solo il quinto è arbitrario, il resto fa uso solo delle informazioni presenti del codice parsato.\r\nLa chiamata alla funzione di decodifica può quindi essere sostituita con un'istruzione mov eax, imm32 che\r\nottiene il puntatore alla stringa già decodificata.\r\nPer fare questo è necessario aggiungere un'altra sezione dati (.data2) al PE di Emotet.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 36 of 47\n\nLe stringhe di Emotet, da queste si intuisce il funzionamento del malware.\r\nIl codice per la decodifica delle stringhe è riportato sotto, la procedura ritorna la sezioni dati da passare a\r\nsave_pe .\r\ndef decode_strings(dll):\r\n def _decode_string(data, offset, unicode):\r\n xor_key = struct.unpack(\" 0x80:\r\n return None\r\n res = \"\"\r\n offset += 8\r\n while str_len \u003e 0:\r\n cur_data = xor_key ^ struct.unpack(\" 1:\r\n res += chr((cur_data \u003e\u003e8) \u0026 0xff)\r\n res += (\"\\0\" if unicode else \"\")\r\n if str_len \u003e 2:\r\n res += chr((cur_data \u003e\u003e16) \u0026 0xff)\r\n res += (\"\\0\" if unicode else \"\")\r\n if str_len \u003e 3:\r\n res += chr((cur_data \u003e\u003e24) \u0026 0xff)\r\n res += (\"\\0\" if unicode else \"\")\r\n str_len -= 4\r\n offset += 4\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 37 of 47\n\nres += \"\\0\"\r\n res += (\"\\0\" if unicode else \"\")\r\n return res\r\n def _is_null_sub(addr):\r\n return len(Function.CACHE[inst.near_branch_target].head.nexts) == 0 and len(Function.CACHE[in\r\n #Look for push and mov of offsets in the data sections\r\n data_va_min = dll[\"image_base\"] + dll[\"data\"][\"rva\"]\r\n data_va_max = data_va_min + dll[\"data\"][\"size\"]\r\n data = []\r\n decoders = {}\r\n for _, bb in BasicBlock.CACHE.items():\r\n armed = False\r\n for n, inst in enumerate(bb.insts):\r\n if inst.code == Code.PUSHD_IMM32 and inst.immediate(0) \u003e= data_va_min and inst.immediate\r\n data.append((bb, n, inst.immediate(0)))\r\n armed = True\r\n elif inst.code == Code.MOV_R32_IMM32 and inst.immediate(1) \u003e= data_va_min and inst.immedi\r\n data.append((bb, n, inst.immediate(1)))\r\n armed = True\r\n elif armed and inst.flow_control == FlowControl.CALL:\r\n if not _is_null_sub(inst.near_branch_target):\r\n addr = inst.near_branch_target\r\n decoders[addr] = decoders.get(addr, 0)\r\n decoders[addr]+=1\r\n bb, on, va = data[-1]\r\n data[-1] = (bb, on, va, n, addr)\r\n armed = False\r\n #Remove functions that call other functions marked as decoders\r\n to_remove = []\r\n wrappers = {}\r\n decoders_addrs = [x for x in decoders.keys()]\r\n for i in range(0, len(decoders)):\r\n others = decoders_addrs[0:i] + decoders_addrs[i+1:]\r\n cur_decoder = Function.CACHE[decoders_addrs[i]]\r\n for callee in cur_decoder.callees:\r\n if callee.head.start_address in others:\r\n to_remove.append(cur_decoder.head.start_address)\r\n wrappers[decoders_addrs[i]] = callee.head.start_address\r\n for r in to_remove:\r\n del decoders[r]\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 38 of 47\n\nif len(decoders) != 2:\r\n raise ValueError(\"Cannot recognize the decoders!\")\r\n decoders_addrs = [x for x in decoders.keys()]\r\n unicode_dec = decoders_addrs[0] if decoders[decoders_addrs[0]] \u003e decoders[decoders_addrs[1]] else\r\n singlebyte_dec = decoders_addrs[0] if decoders[decoders_addrs[0]] \u003c= decoders[decoders_addrs[1]]\r\n #print(f\"Found {len(data)} references to data.\")\r\n #print(f\"Unicode decoder: {hex(unicode_dec)}, single byte decoder: {hex(singlebyte_dec)}.\")\r\n #Decode strings\r\n data_sec = make_sec(dll, \".data2\", dll[\"data\"][\"size\"] * 2, True)\r\n data_offset = 0\r\n data_data = bytearray(dll[\"data\"][\"size\"] * 2)\r\n data_rva = dll[\"image_base\"] + data_sec[\"rva\"]\r\n for bb, n_data, vaddr, n_call, call_target in [d for d in data if len(d) == 5]:\r\n offset = vaddr - dll[\"image_base\"] - dll[\"data\"][\"rva\"]\r\n string = _decode_string(dll[\"data\"][\"data\"], offset, call_target != singlebyte_dec)\r\n if string is None:\r\n continue\r\n new_va = data_rva + data_offset\r\n data_data[data_offset:data_offset + len(string)] = string.encode(\"ascii\")\r\n data_offset += len(string)\r\n new_inst = Instruction.create_reg_u32(Code.MOV_R32_IMM32, Register.EAX, new_va)\r\n \r\n \"\"\"\r\n if bb.insts[n_data].mnemonic == Mnemonic.PUSH:\r\n new_inst = Instruction.create_u32(Code.PUSHD_IMM32, new_va)\r\n else:\r\n new_inst = Instruction.create_reg_u32(Code.MOV_R32_IMM32, Register.EAX, new_va)\r\n \"\"\"\r\n del bb.insts[n_call]\r\n bb.insts.insert(n_call, new_inst)\r\n bb.update_len()\r\n data_sec[\"data\"] = data_data\r\n return data_sec\r\nIl codice per la decodifica delle stringhe.\r\nAnche in questo caso si tratta di codice prototipo.\r\nQui, come nel caso delle API importate, non sono aggiunte rilocazioni per gli indirizzi usati.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 39 of 47\n\nIl PE ottenuto non è quindi usabile per il debug a meno di non forzarne la caricatura al suo base address designato\r\n(ad esempio rimuovengo il flag \"DLL can move\" dall'header PE).\r\nRimozione della CFO\r\nQuesta è la parte più complessa (e buggata) dello script che stiamo costruendo.\r\nOsserviamo il codice sottostante.\r\nIl codice che dirama il controllo in base al token.\r\nAvevamo già notato che questa forma di CFO si basa sul confronto di un valore, detto token, contenuto in un\r\nregistro. In base al valore attuale del token, l'esecuzione finisce in un blocco piuttosto che in un altro.\r\nLa prima sfida consiste nell'identificare i BB di una funzione che effettuano questi confronti (chiamiamoli \"BB di\r\ndiramazione\"). Possiamo dire che hanno tutti queste caratteristiche:\r\nSono composti da due, tre od una sola istruzione.\r\nNel caso vi siano due istruzioni, la prima è del tipo cmp rm32, imm32 o cmp r32, rm32 . Ovvero un\r\nconfronto tra un registro ed un valore immediato o un altro registro. La seconda è un salto condizionale.\r\nNel caso vi sia solo un'istruzione, questa è un salto condizionale.\r\nI salti condizionali hanno la forma jg , jz / je e jnz / jne .\r\nNel caso vi siano tre istruzioni, la prima è del tipo mov rm32, imm32 e le altre due come nel punto 2.\r\nOvvero, la prima istruzione imposta il registro, usato come secondo operando nel confronto, ad un valore\r\nnoto.\r\nIl registro usato per contenere il token è variabile.\r\nQuesti punti non identificano i BB di diramazione in modo univoco, vi possono essere dei falsi positivi, in fin dei\r\nconti operazioni di confronto e salto compaiono spesso anche in funzioni non offuscate.\r\nDobbiamo quindi aggiungere delle euristiche per limitare i falsi positivi, ad esempio:\r\nDal primo blocco di una funzione raggiungiamo il primo blocco che ha due successori. Questo è un BB di\r\ndiramazione se ha almeno due istruzioni, è nella forma indicata dalla lista sopra e almeno uno dei suoi\r\nsuccessori è, a sua volta, un BB di diramazione.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 40 of 47\n\nUn successore del primo BB di diramazione è, a sua volta, un BB di diramazione se è nella forma indicata\r\ndalla lista sopra.\r\nChiamiamo il primo blocco di diramazione (punto 1), il \"blocco della CFO\".\r\nQuando siamo in grado di identificare in modo abbastanza affidabile i BB di diramazione, possiamo focalizzarci\r\nsul problema successivo: costrutire una mappa che per ogni valore del token ci dia il BB a cui arriva l'esecuzione.\r\nAd esempio, limitatamente ai blocco mostrati nella figura in alto, vogliamo costruire la mappa seguente:\r\n{\r\n0x2C2F6692: \u003cBB a 0x1001c112\u003e,\r\n0x31BCED90: \u003cBB a 0x1001c108\u003e,\r\n0x00D59B4E: \u003cBB a 0x1001bf1e\u003e,\r\n0x36A336EE: \u003cBB a 0x1001c08b\u003e,\r\n}\r\nMappa che per ogni token indica il BB di destinazione.\r\nPer costruire questa mappa usiamo una funzione ricorsiva che è inizialmente richiamata sul primo BB di\r\ndiramazione. Questa funzione necessità di sapere lo stato (parziale) dei registri e l'ultimo valore usato per il\r\nconfronto.\r\nCon queste informazioni, quando trova un salto nella forma jz o jnz, può associare l'ultimo valore usato per il\r\nconfronto con una delle destinazioni del salto.\r\nCi serve quindi una funzione che processi ogni istruzione di un BB per costrutire una mappa dei valori contenuti\r\nnei registri, limitandosi a considerare solo le istruzioni mov rm32, imm32 e mov r32, rm32 , poichè sono queste\r\nquelle usate per impostare eventuali registri usati nei confronti.\r\n def _compute_registers(self, insts, cur_reg={}):\r\n registers = cur_reg.copy()\r\n for inst in insts:\r\n if self._is_mov_rm32_imm32(inst):\r\n registers[inst.op0_register] = inst.immediate(1)\r\n if self._is_mov_r32_rm32_any(inst) and inst.op1_register in registers:\r\n registers[inst.op0_register] = registers[inst.op1_register]\r\n return registers\r\n def _is_cfo_bb(self, bb):\r\n return len(bb.nexts) == 2 and (\r\n len(bb.insts) == 1 and self._is_jcc(bb.insts[0])\r\n or\r\n len(bb.insts) == 2 and self._is_cmp_r32_ri32(bb.insts[0]) and self._is_jcc(bb.insts[1])\r\n or\r\n len(bb.insts) == 3 and self._is_mov_rm32_imm32(bb.insts[0]) and self._is_cmp_r32_ri32(bb\r\n and bb.insts[0].op0_register == bb.insts[1].op1_register\r\n )\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 41 of 47\n\n#Make a map {token: bb} than tell for every token the BB of destination\r\n def _map_token(self, tokens, last_rhs, registers, bb, first=True):\r\n if first == False and not self._is_cfo_bb(bb):\r\n print(\"Stopped because not first and not a CFO BB : \" + hex(bb.start_address))\r\n bb._show()\r\n return\r\n if first == True and (not self._is_cfo_bb(bb) or (not self._is_cfo_bb(bb.nexts[0]) and not se\r\n print(\"Stopped because first and not a CFO BB followed by CFOs : \" + hex(bb.start_addres\r\n bb._show()\r\n return\r\n if not first and bb == self._cfo_start:\r\n return\r\n if len(bb.insts) == 2:\r\n cmp_reg, cmp_imm = self._get_cmp_operands(bb.insts[0], registers)\r\n if cmp_reg is None or cmp_imm is None:\r\n raise ValueError(\"Excepted a cmp instruction!\")\r\n if cmp_reg != self._cfo_reg:\r\n raise ValueError(\"Expected the CFO cmp reg!\")\r\n if not self._is_jcc(bb.insts[1]):\r\n raise ValueError(\"Excepted a jcc instruction!\")\r\n cc = bb.insts[1].condition_code\r\n elif len(bb.insts) == 1:\r\n cmp_imm = last_rhs\r\n if cmp_imm is None:\r\n raise ValueError(\"Expected a non null rhs!\")\r\n cc = bb.insts[0].condition_code\r\n elif len(bb.insts) == 3:\r\n registers = self._compute_registers(bb.insts, registers)\r\n cmp_imm = registers[bb.insts[0].op0_register]\r\n cc = bb.insts[2].condition_code\r\n if cc == ConditionCode.E:\r\n tokens[cmp_imm] = bb.nexts[1]\r\n self._map_token(tokens, cmp_imm, registers, bb.nexts[0], False)\r\n elif cc == ConditionCode.NE:\r\n tokens[cmp_imm] = bb.nexts[0]\r\n self._map_token(tokens, cmp_imm, registers, bb.nexts[1], False)\r\n else:\r\n self._map_token(tokens, cmp_imm, registers, bb.nexts[0], False)\r\n self._map_token(tokens, cmp_imm, registers, bb.nexts[1], False)\r\nCodice per la costruzione della mappa token-BB.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 42 of 47\n\nUna volta ottenuta questa mappa, l'idea generale per rimuovere l'offuscazione CFO è quella di partire dal primo\r\nblocco di una funzione e seguire tutti i successori, calcolando nel frattendo i valori dei vari registri.\r\nQualora un successore sia il blocco della CFO (vedi definizione sopra), lo sostituiamo con il blocco indicatoci\r\ndalla mappa appena costruita.\r\nDobbiamo però considerare altri tre aspetti importanti, i primi due si risolvono con lo stesso accorgimento e li\r\ntrattiamo insieme. Ci riferiamo ai cicli (che risulterebbero in una ricorsione infinita) e ai blocchi condivisi.\r\nRiconsideriamo la figura in alto, ma focalizzandosi su un blocco in particolare.\r\nIl blocco evidenziato è un blocco condiviso, due esecuzioni vi passano sopra con valori del token\r\nche sono diversi.\r\nIl blocco evidenziato è un blocco condiviso da due rami di esecuzioni (il primo proveniente dal blocco sopra,\r\nl'altro da un altro blocco non mostrato) che hanno due valori diversi del token.\r\nIl nostro algoritmo si limiterebbe a cambiare il successo di questo blocco prima verso una destinazione e poi verso\r\nl'altra, risultato così in un grafo non corretto.\r\nPer ovviare a questo problema, salviamo per ogni blocco lo stato dei registri alla fine della sua esecuzione, nel\r\ncaso questa differisse con lo stato creato da una precedente esecuzione, il blocco è clonato.\r\nL'indirizzo a cui mettere il blocco clonato può essere arbitrario, purchè unico, questo è il vantaggio di aver\r\nreificato l'esecuzione tramite i BB.\r\nInoltre, se il valore dei registri risulta identico a quello già calcolato da un'altra esecuzione, il blocco è già stato\r\nprocessato e possiamo interrompere il ramo di esecuzione corrente, di fatto rompedo la ricorsione infinita.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 43 of 47\n\nL'ultimo problema sta nel modo in cui alcuni BB calcolano il token successivo.\r\nNella maggioranza dei casi viene usata un'istruzione mov ma è presente anche un'altra variante, usata come\r\nottimizzazione al posto di un salto.\r\nQuesta variante è usata quanto vi è una chiamata ad una funzione che ritorna un valore 0 o non-0 e, in base a\r\nquesto, due possibili token sono scelti.\r\nAnzichè usare un \"if\" per scegliere uno dei due valori, t0 o t1 , viene usata la formula token = t0 + (t1-t0)\r\n\u0026 (0xffffffff if eax != 0 else 0) .\r\nIl registro ESI contiene il token, la chiamata in alto ritorna un valore nullo o non nullo ed in base a\r\nquesta dicotomia, un token viene scelto.\r\nL'espressione 0xffffffff if eax != 0 else 0 è calcolata con due istruzioni: neg , che setta il CF solo se eax\r\nnon è zero, e sbb (sub with borrow) che ritorna 0, se CF è 0, o 0xffffffff, se CF è 1.\r\nQuesto è un caso particolare che va considerato a parte, qualora rilevassimo la presenza di queste istruzioni,\r\ndobbiamo considerare come se il blocco avesse due successori, ognuno con il proprio stato dei registri (che\r\ndifferisce solo per il registro in cui è messo il token).\r\nUn'altra complicazione è dovuta al fatto che a volte l'ultima istruzione si trova in un BB a parte, dobbiamo quindi\r\ngestire anche questo caso particolare.\r\nIl codice finale è quello del metodo _cfo_trace della classe Function .\r\ndef _cfo_trace(self, registers, bb, parent_bb, parent_next_index):\r\n print(f\"Tracing block {hex(bb.start_address)}.\")\r\n print(f\"Last registers: {bb._cfo_last_registers is not None}.\")\r\n print(f\"Last token: {bb._cfo_last_registers[self._cfo_reg] if bb._cfo_last_registers is not N\r\n #If this is the last block, done\r\n if len(bb.nexts) == 0:\r\n return\r\n #Compute the registers at the end of the block\r\n new_registers = self._compute_registers(bb.insts, registers)\r\n print(f\"Registers computed, token is {hex(new_registers[self._cfo_reg])}.\")\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 44 of 47\n\n#Dynamic branch case\r\n branch_z, branch_nz = self._get_bb_dynamic_branches(bb)\r\n if branch_z is not None and branch_nz is not None:\r\n print(f\"Dynamic branches.\")\r\n if (len(bb.nexts) != 1 or bb.nexts[0] != self._cfo_start):\r\n raise ValueError(\"WEIRD CASE!\")\r\n \r\n #Make the new regs\r\n regs_branch_z = new_registers.copy()\r\n regs_branch_z[self._cfo_reg] = branch_z\r\n regs_branch_nz = new_registers.copy()\r\n regs_branch_nz[self._cfo_reg] = branch_nz\r\n #Update this block regs with a 64-bit value holding both values\r\n new_registers[self._cfo_reg] = branch_z | (branch_nz \u003c\u003c 32)\r\n print(f\"Registers computed, token is {hex(new_registers[self._cfo_reg])}.\")\r\n #If this is has already been done, stop\r\n cfo_token = new_registers[self._cfo_reg]\r\n if bb._cfo_last_registers and bb._cfo_last_registers[self._cfo_reg] == cfo_token:\r\n print(\"ALREADY PROCESSED\")\r\n return\r\n print(f\"{hex(bb.start_address)} never processed with {hex(cfo_token)}.\")\r\n #Make the dinamic branches explicit\r\n if branch_z is not None and branch_nz is not None:\r\n print(\"Dynamic branch processing.\")\r\n bb.insts = bb.insts[:-6]\r\n bb.insts.append(Instruction.create_reg_reg(Code.TEST_RM32_R32, Register.EAX, Register.EAX\r\n bb.insts.append(Instruction.create_branch(Code.JE_REL32_32, self._cfo_start.start_address\r\n bb.update_len()\r\n bb.nexts = [self._cfo_tokens[branch_nz], self._cfo_tokens[branch_z]]\r\n self._cfo_trace(regs_branch_nz, bb.nexts[0], bb, 0)\r\n self._cfo_trace(regs_branch_z, bb.nexts[1], bb, 1)\r\n else:\r\n #If this has been done but with different register, copy the block\r\n if bb._cfo_last_registers and bb._cfo_last_registers[self._cfo_reg] != cfo_token:\r\n print(\"Copying block.\")\r\n bb = bb.duplicate()\r\n parent_bb.nexts[parent_next_index] = bb\r\n #Move to each next bb, but skip CFO\r\n bb._cfo_last_registers = new_registers\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 45 of 47\n\nfor n, nbb in enumerate(bb.nexts):\r\n if nbb == self._cfo_start:\r\n bb.nexts[n] = self._cfo_tokens[cfo_token]\r\n print(f\"Successor {n} goes to CFO, replacing with {hex(bb.nexts[n].start_address\r\n self._cfo_trace(bb._cfo_last_registers, bb.nexts[n], bb, n)\r\nIl metodo per la rimozione della CFO di una funzione.\r\nLe tecniche usate necessitano ancora di vari affinamenti, ma siamo riusciti a deoffuscare la routine principale di\r\nEmotet.\r\nLa routine principale di Emotet, prima (a sinistra) di essere processata, e dopo (a destra).\r\nPer riferimento (e solo per questo), è possibile scaricare lo script usato qui.\r\nFacciamo presente, e rimarcato dall'estensione .txt usata, che lo script non è da intendersi come un deoffuscatore\r\nma piuttosto come esempio di alcune tecniche di deoffuscazione.\r\nLo script contenuto nell'archivio allegato nel paragrafo precedente, contiene alcune migliorie rispetto al codice\r\nmostrato in questo articolo.\r\nIl DB IDA della DLL processa è scaricabile qui (si ricorda che si tratta essenzialmente di un PoC).\r\nBreve panoramica di Emotet\r\nDi Emotet si parla già a sufficienza in letteratura, vogliamo qui solo dare una panoramica superficiale di quello\r\nche è più facilmente intuibile dall'analisi statica della DLL deoffuscata.\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 46 of 47\n\nQuest'ultima carica le seguenti librerie: advapi32.dll, crypt32.dll, shell32.dll, shlwapi.dll, urlmon.dll, userenv.dll,\r\nwininet.dll.\r\nIn questo modo il meccanismo di importazione della API visto precedentemente, che fa uso del PEB e della\r\nrelativa lista di moduli caricati, può trovare le funzioni necessarie.\r\nEffettua una serie di controlli e, se tutti i requisiti sono soddisfatti, crea una copia di se stesso all'interno di una\r\nnuova cartella, generata con caratteri casuali, dentro %LocalAppData%. In caso contrario invoca ExitProcess e\r\ntermina l'esecuzione.\r\nSuccessivamente genera un servizio con rundll32.exe , quindi il loader chiama la funzione\r\nChangeServiceConfig2W per modificarne la descrizione.\r\nLa chiamata API a OpenSCManagerW viene utilizzata per pianificare operazioni che ne garantiscono la persistenza\r\nal riavvio della macchina. Altro metodo utilizzato per la persistenza è l'uso della chiave di registro\r\n\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\" come abbiamo visto nella sezione decodificata.\r\nTramite la chiamata a PathFindFileNameW verifica la presenza di determinati file sul disco, enumera i servizi\r\n( EnumServicesStatusExW, Process32FirstW, Process32NextW) , acquisisce informazioni sulla macchina\r\n( GetComputerNameExW ), infine i dati raccolti vengono cifrati tramite l'uso della libreria crypt32 e inviati al C2\r\ntramite richiesta POST.\r\nLa presenza delle API InternetXXX mostra chiaramente come Emotet comunichi con il C2.\r\nLa stringa \"%u.%u.%u.%u\" evidenzia l'utilizzo di un indirizzo IP, anzichè di un hostname, per identificare il C2.\r\nSource: https://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nhttps://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/\r\nPage 47 of 47",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://cert-agid.gov.it/news/malware/semplificare-lanalisi-di-emotet-con-python-e-iced-x86/"
	],
	"report_names": [
		"semplificare-lanalisi-di-emotet-con-python-e-iced-x86"
	],
	"threat_actors": [],
	"ts_created_at": 1775434694,
	"ts_updated_at": 1775791273,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/bbe2189646401528639e5ca7b243de9ef5b09a15.pdf",
		"text": "https://archive.orkl.eu/bbe2189646401528639e5ca7b243de9ef5b09a15.txt",
		"img": "https://archive.orkl.eu/bbe2189646401528639e5ca7b243de9ef5b09a15.jpg"
	}
}