1 | //===-- llvm-cfi-verify.cpp - CFI Verification tool for LLVM --------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | // |
9 | // This tool verifies Control Flow Integrity (CFI) instrumentation by static |
10 | // binary analysis. See the design document in /docs/CFIVerify.rst for more |
11 | // information. |
12 | // |
13 | // This tool is currently incomplete. It currently only does disassembly for |
14 | // object files, and searches through the code for indirect control flow |
15 | // instructions, printing them once found. |
16 | // |
17 | //===----------------------------------------------------------------------===// |
18 | |
19 | #include "lib/FileAnalysis.h" |
20 | #include "lib/GraphBuilder.h" |
21 | |
22 | #include "llvm/BinaryFormat/ELF.h" |
23 | #include "llvm/DebugInfo/Symbolize/SymbolizableModule.h" |
24 | #include "llvm/Support/CommandLine.h" |
25 | #include "llvm/Support/Error.h" |
26 | #include "llvm/Support/FormatVariadic.h" |
27 | #include "llvm/Support/SpecialCaseList.h" |
28 | #include "llvm/Support/VirtualFileSystem.h" |
29 | |
30 | #include <cstdlib> |
31 | |
32 | using namespace llvm; |
33 | using namespace llvm::object; |
34 | using namespace llvm::cfi_verify; |
35 | |
36 | static cl::OptionCategory CFIVerifyCategory("CFI Verify Options" ); |
37 | |
38 | cl::opt<std::string> InputFilename(cl::Positional, cl::desc("<input file>" ), |
39 | cl::Required, cl::cat(CFIVerifyCategory)); |
40 | cl::opt<std::string> IgnorelistFilename(cl::Positional, |
41 | cl::desc("[ignorelist file]" ), |
42 | cl::init(Val: "-" ), |
43 | cl::cat(CFIVerifyCategory)); |
44 | cl::opt<bool> PrintGraphs( |
45 | "print-graphs" , |
46 | cl::desc("Print graphs around indirect CF instructions in DOT format." ), |
47 | cl::init(Val: false), cl::cat(CFIVerifyCategory)); |
48 | cl::opt<unsigned> PrintBlameContext( |
49 | "blame-context" , |
50 | cl::desc("Print the blame context (if possible) for BAD instructions. This " |
51 | "specifies the number of lines of context to include, where zero " |
52 | "disables this feature." ), |
53 | cl::init(Val: 0), cl::cat(CFIVerifyCategory)); |
54 | cl::opt<unsigned> PrintBlameContextAll( |
55 | "blame-context-all" , |
56 | cl::desc("Prints the blame context (if possible) for ALL instructions. " |
57 | "This specifies the number of lines of context for non-BAD " |
58 | "instructions (see --blame-context). If --blame-context is " |
59 | "unspecified, it prints this number of contextual lines for BAD " |
60 | "instructions as well." ), |
61 | cl::init(Val: 0), cl::cat(CFIVerifyCategory)); |
62 | cl::opt<bool> Summarize("summarize" , cl::desc("Print the summary only." ), |
63 | cl::init(Val: false), cl::cat(CFIVerifyCategory)); |
64 | |
65 | ExitOnError ExitOnErr; |
66 | |
67 | static void printBlameContext(const DILineInfo &LineInfo, unsigned Context) { |
68 | auto FileOrErr = MemoryBuffer::getFile(Filename: LineInfo.FileName); |
69 | if (!FileOrErr) { |
70 | errs() << "Could not open file: " << LineInfo.FileName << "\n" ; |
71 | return; |
72 | } |
73 | |
74 | std::unique_ptr<MemoryBuffer> File = std::move(FileOrErr.get()); |
75 | SmallVector<StringRef, 100> Lines; |
76 | File->getBuffer().split(A&: Lines, Separator: '\n'); |
77 | |
78 | for (unsigned i = std::max<size_t>(a: 1, b: LineInfo.Line - Context); |
79 | i < |
80 | std::min<size_t>(a: Lines.size() + 1, b: LineInfo.Line + Context + 1); |
81 | ++i) { |
82 | if (i == LineInfo.Line) |
83 | outs() << ">" ; |
84 | else |
85 | outs() << " " ; |
86 | |
87 | outs() << i << ": " << Lines[i - 1] << "\n" ; |
88 | } |
89 | } |
90 | |
91 | static void printInstructionInformation(const FileAnalysis &Analysis, |
92 | const Instr &InstrMeta, |
93 | const GraphResult &Graph, |
94 | CFIProtectionStatus ProtectionStatus) { |
95 | outs() << "Instruction: " << format_hex(N: InstrMeta.VMAddress, Width: 2) << " (" |
96 | << stringCFIProtectionStatus(Status: ProtectionStatus) << "): " ; |
97 | Analysis.printInstruction(InstrMeta, OS&: outs()); |
98 | outs() << " \n" ; |
99 | |
100 | if (PrintGraphs) |
101 | Graph.printToDOT(Analysis, OS&: outs()); |
102 | } |
103 | |
104 | static void printInstructionStatus(unsigned BlameLine, bool CFIProtected, |
105 | const DILineInfo &LineInfo) { |
106 | if (BlameLine) { |
107 | outs() << "Ignorelist Match: " << IgnorelistFilename << ":" << BlameLine |
108 | << "\n" ; |
109 | if (CFIProtected) |
110 | outs() << "====> Unexpected Protected\n" ; |
111 | else |
112 | outs() << "====> Expected Unprotected\n" ; |
113 | |
114 | if (PrintBlameContextAll) |
115 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
116 | } else { |
117 | if (CFIProtected) { |
118 | outs() << "====> Expected Protected\n" ; |
119 | if (PrintBlameContextAll) |
120 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
121 | } else { |
122 | outs() << "====> Unexpected Unprotected (BAD)\n" ; |
123 | if (PrintBlameContext) |
124 | printBlameContext(LineInfo, Context: PrintBlameContext); |
125 | } |
126 | } |
127 | } |
128 | |
129 | static void |
130 | printIndirectCFInstructions(FileAnalysis &Analysis, |
131 | const SpecialCaseList *SpecialCaseList) { |
132 | uint64_t ExpectedProtected = 0; |
133 | uint64_t UnexpectedProtected = 0; |
134 | uint64_t ExpectedUnprotected = 0; |
135 | uint64_t UnexpectedUnprotected = 0; |
136 | |
137 | std::map<unsigned, uint64_t> BlameCounter; |
138 | |
139 | for (object::SectionedAddress Address : Analysis.getIndirectInstructions()) { |
140 | const auto &InstrMeta = Analysis.getInstructionOrDie(Address: Address.Address); |
141 | GraphResult Graph = GraphBuilder::buildFlowGraph(Analysis, Address); |
142 | |
143 | CFIProtectionStatus ProtectionStatus = |
144 | Analysis.validateCFIProtection(Graph); |
145 | bool CFIProtected = (ProtectionStatus == CFIProtectionStatus::PROTECTED); |
146 | |
147 | if (!Summarize) { |
148 | outs() << "-----------------------------------------------------\n" ; |
149 | printInstructionInformation(Analysis, InstrMeta, Graph, ProtectionStatus); |
150 | } |
151 | |
152 | if (IgnoreDWARFFlag) { |
153 | if (CFIProtected) |
154 | ExpectedProtected++; |
155 | else |
156 | UnexpectedUnprotected++; |
157 | continue; |
158 | } |
159 | |
160 | auto InliningInfo = Analysis.symbolizeInlinedCode(Address); |
161 | if (!InliningInfo || InliningInfo->getNumberOfFrames() == 0) { |
162 | errs() << "Failed to symbolise " << format_hex(N: Address.Address, Width: 2) |
163 | << " with line tables from " << InputFilename << "\n" ; |
164 | exit(EXIT_FAILURE); |
165 | } |
166 | |
167 | const auto &LineInfo = InliningInfo->getFrame(Index: 0); |
168 | |
169 | // Print the inlining symbolisation of this instruction. |
170 | if (!Summarize) { |
171 | for (uint32_t i = 0; i < InliningInfo->getNumberOfFrames(); ++i) { |
172 | const auto &Line = InliningInfo->getFrame(Index: i); |
173 | outs() << " " << format_hex(N: Address.Address, Width: 2) << " = " |
174 | << Line.FileName << ":" << Line.Line << ":" << Line.Column |
175 | << " (" << Line.FunctionName << ")\n" ; |
176 | } |
177 | } |
178 | |
179 | if (!SpecialCaseList) { |
180 | if (CFIProtected) { |
181 | if (PrintBlameContextAll && !Summarize) |
182 | printBlameContext(LineInfo, Context: PrintBlameContextAll); |
183 | ExpectedProtected++; |
184 | } else { |
185 | if (PrintBlameContext && !Summarize) |
186 | printBlameContext(LineInfo, Context: PrintBlameContext); |
187 | UnexpectedUnprotected++; |
188 | } |
189 | continue; |
190 | } |
191 | |
192 | unsigned BlameLine = 0; |
193 | for (auto &K : {"cfi-icall" , "cfi-vcall" }) { |
194 | if (!BlameLine) |
195 | BlameLine = |
196 | SpecialCaseList->inSectionBlame(Section: K, Prefix: "src" , Query: LineInfo.FileName); |
197 | if (!BlameLine) |
198 | BlameLine = |
199 | SpecialCaseList->inSectionBlame(Section: K, Prefix: "fun" , Query: LineInfo.FunctionName); |
200 | } |
201 | |
202 | if (BlameLine) { |
203 | BlameCounter[BlameLine]++; |
204 | if (CFIProtected) |
205 | UnexpectedProtected++; |
206 | else |
207 | ExpectedUnprotected++; |
208 | } else { |
209 | if (CFIProtected) |
210 | ExpectedProtected++; |
211 | else |
212 | UnexpectedUnprotected++; |
213 | } |
214 | |
215 | if (!Summarize) |
216 | printInstructionStatus(BlameLine, CFIProtected, LineInfo); |
217 | } |
218 | |
219 | uint64_t IndirectCFInstructions = ExpectedProtected + UnexpectedProtected + |
220 | ExpectedUnprotected + UnexpectedUnprotected; |
221 | |
222 | if (IndirectCFInstructions == 0) { |
223 | outs() << "No indirect CF instructions found.\n" ; |
224 | return; |
225 | } |
226 | |
227 | outs() << formatv(Fmt: "\nTotal Indirect CF Instructions: {0}\n" |
228 | "Expected Protected: {1} ({2:P})\n" |
229 | "Unexpected Protected: {3} ({4:P})\n" |
230 | "Expected Unprotected: {5} ({6:P})\n" |
231 | "Unexpected Unprotected (BAD): {7} ({8:P})\n" , |
232 | Vals&: IndirectCFInstructions, Vals&: ExpectedProtected, |
233 | Vals: ((double)ExpectedProtected) / IndirectCFInstructions, |
234 | Vals&: UnexpectedProtected, |
235 | Vals: ((double)UnexpectedProtected) / IndirectCFInstructions, |
236 | Vals&: ExpectedUnprotected, |
237 | Vals: ((double)ExpectedUnprotected) / IndirectCFInstructions, |
238 | Vals&: UnexpectedUnprotected, |
239 | Vals: ((double)UnexpectedUnprotected) / IndirectCFInstructions); |
240 | |
241 | if (!SpecialCaseList) |
242 | return; |
243 | |
244 | outs() << "\nIgnorelist Results:\n" ; |
245 | for (const auto &KV : BlameCounter) { |
246 | outs() << " " << IgnorelistFilename << ":" << KV.first << " affects " |
247 | << KV.second << " indirect CF instructions.\n" ; |
248 | } |
249 | } |
250 | |
251 | int main(int argc, char **argv) { |
252 | cl::HideUnrelatedOptions(Categories: {&CFIVerifyCategory, &getColorCategory()}); |
253 | cl::ParseCommandLineOptions( |
254 | argc, argv, |
255 | Overview: "Identifies whether Control Flow Integrity protects all indirect control " |
256 | "flow instructions in the provided object file, DSO or binary.\nNote: " |
257 | "Anything statically linked into the provided file *must* be compiled " |
258 | "with '-g'. This can be relaxed through the '--ignore-dwarf' flag." ); |
259 | |
260 | InitializeAllTargetInfos(); |
261 | InitializeAllTargetMCs(); |
262 | InitializeAllAsmParsers(); |
263 | InitializeAllDisassemblers(); |
264 | |
265 | if (PrintBlameContextAll && !PrintBlameContext) |
266 | PrintBlameContext.setValue(V: PrintBlameContextAll); |
267 | |
268 | std::unique_ptr<SpecialCaseList> SpecialCaseList; |
269 | if (IgnorelistFilename != "-" ) { |
270 | std::string Error; |
271 | SpecialCaseList = SpecialCaseList::create(Paths: {IgnorelistFilename}, |
272 | FS&: *vfs::getRealFileSystem(), Error); |
273 | if (!SpecialCaseList) { |
274 | errs() << "Failed to get ignorelist: " << Error << "\n" ; |
275 | exit(EXIT_FAILURE); |
276 | } |
277 | } |
278 | |
279 | FileAnalysis Analysis = ExitOnErr(FileAnalysis::Create(Filename: InputFilename)); |
280 | printIndirectCFInstructions(Analysis, SpecialCaseList: SpecialCaseList.get()); |
281 | |
282 | return EXIT_SUCCESS; |
283 | } |
284 | |