{
	"id": "ed2881a5-cefe-41fd-bab2-926cdb270c0f",
	"created_at": "2026-04-06T00:13:08.645159Z",
	"updated_at": "2026-04-10T13:12:41.22758Z",
	"deleted_at": null,
	"sha1_hash": "c146846f0f87432527d2d6056547e35eda8c9997",
	"title": "Evading Detection with Excel 4.0 Macros and the BIFF8 XLS Format",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 316863,
	"plain_text": "Evading Detection with Excel 4.0 Macros and the BIFF8 XLS\r\nFormat\r\nBy Malware Enthusiast\r\nPublished: 2020-05-13 · Archived: 2026-04-05 19:23:53 UTC\r\nAbusing legacy functionality built into the Microsoft Office suite is a tale as old as time. One functionality that is\r\npopular with red teamers and maldoc authors is using Excel 4.0 Macros to embed standard malicious behavior in\r\nExcel files and then execute phishing campaigns with these documents. These macros, which are fully\r\ndocumented online, can make web requests, execute shell commands, access win32 APIs, and have many other\r\ncapabilities which are desirable to malware authors. As an added bonus, the Excel format embeds macros within\r\nMacro sheets which can be more challenging to examine statically than VBA macros which are easier to extract.\r\nAs a result, many malicious macro documents have a much lower than expected rate of detection in the AV world.\r\nMalware campaigns, such as the ZLoader campaign (described in great detail by InQuest Labs here, here, and\r\nhere) are actively abusing this functionality to perform mass phishing attacks. The campaign is so prolific that I’ve\r\nactually received one of these maldocs in one of my personal email accounts. Because of its effectiveness and low\r\ndetection rate, this technique is also popular in the penetration testing community. Outflank described how to\r\nembed shellcode in Excel 4.0 Macros in 2018, and tooling has been published to abuse this functionality via\r\nExcel’s ExecuteExcel4Macro VBA API.\r\nWhile there is clearly already a spotlight on the subject of Excel 4.0 Macros, I believe that only the surface of this\r\nattack vector has been scratched. There’s no doubt that defenders are building better signal on malicious macros\r\n(one of the tools which originally had 0 detections on VirusTotal is now up to 15 at the time of writing this post),\r\nbut there is also evidence that some of this signal can be brittle and unreliable.\r\nFor example, the ZLoader campaign obfuscates its macros using a series of cells that build each command from\r\nCHAR expressions. Ex: =CHAR(61) evaluates to the = character.\r\nThere’s plenty to build a signature on in this sheet:\r\nThe repeated usage of the =CHAR(#) cells to define formula content one character at a time.\r\nThe use of the Auto_Open label which triggers automatic execution of the macro sheet once the “Enable\r\nContent” button is pressed.\r\nZLoader marks their macro sheets as hidden which has a detectable static signature\r\nThe use of numerous Formula expressions to dynamically generate additional expressions at runtime.\r\nA lot of this would appear to be good enough signal to just block outright – Windows Defender, for example,\r\nconsiders just about any usage of =CHAR(#) to be malicious. Making an empty macro sheet that contains one cell\r\nwith =CHAR(42) and another with =HALT() will immediately flag the document as malicious:\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 1 of 9\n\nIf you try to save this document with Windows Defender enabled, it will block the save operation\r\nThis is probably a bit overkill, but apparently the number of legitimate users that do this is small enough that\r\nWindows can roll out a patch to all machines marking it malicious. A more reasonable signature, which seems\r\nresistant to false positives, is @DissectMalware’s macro_sheet_obfuscated_char YARA rule:\r\nrule macro_sheet_obfuscated_char\r\n{\r\nmeta:\r\ndescription = \"Finding hidden/very-hidden macros with many CHAR functions\"\r\nAuthor = \"DissectMalware\"\r\nSample = \"0e9ec7a974b87f4c16c842e648dd212f80349eecb4e636087770bc1748206c3b (Zloader)\"\r\nstrings:\r\n$ole_marker = {D0 CF 11 E0 A1 B1 1A E1}\r\n$macro_sheet_h1 = {85 00 ?? ?? ?? ?? ?? ?? 01 01}\r\n$macro_sheet_h2 = {85 00 ?? ?? ?? ?? ?? ?? 02 01}\r\n$char_func = {06 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 1E\r\n3D 00 41 6F 00}\r\ncondition:\r\n$ole_marker at 0 and 1 of ($macro_sheet_h*) and #char_func \u003e 10\r\n}\r\nThis rule looks for three things:\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 2 of 9\n\n1. The standard magic header for Office documents D0CF11E0A1B11AE1 at the start of the file.\r\n2. A macro sheet (defined in a BoundSheet8 BIFF Record) with a hidden state set to Hidden or VeryHidden.\r\n3. The presence of at least 10 Formula BIFF Records which have an Rgce field containing two Ptg structures\r\n– a PtgInt representing the value 0x3D (which maps to the = character) and a PtgFunc with an Ftab value\r\nof 0x6F (the matching tab value for the CHAR function).\r\nUnless you are fairly acquainted with the Excel 2003 Binary format (also known as BIFF8), the third search\r\ncondition is likely to read as a series of random letters jammed together rather than anything coherent. To better\r\nunderstand what exactly is being discussed, let’s take a quick detour into the BIFF8 file format.\r\nThe Excel 97-2003 Binary File Format (BIFF8)\r\nBefore office documents were saved in the Open Office XML (OOXML) format, they were saved in a much more\r\nsuccinct binary format focused on describing the maximum amount of information with the minimum number of\r\nbytes. Legacy office documents are stored in a Compound Binary File Format (CBF) while their actual application\r\nspecific data (such as Word document content or Excel workbook information) is stored within binary streams\r\nembedded in the CBF header.\r\nExcel’s workbook stream is a direct series of  Binary Interchange File Format (BIFF) records. The records are\r\nfairly simple – there are 2 bytes for describing the record type, 2 bytes for describing the remaining length of the\r\nrecord, and then the relevant record bytes. An Excel workbook is just a series of BIFF records beginning with a\r\nBOF record and eventually ending with a final EOF record. Microsoft’s Open Specifications project has helpfully\r\ndocumented every one of these records online. For example, if we are parsing a stream and read a record\r\nbeginning with the byte sequence 85 00 0E 00 , we are reading a BoundSheet8 record that is 14 bytes long.\r\nFrom Microsoft’s documentation we can see that BoundSheet8 records contain a 4 byte offset pointing to the\r\nrelevant BOF record, 2 bits used for describing the visible state of the sheet, a single byte used for describing the\r\nsheet type, and a variable number of bytes used for the name of the sheet.\r\nHex dump of a VeryHidden Macro sheet’s BoundSheet8 BIFF record\r\nThe above hex dump represents a BoundSheet8 record for a Macro sheet that has been “Very Hidden” –\r\nessentially made inaccessible from within Excel’s UI. This record would match the YARA sig byte regex of\r\n$macro_sheet_h2 = {85 00 ?? ?? ?? ?? ?? ?? 02 01} . The signature begins with the matching BIFF record id\r\nfor BoundSheet8 ( 85 00 ), then ignores the size (2 bytes) and the lbPlyPos record (4 bytes). It then matches the\r\nhsState field ( 02 ) followed by the byte indicating that the sheet is a macro sheet ( 01 ). This is a reasonable\r\nmatch for sheets that follow the BIFF8 specification.\r\nFiddling with BIFF Records\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 3 of 9\n\nHowever, there are a few tricks to essentially dodge this signature component by abusing flexibility in the\r\nspecification. For example, the hsState field is only supposed to be represented by 2 bits – the remaining 6 bits of\r\nthat byte are reserved. Theoretically this means that touching these bits should invalidate a spreadsheet, but this is\r\nnot what happens in practice. Say we replaced the value 02 (b’00000010 in binary) with a different value by\r\nflipping some bits (b’10101010) like AA – would Excel also treat that as a hidden sheet? I can’t speak for all\r\nversions of Excel, but in my testing with Excel 2010 and 2019, the answer is yes.\r\nEssentially, by following the majority of the specification, but not following the exact way that Excel has\r\ntraditionally generated these documents creates an entirely new set of Excel binary sheets which bypasses most\r\nstatic signatures. The remainder of this blog post will focus on a few examples of abusing the BIFF8 specification\r\nto create alternate, but valid, Excel documents.\r\nLabel (Lbl) Records\r\nLbl records are used for explicitly naming cells in a worksheet for reference by other formulas. In some cases, Lbl\r\nrecords can contain macros or trigger the download and execution of other macros. From a malicious macro\r\nauthor’s perspective, though, the most likely usage of a Lbl record is to define the Auto_Open cell for their\r\nworkbook. If a workbook has an explicitly defined Auto_Open cell then, once macros are enabled, Excel will\r\nimmediately begin evaluating the macros defined at that cell and continue evaluating cells below it until a\r\nHALT() function is invoked. Understandably, the existence of an Auto_Open Lbl record is considered fairly\r\nsuspicious, so there are a number of workarounds attackers have taken to hide their usage of this functionality.\r\nLet’s see if there are some other evasion techniques hiding in the Lbl record specification:\r\nBy default, when an Auto_Open label is defined in a BIFF8 document, it has its fBuiltin flag set to true, and its\r\nname field set to the value 01 , indicating that this is an Auto_Open function. The first 17 bytes of this record\r\n(21 if you include the 4 byte header) can likely be used as a signature to identify usage. This does assume a lack of\r\nmeddling with the reserved bytes which default to 00 , but signatures could probably replace these with wildcard\r\nbytes and not pick up too many false positives. Given that normal labels are never going to have a single byte\r\nvalue of 01 , there is a very small chance of triggering false positives with this as well.\r\nA default Lbl entry for Auto_Open\r\nIf a user attempts to save any variation on the Auto_Open label (like alternative capitalization AuTo_OpEn),\r\nExcel will automatically convert it back to the shortened fBuiltin version shown above. However, when Excel\r\nopens an OOXML formatted workbook there is no equivalent shorthand record for Auto_Open, it is simply stored\r\nas a string. So what happens if we explicitly create a Lbl record, leave fBuiltin as false, and give it a name of\r\nAuto_Open?\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 4 of 9\n\nA Lbl record with fBuiltin flipped to false, and the Name field set to Auto_Open\r\nIf a Lbl record is generated with these properties and inserted into an Excel document, Excel will still treat the\r\nreferenced cell as an Auto_Open cell and trigger it. So we can create a label that triggers Auto_Open behavior\r\nbut doesn’t look like the default record. This is a good start, but once a technique like this became well known it\r\nwould also be vulnerable to a quick signature. As is, there are already plenty of AV solutions that will explicitly\r\nlook for the Auto_Open string since attackers have been abusing this in OOXML documents in the wild.\r\nAn example of an OOXML document abusing Excel’s flexible Auto_Open parsing\r\nExcel is surprisingly flexible when it comes to considering a text field matching the Auto_Open label –\r\napparently the application only checks if the label starts with the string Auto_Open. This results in maldocs with\r\nlabels like Auto_Open21. In fact, if you use Excel to save a label with name like Auto_Open222, it will actually\r\nsave the record using a combination of the fBuiltin flag, and then append the extra characters, as can be seen\r\nbelow.\r\nHow Excel saves the label Auto_Open222 – note it maintains the fBuiltin flag (20) and doesn’t\r\ninclude the Auto_Open text, just the 0x01 indicating Auto_Open\r\nAppending characters is great, but can we inject additional characters into the Auto_Open string in a way that\r\nExcel will still read it? A common trick in bypassing input validation is to try injecting null bytes to see if it results\r\nin the string being terminated early. Occasionally null bytes are also good for changing the length of a string\r\nwithout affecting its value.\r\nThe Auto_Open label with null bytes injected\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 5 of 9\n\nHow Excel’s Name Manager renders this Lbl record\r\nExcel will actually give us the best of both worlds, from an attacker perspective, when injecting null bytes. The\r\nAuto_Open functionality will remain intact and still trigger for the cell we specify, but the Name Manager will\r\nnot properly display any part of the name after the first null byte. Additionally, our Lbl record’s name data will not\r\nbe easily match-able with a predictable regex.\r\nThe rabbit hole actually can go deeper than just null byte injection, however – the Name field in Lbl records is\r\nrepresented by a XLUnicodeStringNoCch record. This record allows us to specify strings using either\r\n(essentially) ASCII or UTF16 depending on whether we set the fHighByte flag. Besides further breaking any\r\nsignatures relying on a contiguous Auto_Open string, the usage of UTF16 opens a whole new world of string\r\nabuse to attackers.\r\nUnicode is traditionally a parsing nightmare in the security space due to inconsistent handling of edge cases across\r\nimplementations. Excel is no exception to this, and it appears that when an unexpected character is encountered,\r\nthe label parsing code will simply ignore it. From testing it appears that any “invalid” unicode character found will\r\nbe skipped entirely. There are likely exceptions to the rule, but it appears that any entry that claims to be an invalid\r\ncombination on fileformat.info can be injected into XLUnicodeStringNoCch records without impacting parsing.\r\nFor example, if we build a string like\r\n\"\\ufefeA\\uffffu\\ufefft\\ufffeo\\uffef_\\ufff0O\\ufff1p\\ufff6e\\ufefdn\\udddd\" , this will still trigger the Excel\r\nAuto_Open functionality.\r\nAfter some fun with Unicode this looks VERY different from our initial Lbl record\r\nThis could be combined with null byte injection to hide the manipulation from the Name Manager UI entirely, or\r\nthe Lbl record’s fHidden bit could be set to stop it from appearing in the Name Manager entirely. The ability to\r\ninject an arbitrary amount garbage in between letters in the Lbl name significantly increases the difficulty of\r\nbuilding a reliable signature for this technique.\r\nThe Rgce and Ptg Structures\r\nLet’s revisit the YARA rule from earlier, specifically the part for detecting usages of =CHAR(#):\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 6 of 9\n\n$char_func = {06 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 1E 3D 00 41 6F 00}\r\nThis signature is keying on the beginning of a Formula record, and then the CellParsedFormula structure towards\r\nthe end. CellParsedFormula structures contain three things:\r\n1. cce – The size of the following rgce structure\r\n2. rgce – The actual structure containing what we’d consider to contain the formula\r\n3. rgcb – A secondary structure containing supporting information that might be referenced in rgce\r\nSo what on earth is an Rgce structure? Why it’s a set of Ptg structures of course! Ptg structures, short for “Parse\r\nThing”, are the base component of Formulas. While one might expect to find a string representation of a formula\r\nlike =CHAR(61), this wouldn’t mesh with BIFF8’s hyper-focus on reducing file size. Each formula is represented\r\nas a series of Ptg expressions which describes a small piece of what a user would consider to be a formula. For\r\nexample, =CHAR(61) is in fact two components – a reference to the internal CHAR function, and the number 61.\r\nEach of these representations has a corresponding Ptg structure.\r\nThe CHAR function is represented by a PtgFunc, a Ptg record which contains a reference to a predefined list of\r\nfunctions in Excel known as the Ftab.\r\nThe number 61 is represented by a PtgInt structure which is just the standard Ptg header and an integer with the\r\nvalue of 61:\r\nMany Ptg records, like the PtgInt, are fairly straightforward\r\nAs a result, we end up with the binary signature of 1E 3D 00 41 6F 00 ( 41 is the Ptg number for PtgFunc).\r\nOne thing that might stand out here, however, is the fact that the ordering of this data seems backwards – the\r\nPtgInt(61) data is stored before the PtgFunc(CHAR) data.\r\nThis is because Ptg expressions are described using Reverse Polish Notation (RPN). RPN allows for quick parsing\r\nof a series of operators and operands without needing to worry about parentheses, items are processed in the order\r\nthey are read. For example: 3 4 − 5 + represents taking the value 3 and 4, then applying the subtraction\r\nfunction to those values to get -1. The value 5 is taken and the addition function is applied to -1 and 5, resulting in\r\n4. This mentality is useful for stack-based programming languages, and it is used here to simulate what is\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 7 of 9\n\nessentially a stack of Ptg expressions. In our example here, the operand PtgInt(61) is popped off the stack, then\r\nthe PtgFunc(CHAR) is applied to it.\r\nThe reason this is relevant is because the RPN stack-based format of Ptg structures allows us to easily create some\r\nvery obfuscated expressions without needing to worry about their binary representation. For example, Microsoft\r\nDefender blocks all =CHAR(#) expressions – but what if we write a formula like =CHAR(ROUND(61.0,0)).\r\nThis function is essentially the same, but ends up being represented very differently at the byte level:\r\nThe bytes of our new Formula’s rgce\r\nThe rgce listed here is now PtgNum(61.0), PtgInt(0), PtgFunc(ROUND), PtgFunc(CHAR). As an added\r\n“bonus”, PtgNum represents its data as a double, so the value of 61 is represented as 00 00 00 00 00 80 4E 40 .\r\nEmbedding a function has also completely changed the order of our Ptg structures such that the bytes of\r\nPtgFunc(CHAR) and PtgNum(61.0) are no longer adjacent. The original signature of 1E 3D 00 41 6F 00 is no\r\nlonger tracking this Formula.\r\nIn short, the rgce block is ideally designed from a malware author’s perspective. There are numerous ways to\r\nrepresent the exact same functionality that look completely different from a static analysis perspective. The byte\r\nlayout of the rgce block is also highly sensitive to change, turning a single value into a function invocation can\r\nrearrange the order of all other Ptg bytes within the expression.\r\nIntroducing Macrome\r\nMuch of the work necessary for testing some of these methods involved manually writing XLS files rather than\r\nusing Excel. While there are plenty of tools for reading the BIFF8 XLS format, good tooling for manually creating\r\nand modifying XLS files doesn’t appear to be as common. As a result, I’ve created a tool for building and\r\ndeobfuscating BIFF8 XLS Macro documents. This tool, Macrome, uses a modified version of the b2xtranslator\r\nlibrary used by BiffView.\r\nMacrome implements many of the obfuscations described in this blog post to help penetration testers more easily\r\ncreate documents for phishing campaigns. The modified b2xtranslator library can be used for research and\r\nexperimentation with alternate obfuscation methods. Macrome also provides functionality that can be used to\r\nreverse many of these obfuscations in support of malware analysts and defenders. The tool was originally going to\r\ninclude functionality to process macros to help bypass obfuscated formulas, but @DissectMalware has already\r\ncreated a fantastic tool called XLMMacroDeobfuscator which goes above and beyond anything I was planning on\r\ndropping. It’s really a great piece of tech that I’d recommend anyone who has to analyze these kinds of\r\ndocuments.\r\nI’ll be posting in the future about how to further expand Macrome and implement your own obfuscation and\r\ndeobfuscation methods. In the meantime, please give the tool a try at https://github.com/michaelweber/Macrome.\r\nIf you have any suggestions or feature requests please let me know here or open an issue!\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 8 of 9\n\nSource: https://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nhttps://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/\r\nPage 9 of 9",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/"
	],
	"report_names": [
		"evading-av-with-excel-macros-and-biff8-xls"
	],
	"threat_actors": [
		{
			"id": "2864e40a-f233-4618-ac61-b03760a41cbb",
			"created_at": "2023-12-01T02:02:34.272108Z",
			"updated_at": "2026-04-10T02:00:04.97558Z",
			"deleted_at": null,
			"main_name": "WildCard",
			"aliases": [],
			"source_name": "ETDA:WildCard",
			"tools": [
				"RustDown",
				"SysJoker"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "256a6a2d-e8a2-4497-b399-628a7fad4b3e",
			"created_at": "2023-11-30T02:00:07.299845Z",
			"updated_at": "2026-04-10T02:00:03.484788Z",
			"deleted_at": null,
			"main_name": "WildCard",
			"aliases": [],
			"source_name": "MISPGALAXY:WildCard",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434388,
	"ts_updated_at": 1775826761,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/c146846f0f87432527d2d6056547e35eda8c9997.pdf",
		"text": "https://archive.orkl.eu/c146846f0f87432527d2d6056547e35eda8c9997.txt",
		"img": "https://archive.orkl.eu/c146846f0f87432527d2d6056547e35eda8c9997.jpg"
	}
}