Skip to content

Commit 3cf1236

Browse files
authored
Add check for Stack Clash Protection (#22)
Add new feature `disassembly` checking for Stack Clash Protection[1], enabled by the compiler flag `-fstack-clash-protection`. Unlike other hardening features this can not be determined from the ELF information, but only from the disassembled code. Support limited to x86 due to the used disassembler iced_x86. This check scales with the size of the binary, noticeable for huge binaries like chromium (~800ms). [1]: https://blog.llvm.org/posts/2021-01-05-stack-clash-protection/
1 parent cc0131f commit 3cf1236

File tree

7 files changed

+261
-7
lines changed

7 files changed

+261
-7
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ colored_json = {version = "3.0.1", optional = true}
3131
either = "1.8.1"
3232
glob = "0.3.0"
3333
goblin = "0.6.0"
34+
iced-x86 = {version = "1.18.0", optional = true}
3435
ignore = "0.4.18"
3536
itertools = "0.10.5"
3637
memmap2 = "0.5.7"
@@ -66,7 +67,8 @@ path = "src/main.rs"
6667

6768
[features]
6869
color = ["colored", "colored_json", "xattr"]
69-
default = ["elf", "macho", "pe", "color", "maps"]
70+
default = ["elf", "macho", "pe", "color", "maps", "disassembly"]
71+
disassembly = ["iced-x86"]
7072
elf = ["shared"]
7173
macho = ["shared"]
7274
maps = []

examples/elf_print_checksec_results.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn main() {
1212
if let Ok(buf) = fs::read(&argv[1]) {
1313
match Object::parse(&buf).unwrap() {
1414
Object::Elf(elf) => {
15-
println!("{:#?}", CheckSecResults::parse(&elf))
15+
println!("{:#?}", CheckSecResults::parse(&elf, &buf))
1616
}
1717
_ => println!("Not an elf binary."),
1818
}

src/disassembly.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#[cfg(feature = "disassembly")]
2+
use iced_x86::{Decoder, DecoderOptions, Instruction, Mnemonic};
3+
4+
#[cfg(feature = "disassembly")]
5+
#[derive(Clone, Copy, PartialEq)]
6+
pub enum Bitness {
7+
B64,
8+
B32,
9+
}
10+
11+
#[cfg(feature = "disassembly")]
12+
impl Bitness {
13+
#[must_use]
14+
pub fn as_u32(self) -> u32 {
15+
match self {
16+
Self::B64 => 64,
17+
Self::B32 => 32,
18+
}
19+
}
20+
}
21+
22+
#[cfg(feature = "disassembly")]
23+
#[derive(PartialEq)]
24+
enum SCPSteps {
25+
Init,
26+
StartCmp,
27+
StartJump,
28+
CheckSubFirst,
29+
CheckOr,
30+
CheckSubLast,
31+
CheckXor,
32+
EndCmp,
33+
}
34+
35+
#[cfg(feature = "disassembly")]
36+
#[allow(clippy::too_many_lines)]
37+
#[must_use]
38+
pub fn has_stack_clash_protection(
39+
bytes: &[u8],
40+
bitness: Bitness,
41+
rip: u64,
42+
) -> bool {
43+
let mut decoder =
44+
Decoder::with_ip(bitness.as_u32(), bytes, rip, DecoderOptions::NONE);
45+
46+
let mut instr = Instruction::default();
47+
48+
let mut step = SCPSteps::Init;
49+
let mut start_addr = 0;
50+
let mut check_addr = 0;
51+
let mut jump_addr = 0;
52+
53+
while decoder.can_decode() {
54+
decoder.decode_out(&mut instr);
55+
56+
/*
57+
GCC:
58+
109e: cmp rsp,rcx
59+
10a1: je 10b8 <main+0x68>
60+
10a3: sub rsp,0x1000
61+
10aa: or QWORD PTR [rsp+0xff8],0x0
62+
10b3: cmp rsp,rcx
63+
10b6: jne 10a3 <main+0x53>
64+
10b8:
65+
66+
109e: cmp rsp,rcx
67+
10a1: je 10b5 <main+0x65>
68+
10a3: sub rsp,0x1000
69+
10aa: or QWORD PTR [rsp+0xff8],0x0
70+
10b3: jmp 109e <main+0x4e>
71+
10b5:
72+
73+
Clang:
74+
118b: cmp rbx,rsp
75+
118e: jge 11a1 <main+0x61>
76+
1190: xor QWORD PTR [rsp],0x0
77+
1195: sub rsp,0x1000
78+
119c: cmp rbx,rsp
79+
119f: jl 1190 <main+0x50>
80+
11a1:
81+
82+
1187: cmp rbx,rsp
83+
118a: jge 119a <main+0x5e>
84+
118c: xor QWORD PTR [rsp],0x0
85+
1191: sub rsp,0x1000
86+
1198: jmp 1187 <main+0x4b>
87+
119a:
88+
*/
89+
90+
let mnemonic = instr.mnemonic();
91+
92+
if step == SCPSteps::Init
93+
&& mnemonic == Mnemonic::Cmp
94+
&& (is_stack_pointer(instr.op0_register(), bitness)
95+
|| is_stack_pointer(instr.op1_register(), bitness))
96+
{
97+
step = SCPSteps::StartCmp;
98+
start_addr = instr.ip();
99+
continue;
100+
} else if step == SCPSteps::StartCmp
101+
&& (mnemonic == Mnemonic::Je || mnemonic == Mnemonic::Jge)
102+
{
103+
step = SCPSteps::StartJump;
104+
jump_addr = if bitness == Bitness::B64 {
105+
instr.memory_displacement64()
106+
} else {
107+
u64::from(instr.memory_displacement32())
108+
};
109+
continue;
110+
} else if step == SCPSteps::StartJump
111+
&& mnemonic == Mnemonic::Sub
112+
&& is_stack_pointer(instr.op0_register(), bitness)
113+
&& instr.immediate(1) == 4096
114+
{
115+
step = SCPSteps::CheckSubFirst;
116+
if check_addr == 0 {
117+
check_addr = instr.ip();
118+
}
119+
continue;
120+
} else if step == SCPSteps::CheckSubFirst
121+
&& mnemonic == Mnemonic::Or
122+
&& is_stack_pointer(instr.memory_base(), bitness)
123+
&& instr.immediate(1) == 0
124+
{
125+
step = SCPSteps::CheckOr;
126+
if check_addr == 0 {
127+
check_addr = instr.ip();
128+
}
129+
continue;
130+
} else if step == SCPSteps::StartJump
131+
&& mnemonic == Mnemonic::Xor
132+
&& is_stack_pointer(instr.memory_base(), bitness)
133+
&& instr.immediate(1) == 0
134+
{
135+
step = SCPSteps::CheckXor;
136+
if check_addr == 0 {
137+
check_addr = instr.ip();
138+
}
139+
continue;
140+
} else if step == SCPSteps::CheckXor
141+
&& mnemonic == Mnemonic::Sub
142+
&& is_stack_pointer(instr.op0_register(), bitness)
143+
&& instr.immediate(1) == 4096
144+
{
145+
step = SCPSteps::CheckSubLast;
146+
continue;
147+
} else if (step == SCPSteps::CheckOr || step == SCPSteps::CheckSubLast)
148+
&& mnemonic == Mnemonic::Jmp
149+
{
150+
let mem_disp = if bitness == Bitness::B64 {
151+
instr.memory_displacement64()
152+
} else {
153+
u64::from(instr.memory_displacement32())
154+
};
155+
if mem_disp == start_addr && jump_addr == instr.next_ip() {
156+
return true;
157+
}
158+
} else if (step == SCPSteps::CheckOr || step == SCPSteps::CheckSubLast)
159+
&& mnemonic == Mnemonic::Cmp
160+
&& (is_stack_pointer(instr.op0_register(), bitness)
161+
|| is_stack_pointer(instr.op1_register(), bitness))
162+
{
163+
step = SCPSteps::EndCmp;
164+
continue;
165+
} else if step == SCPSteps::EndCmp
166+
&& (mnemonic == Mnemonic::Jne || mnemonic == Mnemonic::Jl)
167+
{
168+
let mem_disp = if bitness == Bitness::B64 {
169+
instr.memory_displacement64()
170+
} else {
171+
u64::from(instr.memory_displacement32())
172+
};
173+
if mem_disp == check_addr && jump_addr == instr.next_ip() {
174+
return true;
175+
}
176+
}
177+
178+
step = SCPSteps::Init;
179+
start_addr = 0;
180+
check_addr = 0;
181+
jump_addr = 0;
182+
}
183+
184+
false
185+
}
186+
187+
#[cfg(feature = "disassembly")]
188+
fn is_stack_pointer(reg: iced_x86::Register, bitness: Bitness) -> bool {
189+
reg == match bitness {
190+
Bitness::B64 => iced_x86::Register::RSP,
191+
Bitness::B32 => iced_x86::Register::ESP,
192+
}
193+
}

src/elf.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ use goblin::elf::dynamic::{
88
};
99
use goblin::elf::header::ET_DYN;
1010
use goblin::elf::program_header::{PF_X, PT_GNU_RELRO, PT_GNU_STACK};
11+
#[cfg(feature = "disassembly")]
12+
use goblin::elf::section_header::{SHF_ALLOC, SHF_EXECINSTR, SHT_PROGBITS};
1113
use goblin::elf::Elf;
1214
use serde_derive::{Deserialize, Serialize};
15+
#[cfg(feature = "disassembly")]
16+
use std::convert::TryFrom;
1317
use std::fmt;
1418
#[cfg(target_os = "linux")]
1519
use std::path::{Path, PathBuf};
1620

1721
#[cfg(feature = "color")]
1822
use crate::colorize_bool;
23+
#[cfg(feature = "disassembly")]
24+
use crate::disassembly::{has_stack_clash_protection, Bitness};
1925
#[cfg(target_os = "linux")]
2026
use crate::ldso::{LdSoError, LdSoLookup};
2127
use crate::shared::{Rpath, VecRpath};
@@ -140,7 +146,7 @@ impl fmt::Display for Fortify {
140146
/// pub fn print_results(binary: &String) {
141147
/// if let Ok(buf) = fs::read(binary) {
142148
/// if let Ok(elf) = Elf::parse(&buf) {
143-
/// println!("{:#?}", CheckSecResults::parse(&elf));
149+
/// println!("{:#?}", CheckSecResults::parse(&elf, &buf));
144150
/// }
145151
/// }
146152
/// }
@@ -154,6 +160,8 @@ pub struct CheckSecResults {
154160
pub clang_cfi: bool,
155161
/// Clang SafeStack (*CFLAGS=*`-fsanitize=safe-stack`)
156162
pub clang_safestack: bool,
163+
/// Stack Clash Protection (*CFLAGS=*`-fstack-clash-protection`)
164+
pub stack_clash_protection: bool,
157165
/// Fortify (*CFLAGS=*`-D_FORTIFY_SOURCE`)
158166
pub fortify: Fortify,
159167
/// Fortified functions
@@ -175,7 +183,7 @@ pub struct CheckSecResults {
175183
}
176184
impl CheckSecResults {
177185
#[must_use]
178-
pub fn parse(elf: &Elf) -> Self {
186+
pub fn parse(elf: &Elf, bytes: &[u8]) -> Self {
179187
let (fortified, fortifiable) = elf.has_fortified();
180188
let fortify = match (fortified, fortifiable) {
181189
(1.., 0) => Fortify::Full,
@@ -190,6 +198,7 @@ impl CheckSecResults {
190198
fortify,
191199
fortified,
192200
fortifiable,
201+
stack_clash_protection: elf.has_stack_clash_protection(bytes),
193202
nx: elf.has_nx(),
194203
pie: elf.has_pie(),
195204
relro: elf.has_relro(),
@@ -210,11 +219,12 @@ impl fmt::Display for CheckSecResults {
210219
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211220
write!(
212221
f,
213-
"Canary: {} CFI: {} SafeStack: {} Fortify: {} Fortified: {:2} \
222+
"Canary: {} CFI: {} SafeStack: {} StackClash: {} Fortify: {} Fortified: {:2} \
214223
Fortifiable: {:2} NX: {} PIE: {} Relro: {} RPATH: {} RUNPATH: {}",
215224
self.canary,
216225
self.clang_cfi,
217226
self.clang_safestack,
227+
self.stack_clash_protection,
218228
self.fortify,
219229
self.fortified,
220230
self.fortifiable,
@@ -230,13 +240,15 @@ impl fmt::Display for CheckSecResults {
230240
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231241
write!(
232242
f,
233-
"{} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {}",
243+
"{} {} {} {} {} {} {} {} {} {} {} {:2} {} {:2} {} {} {} {} {} {} {} {} {} {}",
234244
"Canary:".bold(),
235245
colorize_bool!(self.canary),
236246
"CFI:".bold(),
237247
colorize_bool!(self.clang_cfi),
238248
"SafeStack:".bold(),
239249
colorize_bool!(self.clang_safestack),
250+
"StackClash:".bold(),
251+
colorize_bool!(self.stack_clash_protection),
240252
"Fortify:".bold(),
241253
self.fortify,
242254
"Fortified:".bold(),
@@ -282,6 +294,8 @@ pub trait Properties {
282294
fn has_clang_cfi(&self) -> bool;
283295
/// check for `__safestack_init` in dynstrtab
284296
fn has_clang_safestack(&self) -> bool;
297+
/// checks for Stack Clash Protection
298+
fn has_stack_clash_protection(&self, bytes: &[u8]) -> bool;
285299
/// check for symbols ending in `_chk` from dynstrtab
286300
fn has_fortify(&self) -> bool;
287301
/// counts fortified and fortifiable symbols from dynstrtab
@@ -433,6 +447,39 @@ impl Properties for Elf<'_> {
433447
}
434448
false
435449
}
450+
#[allow(unused_variables)]
451+
fn has_stack_clash_protection(&self, bytes: &[u8]) -> bool {
452+
for sym in &self.syms {
453+
if let Some(name) = self.strtab.get_at(sym.st_name) {
454+
if name == "__rust_probestack" {
455+
return true;
456+
}
457+
}
458+
}
459+
#[cfg(not(feature = "disassembly"))]
460+
return false;
461+
#[cfg(feature = "disassembly")]
462+
self.section_headers
463+
.iter()
464+
.filter(|sh| {
465+
sh.sh_type == SHT_PROGBITS
466+
&& sh.sh_flags & u64::from(SHF_EXECINSTR | SHF_ALLOC)
467+
== u64::from(SHF_EXECINSTR | SHF_ALLOC)
468+
})
469+
.any(|sh| {
470+
if let Some(execbytes) = bytes.get(
471+
usize::try_from(sh.sh_offset).unwrap()
472+
..usize::try_from(sh.sh_offset + sh.sh_size).unwrap(),
473+
) {
474+
let bitness =
475+
if self.is_64 { Bitness::B64 } else { Bitness::B32 };
476+
477+
has_stack_clash_protection(execbytes, bitness, sh.sh_addr)
478+
} else {
479+
false
480+
}
481+
})
482+
}
436483
fn has_fortify(&self) -> bool {
437484
for sym in &self.dynsyms {
438485
if !sym.is_function() {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
//! `*CheckSecResults` structs.
4545
//!
4646
47+
#[cfg(feature = "disassembly")]
48+
pub mod disassembly;
4749
#[cfg(feature = "elf")]
4850
pub mod elf;
4951
#[cfg(target_os = "linux")]

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ fn parse_bytes(bytes: &[u8], file: &Path) -> Result<Vec<Binary>, ParseError> {
341341
match Object::parse(bytes)? {
342342
#[cfg(feature = "elf")]
343343
Object::Elf(elf) => {
344-
let results = elf::CheckSecResults::parse(&elf);
344+
let results = elf::CheckSecResults::parse(&elf, bytes);
345345
let bin_type =
346346
if elf.is_64 { BinType::Elf64 } else { BinType::Elf32 };
347347
Ok(vec![Binary::new(

0 commit comments

Comments
 (0)