include
WorkbookStream.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using b2xtranslator.Spreadsheet.XlsFileFormat;
using b2xtranslator.Spreadsheet.XlsFileFormat.Ptg;
using b2xtranslator.Spreadsheet.XlsFileFormat.Records;
using b2xtranslator.Spreadsheet.XlsFileFormat.Structures;
using b2xtranslator.StructuredStorage.Common;
using b2xtranslator.StructuredStorage.Reader;
using b2xtranslator.xls.XlsFileFormat;
using b2xtranslator.xls.XlsFileFormat.Records;
namespace Macrome
{
public clast WorkbookStream
{
private List _biffRecords;
public List Records
{
get { return _biffRecords; }
}
public WorkbookStream(string filePath)
{
using (var fs = new FileStream(filePath, FileMode.Open))
{
StructuredStorageReader ssr = new StructuredStorageReader(fs);
try
{
var wbStream = ssr.GetStream("Workbook");
byte[] wbBytes = new byte[wbStream.Length];
wbStream.Read(wbBytes, 0, wbBytes.Length, 0);
_biffRecords = RecordHelper.ParseBiffStreamBytes(wbBytes);
}
catch (StreamNotFoundException)
{
var wbStream = ssr.GetStream("Book");
Console.WriteLine("WARNING: Main stream is in a Book record indicating legacy Excel 5 BIFF format. This may not parse correctly.");
byte[] wbBytes = new byte[wbStream.Length];
wbStream.Read(wbBytes, 0, wbBytes.Length, 0);
try
{
_biffRecords = RecordHelper.ParseBiffStreamBytes(wbBytes);
}
catch (Exception)
{
throw new NotImplementedException("Error parsing Book stream: Macrome currently doesn't support the Excel 5 BIFF format.");
}
}
}
}
public WorkbookStream(List records)
{
_biffRecords = records;
}
public WorkbookStream(byte[] workbookBytes)
{
_biffRecords = RecordHelper.ParseBiffStreamBytes(workbookBytes);
}
public WorkbookStream RemoveRecord(BiffRecord recordToRemove)
{
if (ContainsRecord(recordToRemove) == false)
{
throw new ArgumentException("Could not find recordToRemove");
}
var removeRecordOffset = GetRecordOffset(recordToRemove);
var newRecords = _biffRecords.Take(removeRecordOffset).Concat(
_biffRecords.TakeLast(_biffRecords.Count - removeRecordOffset - 1)).ToList();
return new WorkbookStream(newRecords);
}
public int GetFirstEmptyRowInColumn(int col)
{
List colFormulas = GetAllRecordsByType().Where(f => f.col == col).ToList();
int maxRwVal = 0;
foreach (var colFormula in colFormulas)
{
if (colFormula.rw > maxRwVal) maxRwVal = colFormula.rw;
}
return maxRwVal;
}
public WorkbookStream InsertRecord(BiffRecord recordToInsert, BiffRecord insertAfterRecord = null)
{
return InsertRecords(new List() {recordToInsert}, insertAfterRecord);
}
public WorkbookStream InsertRecords(List recordsToInsert, BiffRecord insertAfterRecord = null)
{
if (insertAfterRecord == null)
{
List recordsWithAppendedRecord = _biffRecords.Concat(recordsToInsert).ToList();
return new WorkbookStream(recordsWithAppendedRecord);
}
if (ContainsRecord(insertAfterRecord) == false)
{
throw new ArgumentException("Could not find insertAfterRecord");
}
var insertRecordOffset = GetRecordOffset(insertAfterRecord) + 1;
//records [r1, TARGET, r2, r3, r4, r5]
//records.count = 6
//insertRecordOffset = 2
//records.Take(2) = [r1, TARGET]
//records.TakeLast(4) = [r2, r3, r4, r5]
//output = [r1, TARGET, INSERT, r2, r3, r4, r5]
var newRecords = _biffRecords.Take(insertRecordOffset).Concat(recordsToInsert)
.Concat(_biffRecords.TakeLast(_biffRecords.Count - insertRecordOffset)).ToList();
return new WorkbookStream(newRecords);
}
public bool ContainsRecord(BiffRecord record)
{
var matchingRecordTypes = _biffRecords.Where(r => r.Id == record.Id).ToList();
return matchingRecordTypes.Any(r => r.Equals(record));
}
public List GetRecordsForBOFRecord(BOF sheetBeginRecord)
{
var sheetRecords = _biffRecords.SkipWhile(r => r.Equals(sheetBeginRecord) == false).ToList();
int sheetSize = sheetRecords.TakeWhile(r => r.Id != RecordType.EOF).Count() + 1;
return sheetRecords.Take(sheetSize).ToList();
}
public WorkbookStream ReplaceRecord(BiffRecord oldRecord, BiffRecord newRecord)
{
if (ContainsRecord(oldRecord) == false)
{
throw new ArgumentException("Could not find oldRecord");
}
//records [r1, OLD, r2, r3, r4, r5]
//records.count = 6
//replaceRecordOffset = 1
//records.Take(1) = [r1]
//records.TakeLast(4) = [r2, r3, r4, r5]
//output = [r1, NEW, r2, r3, r4, r5]
var replaceRecordOffset = GetRecordOffset(oldRecord);
var newRecords = _biffRecords.Take(replaceRecordOffset).Append(newRecord)
.Concat(_biffRecords.TakeLast(_biffRecords.Count - (replaceRecordOffset + 1))).ToList();
return new WorkbookStream(newRecords);
}
public int GetLabelOffset(string labelName)
{
List labels = GetAllRecordsByType();
int offset = 1;
foreach (var label in labels)
{
if (label.Name.Equals(labelName)) return offset;
offset += 1;
}
throw new ArgumentException(string.Format("Cannot find Lbl record with name {0}", labelName));
}
public WorkbookStream AddSheet(BoundSheet8 sheetHeader, byte[] sheetBytes)
{
WorkbookStream newStream = new WorkbookStream(Records);
List existingBoundSheets = newStream.GetAllRecordsByType();
BoundSheet8 lastSheet8 = existingBoundSheets.Last();
newStream = newStream.InsertRecord(sheetHeader, lastSheet8);
List sheetRecords = RecordHelper.ParseBiffStreamBytes(sheetBytes);
newStream = newStream.InsertRecords(sheetRecords);
newStream = newStream.FixBoundSheetOffsets();
return newStream;
}
public WorkbookStream AddSheet(BoundSheet8 sheetHeader, List records)
{
return AddSheet(sheetHeader, RecordHelper.ConvertBiffRecordsToBytes(records));
}
///
/// Needs to be called any time that we add a record that changes the start
/// offset of worksheet streams.
///
///
public WorkbookStream FixBoundSheetOffsets()
{
List oldSheetBoundRecords = GetAllRecordsByType();
//We ignore the first BOF record for the global/workbook stream
List bofRecords = GetAllRecordsByType().Skip(1).ToList();
WorkbookStream newStream = new WorkbookStream(Records);
int sheetOffset = 0;
//astign each offset in order of definition (as per specification)
foreach (var boundSheet in oldSheetBoundRecords)
{
long offset = newStream.GetRecordByteOffset(bofRecords[sheetOffset]);
BoundSheet8 newBoundSheet8 = ((BiffRecord) boundSheet.Clone()).AsRecordType();
newBoundSheet8.lbPlyPos = (uint)offset;
newStream = newStream.ReplaceRecord(boundSheet, newBoundSheet8);
sheetOffset += 1;
}
return newStream;
}
private int GetRecordOffset(BiffRecord record)
{
if (ContainsRecord(record) == false)
{
throw new ArgumentException(string.Format("Could not find record {0}", record));
}
var recordOffset =
_biffRecords.TakeWhile(r => r.Equals(record) == false).Count();
return recordOffset;
}
public long GetRecordByteOffset(BiffRecord record)
{
int listOffset = GetRecordOffset(record);
//Size of BiffRecord is 4 (header) + Length
return _biffRecords.Take(listOffset).Sum(r => r.Length + 4);
}
public List GetAllRecordsByType() where T : BiffRecord
{
RecordType rt;
if (RecordType.TryParse(typeof(T).Name, out rt))
{
return GetAllRecordsByType(rt).Select(r => (T) r.AsRecordType()).ToList();
}
//Special edge case for the String BIFF record since it overlaps with the c# string keyword
else if (typeof(T).Name.Equals("STRING"))
{
rt = RecordType.String;
return GetAllRecordsByType(rt).Select(r => (T)r.AsRecordType()).ToList();
}
throw new ArgumentException(string.Format("Could not identify matching RecordType for clast {0}",
typeof(T).Name));
}
public List GetAllRecordsByType(RecordType type)
{
return _biffRecords.Where(r => r.Id == type).Select(r => (BiffRecord)r.Clone()).ToList();
}
public List GetAutoOpenLabels()
{
List labels = GetAllRecordsByType();
List autoOpenLabels = new List();
foreach (var label in labels)
{
if (label.IsAutoOpenLabel())
{
autoOpenLabels.Add(label);
}
}
return autoOpenLabels;
}
public List GetMacroSheetBOFs()
{
List sheets = GetAllRecordsByType().ToList();
//Each BoundSheet is mapped to the 1+Nth BOF record (BoundSheet 1 is the 2nd record, etc.)
List bofs = GetAllRecordsByType();
List macroSheetBofs = new List();
int sheetOffset = 1;
foreach (var sheet in sheets)
{
if (sheet.dt == BoundSheet8.SheetType.Macrosheet)
{
macroSheetBofs.Add(bofs[sheetOffset]);
}
sheetOffset += 1;
}
return macroSheetBofs;
}
///
/// We use a few tricks here to obfuscate the Auto_Open Lbl BIFF records.
/// 1) By default the Lbl Auto_Open record is marked as fBuiltin = true with a single byte 0x01 to represent AUTO_OPEN
/// We avoid this easily sig-able series of bytes by using a string instead - which Excel will also process.
/// We can use labels like AuTo_OpEn and Excel will still use it - some astyst tools are case sensitive and don't
/// detect this.
/// 2) The string we use for the Lbl can be Unicode, which will further break signatures expecting an ASCII Auto_Open string
/// 3) We can inject null bytes into the label name and Excel will ignore them when hunting for Auto_Open labels.
/// The name manager will only display up to the first null byte - and most excel label parsers will also break on this.
/// 4) The Unicode BOM character (0xFEFF/0xFFEF) is also disregarded by Excel. We can use this to break detections that will drop
/// nulls and look for Auto_Open without being case sensitive. By injecting this with nulls we break most detection.
///
///
public WorkbookStream ObfuscateAutoOpen(string localizedLabel = "Auto_Open")
{
Random randomUnicodeChar = new Random();
string[] badUnicodeChars = { "\ufefe", "\uffff", "\ufeff", "\ufffe", "\uffef", "\ufff0", "\ufff1", "\ufff6", "\ufefd", "\u0000", "\udddd" };
int indexLabel = 0;
string unicodeLabelWithBadChars = "";
List labels = GetAllRecordsByType();
Lbl autoOpenLbl = labels.First(l => l.fBuiltin && l.Name.Value.Equals("\u0001") ||
l.Name.Value.ToLower().StartsWith(localizedLabel.ToLower()));
Lbl replaceLabelStringLbl = ((BiffRecord)autoOpenLbl.Clone()).AsRecordType();
//Characters that work
//fefe, ffff, feff, fffe, ffef, fff0, fff1, fff6, fefd, 0000, dddd
//Pretty much any character that is invalid unicode - though \ucccc doesn't seem to work - need better criteria for parsing
foreach (char localizedLabelChar in localizedLabel)
{
indexLabel = randomUnicodeChar.Next(localizedLabel.Length);
for (var i = 0; i < 10; i += 1)
{
unicodeLabelWithBadChars += badUnicodeChars[indexLabel];
}
unicodeLabelWithBadChars += localizedLabelChar;
}
replaceLabelStringLbl.SetName(new XLUnicodeStringNoCch(unicodeLabelWithBadChars, true));
replaceLabelStringLbl.fBuiltin = false;
// Hidden removes from the label manager entirely, but doesn't seem to work if fBuiltin is false
// replaceLabelStringLbl.fHidden = true;
WorkbookStream obfuscatedStream = ReplaceRecord(autoOpenLbl, replaceLabelStringLbl);
obfuscatedStream = obfuscatedStream.FixBoundSheetOffsets();
return obfuscatedStream;
}
///
/// Check for the existence of a FilePast BIFF record indicating RC4 or XOR Obfuscation encryption
///
/// true if a FilePast record can be found
public bool HasPastwordToOpen()
{
bool hasPastwordToOpen = GetAllRecordsByType().Count > 0;
return hasPastwordToOpen;
}
public byte[] ToBytes()
{
return RecordHelper.ConvertBiffRecordsToBytes(_biffRecords);
}
public string ToDisplayString()
{
return string.Join("\n",_biffRecords.Select(record => record.ToHexDumpString()));
}
}
}