forked from michaelweber/Macrome
-
Notifications
You must be signed in to change notification settings - Fork 0
/
WorkbookStream.cs
370 lines (298 loc) · 14.2 KB
/
WorkbookStream.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
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 class WorkbookStream
{
private List<BiffRecord> _biffRecords;
public List<BiffRecord> 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<BiffRecord> 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<Formula> colFormulas = GetAllRecordsByType<Formula>().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<BiffRecord>() {recordToInsert}, insertAfterRecord);
}
public WorkbookStream InsertRecords(List<BiffRecord> recordsToInsert, BiffRecord insertAfterRecord = null)
{
if (insertAfterRecord == null)
{
List<BiffRecord> 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<BiffRecord> 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<Lbl> labels = GetAllRecordsByType<Lbl>();
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<BoundSheet8> existingBoundSheets = newStream.GetAllRecordsByType<BoundSheet8>();
BoundSheet8 lastSheet8 = existingBoundSheets.Last();
newStream = newStream.InsertRecord(sheetHeader, lastSheet8);
List<BiffRecord> sheetRecords = RecordHelper.ParseBiffStreamBytes(sheetBytes);
newStream = newStream.InsertRecords(sheetRecords);
newStream = newStream.FixBoundSheetOffsets();
return newStream;
}
public WorkbookStream AddSheet(BoundSheet8 sheetHeader, List<BiffRecord> records)
{
return AddSheet(sheetHeader, RecordHelper.ConvertBiffRecordsToBytes(records));
}
/// <summary>
/// Needs to be called any time that we add a record that changes the start
/// offset of worksheet streams.
/// </summary>
/// <returns></returns>
public WorkbookStream FixBoundSheetOffsets()
{
List<BoundSheet8> oldSheetBoundRecords = GetAllRecordsByType<BoundSheet8>();
//We ignore the first BOF record for the global/workbook stream
List<BOF> bofRecords = GetAllRecordsByType<BOF>().Skip(1).ToList();
WorkbookStream newStream = new WorkbookStream(Records);
int sheetOffset = 0;
//Assign 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<BoundSheet8>();
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<T> GetAllRecordsByType<T>() where T : BiffRecord
{
RecordType rt;
if (RecordType.TryParse(typeof(T).Name, out rt))
{
return GetAllRecordsByType(rt).Select(r => (T) r.AsRecordType<T>()).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<T>()).ToList();
}
throw new ArgumentException(string.Format("Could not identify matching RecordType for class {0}",
typeof(T).Name));
}
public List<BiffRecord> GetAllRecordsByType(RecordType type)
{
return _biffRecords.Where(r => r.Id == type).Select(r => (BiffRecord)r.Clone()).ToList();
}
public List<Lbl> GetAutoOpenLabels()
{
List<Lbl> labels = GetAllRecordsByType<Lbl>();
List<Lbl> autoOpenLabels = new List<Lbl>();
foreach (var label in labels)
{
if (label.IsAutoOpenLabel())
{
autoOpenLabels.Add(label);
}
}
return autoOpenLabels;
}
public List<BOF> GetMacroSheetBOFs()
{
List<BoundSheet8> sheets = GetAllRecordsByType<BoundSheet8>().ToList();
//Each BoundSheet is mapped to the 1+Nth BOF record (BoundSheet 1 is the 2nd record, etc.)
List<BOF> bofs = GetAllRecordsByType<BOF>();
List<BOF> macroSheetBofs = new List<BOF>();
int sheetOffset = 1;
foreach (var sheet in sheets)
{
if (sheet.dt == BoundSheet8.SheetType.Macrosheet)
{
macroSheetBofs.Add(bofs[sheetOffset]);
}
sheetOffset += 1;
}
return macroSheetBofs;
}
/// <summary>
/// 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 analyst 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.
/// </summary>
/// <returns></returns>
public WorkbookStream ObfuscateAutoOpen()
{
List<Lbl> labels = GetAllRecordsByType<Lbl>();
Lbl autoOpenLbl = labels.First(l => l.fBuiltin && l.Name.Value.Equals("\u0001") ||
l.Name.Value.ToLower().StartsWith("auto_open"));
Lbl replaceLabelStringLbl = ((BiffRecord)autoOpenLbl.Clone()).AsRecordType<Lbl>();
//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
//TODO [Stealth] Randomize which invalid unicode characters are injected into this string
replaceLabelStringLbl.SetName(new XLUnicodeStringNoCch("\u0000A\uffffu\u0000\ufefft\ufffeo\uffef_\ufff0O\ufff1p\ufff6e\ufefdn\udddd", 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;
}
/// <summary>
/// Check for the existence of a FilePass BIFF record indicating RC4 or XOR Obfuscation encryption
/// </summary>
/// <returns>true if a FilePass record can be found</returns>
public bool HasPasswordToOpen()
{
bool hasPasswordToOpen = GetAllRecordsByType<FilePass>().Count > 0;
return hasPasswordToOpen;
}
public byte[] ToBytes()
{
return RecordHelper.ConvertBiffRecordsToBytes(_biffRecords);
}
public string ToDisplayString()
{
return string.Join("\n",_biffRecords.Select(record => record.ToHexDumpString()));
}
}
}