-
Notifications
You must be signed in to change notification settings - Fork 7
/
AutomaticallyCreateFunctionIdDatabases.java
606 lines (527 loc) · 20.5 KB
/
AutomaticallyCreateFunctionIdDatabases.java
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
//A script to headlessly create FIDB files based on all programs within the given project. Based on "CreateMultipleLibraries.java" by the Ghidra team, inspired by Threatrack's work (https://blog.threatrack.de/2019/09/20/ghidra-fid-generator/)
//@author Max 'Libra' Kersten for Trellix
//@category FunctionID
//@keybinding
//@menupath
//@toolbar
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import ghidra.app.script.GhidraScript;
import ghidra.feature.fid.db.FidDB;
import ghidra.feature.fid.db.FidFile;
import ghidra.feature.fid.db.FidFileManager;
import ghidra.feature.fid.db.LibraryRecord;
import ghidra.feature.fid.service.FidPopulateResult;
import ghidra.feature.fid.service.FidPopulateResult.Disposition;
import ghidra.feature.fid.service.FidPopulateResultReporter;
import ghidra.feature.fid.service.FidService;
import ghidra.feature.fid.service.Location;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.lang.LanguageID;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
public class AutomaticallyCreateFunctionIdDatabases extends GhidraScript {
/**
* The Function ID service to be used
*/
private FidService fidService;
/**
* The path to the log file where all logs are written (appended) to
*/
private Path logFile;
/**
* Logs a message to Ghidra's console and to the log file in the specified
* output folder
*
* @param message the message to print and write
*/
private void log(String message, boolean isError) {
/*
* Write the (error) message to Ghidra's console, depending on the given boolean
*/
if (isError) {
printerr(message);
} else {
println(message);
}
/*
* Try to append data to the log file
*/
try {
// Append the error tag if the message is an error
if (isError) {
message = "ERROR: " + message;
}
// Append a newline to the message
message += "\n";
// Write the data to the log file
Files.write(logFile, message.getBytes(), StandardOpenOption.APPEND);
} catch (IOException e) {
// Ignore exceptions, the console log already contains the logged information
}
}
/**
* The reporter class, which implements the required Ghidra report class
*/
class FidReporter implements FidPopulateResultReporter {
@Override
public void report(FidPopulateResult result) {
// Check if a result is given, if not, simply return
if (result == null) {
return;
}
// Get the library record from the result
LibraryRecord libraryRecord = result.getLibraryRecord();
// Get the library family name, version, and variant
String libraryFamilyName = libraryRecord.getLibraryFamilyName();
String libraryVersion = libraryRecord.getLibraryVersion();
String libraryVariant = libraryRecord.getLibraryVariant();
// Log the obtained information
log(libraryFamilyName + ':' + libraryVersion + ':' + libraryVariant, false);
// Log the attempted, added, and excluded count for the generated FIDB
log(result.getTotalAttempted() + " total functions visited", false);
log(result.getTotalAdded() + " total functions added", false);
log(result.getTotalExcluded() + " total functions excluded", false);
// Log all exclusions
log("Breakdown of exclusions:", false);
for (Entry<Disposition, Integer> entry : result.getFailures().entrySet()) {
if (entry.getKey() != Disposition.INCLUDED) {
log(" " + entry.getKey() + ": " + entry.getValue(), false);
}
}
// Log all unresolved symbols
log("List of unresolved symbols:", false);
TreeSet<String> symbols = new TreeSet<>();
for (Location location : result.getUnresolvedSymbols()) {
symbols.add(location.getFunctionName());
}
for (String symbol : symbols) {
log(" " + symbol, false);
}
}
}
/**
* Parses the symbols within the given file, reading it line-by-line where each
* line is assumed to be a symbol
*
* @param commonSymbolsFile the file to read
* @return a list of symbols to exclude when making FunctionID signatures
* @throws IOException
* @throws CancelledException
*/
private List<String> parseSymbols(File commonSymbolsFile) throws IOException, CancelledException {
// Check if the common symbols file exists
if (commonSymbolsFile.exists() == false) {
return null;
}
// If it exists, read the file
BufferedReader reader = new BufferedReader(new FileReader(commonSymbolsFile));
// Create a new linked list to store the common symbols in
List<String> commonSymbols = new LinkedList<>();
// Read a single line from the file
String line = reader.readLine();
// Iterate over all lines
while (line != null) {
/*
* If the monitor isn't cancelled, continue, else throw an exception which
* causes the whole script to halt
*/
monitor.checkCancelled();
// If the line of the length is not zero
if (line.length() != 0) {
// Add it to the list
commonSymbols.add(line);
}
// Read the next line
line = reader.readLine();
}
// Once the entire file has been read, close the file access
reader.close();
// Return all common symbols
return commonSymbols;
}
/**
* Recursively generate a mapping for all programs within the given folder and
* its sub-folders
*
* @param folder the folder to start in
* @param languageIdMapping the LanguageID mapping which is populated within
* each call
* @param rawLanguageIds the raw LanguageIDs
* @param languageIds the LanguageID
* @param compilerSpecMapping the compiler spec mapping
* @throws CancelledException
*/
private void generateLanguageIdProgramMapping(Map<String, List<DomainFile>> languageIdMapping, DomainFolder folder,
Set<String> rawLanguageIds, Set<String> languageIds, Map<String, List<DomainFile>> compilerSpecMapping)
throws CancelledException {
// Iterate over all domain files within the given folder
for (DomainFile domainFile : folder.getFiles()) {
/*
* As within any longer loop, a cancel check is present to allow a user to
* cancel the script in a timely manner
*/
monitor.checkCancelled();
/*
* Only if the file is of the "PROGRAM" type, it is to be used
*/
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
Map<String, String> metadata = domainFile.getMetadata();
String languageId = metadata.get("Language ID");
if (languageId != null) {
rawLanguageIds.add(languageId);
/*
* An example of a raw language ID is "x86:LE:32:default (3.0)". Splitting by
* the space removes the version number, giving the language ID that is required
* for later use.
*/
String filteredLanguageId = languageId.split(" ")[0];
languageIds.add(filteredLanguageId);
/*
* Attempt to get the value from the mapping for the given key. If the result is
* null, no such key (nor value) is present in the mapping.
*/
List<DomainFile> domainFiles = languageIdMapping.get(filteredLanguageId);
/*
* If the result is null, there is no key in the mapping, meaning it is new. A
* new list is created, and the key and value are added to the mapping.
*/
if (domainFiles == null) {
domainFiles = new ArrayList<>();
domainFiles.add(domainFile);
languageIdMapping.put(filteredLanguageId, domainFiles);
} else {
/*
* Since the list from the mapping is not null, the key exists with a value. Add
* the current file to the list, and overwrite the mapping's entry with the new
* value for the given key.
*/
domainFiles.add(domainFile);
languageIdMapping.put(filteredLanguageId, domainFiles);
}
}
}
}
// Iterate over all folders within the given folder
for (DomainFolder domainFolder : folder.getFolders()) {
monitor.checkCancelled();
generateLanguageIdProgramMapping(languageIdMapping, domainFolder, rawLanguageIds, languageIds,
compilerSpecMapping);
}
}
/**
* Populates an FIDB file based on the given list of programs for the given
* language ID, excluding the given common symbols
*
* @param fidDb the database to populate
* @param languageID the LanguageID to match with
* @param programs the programs to parse
* @param commonSymbols the common symbols to exclude
* @param reporter the report to use when returning results
* @param projectName the project name to use within the database
* @param projectVersion the version to use within the database
* @throws CancelledException
*/
private void createLibraryFromPrograms(FidDB fidDb, LanguageID languageID, List<DomainFile> programs,
List<String> commonSymbols, FidReporter reporter, String projectName, String projectVersion)
throws CancelledException {
/*
* Set the library name, version, and variant
*/
String libraryName = projectName;
String libraryVersion = projectVersion;
String libraryVariant = languageID.getIdAsString();
// Create the message
String message = "[" + libraryVariant + "] Generating \"" + libraryName + " (" + libraryVersion
+ ")\" based on " + programs.size() + " files";
// Log the message
log(message, false);
// Set the monitor message to inform the analyst when running via a GUI
monitor.setMessage(message);
try {
// Call the FID service to create populate the new library
FidPopulateResult result = fidService.createNewLibraryFromPrograms(fidDb, libraryName, libraryVersion,
libraryVariant, programs, null, languageID, null, commonSymbols, TaskMonitor.DUMMY);
// report the results via the reporter
reporter.report(result);
} catch (MemoryAccessException e) {
Msg.showError(this, null, "Unexpected memory access exception", "Please notify the Ghidra team:", e);
} catch (VersionException e) {
Msg.showError(this, null, "Version Exception",
"One of the programs in your domain folder cannot be upgraded: " + e.getMessage());
} catch (IllegalStateException e) {
Msg.showError(this, null, "Illegal State Exception", "Unknown error: " + e.getMessage());
} catch (IOException e) {
Msg.showError(this, null, "FidDb IOException", "Please notify the Ghidra team:", e);
}
}
/**
* Creates a new FunctionID database file
*
* @param fidbFile the file to be created
* @param fidFileName the file name to use
* @return the newly created FIDB file with write permissions
* @throws IOException
* @throws VersionException
*/
private FidDB createFidDB(File fidbFile, String fidFileName) throws IOException, VersionException {
// Declare the object
FidFile fidFile = null;
// Get the FID file manager
FidFileManager fidFileManager = FidFileManager.getInstance();
// Create a new database
fidFileManager.createNewFidDatabase(fidbFile);
// Get all user added FIDB files
List<FidFile> fids = fidFileManager.getUserAddedFiles();
// Iterate over all FIDB files
for (FidFile fid : fids) {
// If the name equals the given file name
if (fid.getName().equalsIgnoreCase(fidFileName)) {
// If the match is there, set the return variable and break the loop
fidFile = fid;
break;
}
}
// If no matching file was found, something went wrong and null is returned
if (fidFile == null) {
return null;
}
// Return the FIDB file with write permissions
return fidFile.getFidDB(true);
}
@Override
protected void run() throws Exception {
/*
* A note about the askSomething functions. This script is intended to be
* executed by Ghidra in its headless mode, since the GUI serves no purpose in
* this script. Any askSomething function will take an argument from the
* command-line interface when running headless. As such, it allows one to use
* the command-line interface in an easy manner, or with the help of a
* properties file. In this case, all askSomething functions are listed in the
* start of the run-method, since the command-line interface argument order
* matters. This is a convenient way for users to understand what arguments are
* to be provided when running this script headless, without going over all code
* within this script.
*/
/*
* Ask for the project name, which is to be used in the to-be created FIDBs
*/
String projectName = askString("Project name", "What is the project name?");
/*
* Ask for the project version, which is to be used in the to-be created FIDBs
*/
String projectVersion = askString("Project version", "What is the project version?");
/*
* Ask for the output folder. This will be the location where the FIDB files
* along with the log file will be written to.
*/
String outputFolder = askString("Desired output folder", "What is the desired output folder?");
// Create a file object for the output folder
File outputFolderFile = new File(outputFolder);
// If the output folder does not exist, create all required folders
if (outputFolderFile.exists() == false) {
outputFolderFile.mkdirs();
}
/*
* Get the absolute path for the output folder, excluding a trailing folder
* separator
*/
outputFolder = outputFolderFile.getAbsolutePath();
/*
* Ask if a common symbols file is to be used
*/
boolean useCommonSymbolsFile = askYesNo("Common symbols file usage",
"Do you want to use a common symbols file?");
/*
* The path to the symbols file, if desired
*/
String symbolsFilePath = null;
/*
* The file object which refers to the provided path
*/
File commonSymbolsFile = null;
/*
* The list of symbols
*/
List<String> symbols = null;
/*
* If a common symbols file is desired, the path to said file is requested,
* after which a file object pointing to said location is instantiated. Lastly,
* the symbols are parsed, and stored in the symbols list. If the common symbols
* file cannot be found, the script returns early with a related error message
*/
if (useCommonSymbolsFile) {
symbolsFilePath = askString("Common symbols file location",
"What is the location of the common symbols file?");
log("Received the following location for the common symbols file: " + symbolsFilePath, false);
commonSymbolsFile = new File(symbolsFilePath);
if (commonSymbolsFile.exists() == false) {
log("Cannot find the common symbols file:" + symbolsFilePath, true);
return;
}
symbols = parseSymbols(commonSymbolsFile);
}
/*
* Initialise the FunctionID service
*/
fidService = new FidService();
/*
* Initialises the log file object
*/
File log = new File(outputFolder + File.separator + "log.txt");
/*
* Check if the log file exists. If it does, no action is required, as data can
* be appended to it. If it does not, the file itself is created
*/
if (log.exists() == false) {
log.createNewFile();
}
/*
* Set the log file, which is used by the log function
*/
logFile = log.toPath();
/*
* Logging is possible from this point
*/
log("Received project name: " + projectName, false);
log("Received project version: " + projectVersion, false);
log("Received output folder path: " + outputFolder, false);
log("Received choice with regards to the usage of a common symbols file: " + useCommonSymbolsFile, false);
/*
* Initialise a new report instance, to later report on the FIDB generation
* results
*/
FidReporter reporter = new FidReporter();
/*
* Gets the root folder of the current project, allowing the recursive calls
* later on to start at the root node
*/
DomainFolder rootFolder = state.getProject().getProjectData().getRootFolder();
/*
* Contains a mapping of the original compiler spec IDs for the given list of
* files
*/
Map<String, List<DomainFile>> compilerSpecMapping = new HashMap<>();
/*
* Contains all language IDs without version information
*/
Set<String> languageIds = new HashSet<>();
/*
* Contains all language IDs with version information
*/
Set<String> rawLanguageIds = new HashSet<>();
/*
* Initialises the mapping of language IDs and their respective domain files
*/
Map<String, List<DomainFile>> languageIdProgramMapping = new HashMap<>();
/*
* Generate a mapping, stored in a global variable, which contains unique
* language ID strings as a key, and a list of programs associated with said
* language ID as value. This mapping is used later on to make FIDBs for each
* language ID, with the respective files.
*/
generateLanguageIdProgramMapping(languageIdProgramMapping, rootFolder, rawLanguageIds, languageIds,
compilerSpecMapping);
/*
* Create a log message with all unique language IDs, including their version
*/
String collectLanguageIds = "Collected language IDs and their versions:\n";
for (String id : rawLanguageIds) {
collectLanguageIds += "\t" + id + "\n";
}
/*
* Write the message to Ghidra's console and the log file
*/
log(collectLanguageIds, false);
/*
* Create a string which is to be logged, which contains each unique language ID
* and the number of files that are to be processed for said language ID
*/
String languageIdFileMessage = "";
for (Map.Entry<String, List<DomainFile>> entry : languageIdProgramMapping.entrySet()) {
String languageID = entry.getKey();
List<DomainFile> domainFiles = entry.getValue();
languageIdFileMessage += languageID + " contains " + domainFiles.size() + " files\n";
}
// Log the language ID and file summary
log(languageIdFileMessage, false);
/*
* Iterate over the language IDs without version information
*/
for (String id : languageIds) {
/*
* Replace the colon with a dot to avoid invalid file name errors. The replace
* all function uses regular expressions for both arguments, meaning that the
* dot has to be escaped
*/
String fileName = id.replaceAll(":", "\\.") + ".fidb";
/*
* The FIDB file name is the file name that the file will have on disk, based on
* the project name, project version, and the language ID
*/
String fidFileName = projectName.replaceAll(" ", "\\.") + "_" + projectVersion.replaceAll(" ", "\\.") + "_"
+ fileName;
// Create a file object for the FIDB file
File fidbFile = new File(outputFolder + File.separator + fidFileName);
/*
* The createNewFidDatabase function requires the file to NOT exist, meaning it
* needs to be deleted if it exists
*/
if (fidbFile.exists()) {
String error = "The FIDB file already exists: " + fidbFile.getAbsolutePath();
log(error, true);
return;
}
/*
* Create the FIDB file
*/
FidDB fidDb = createFidDB(fidbFile, fidFileName);
/*
* If createFidDB returns null, the file could not be found, and the execution
* needs to exit
*/
if (fidDb == null) {
log("Cannot find any FIDB file named \"" + fidFileName + "\"", true);
return;
}
/*
* Create a language ID object, which is used and required during the generation
* of the FIDB
*/
LanguageID languageID = new LanguageID(id);
/*
* A list of all programs which fall under the given language ID, within the
* given Ghidra project
*/
List<DomainFile> programs = languageIdProgramMapping.get(id);
/*
* Generate the hashes for functions for each program in the list, for the given
* language ID
*/
createLibraryFromPrograms(fidDb, languageID, programs, symbols, reporter, projectName, projectVersion);
// Save the database, otherwise all changes will be lost
fidDb.saveDatabase("Saving", monitor);
// Close the FIDB
fidDb.close();
}
}
}