Files

23 KiB

Table of Contents

  1. Introduction
  2. Setup
  3. Emulation
  4. Tracing
  5. Symbolic Execution
    1. Setup
    2. Run
    3. Discussion
      1. Loading the Trace File
      2. How Hooking Works
      3. Analyzing Symbolic State
  6. Vulnerability CVE-2022-27646
  7. Exploitation

Symbolic Execution

Chapter Tracing explained how to collect a concrete execution trace of your target, which in our specific case is the ARMv7 binary circled. If you followed along the given instructions, this trace was stored in the file circled.yaml. As we will see below, this trace file may then be used as input for the different analysis modules implemented by Morion. These analysis modules execute the collected trace symbolically, which then allows for reasoning about the target's behavior by solving constraints for specified mathematical problems. An example for such a problem might for instance be the question of whether or not it is possible for the program counter (register pc) to become a certain value, and if so, how this can be achieved (e.g. leading to a control-flow hijacking condition).


Morion Overview
Figure 4.1: Morion Overview - Showing Morion's two operation modes tracing and symbolic execution.

Setup

Before running one of Morion's symbolic analysis modules, the collected trace file circled.yaml might optionally be customized. Such customizations could for example be to:

  • Mark (additional) register values or memory locations as being symbolic (in the entry state)
  • Modify the parameter mode of hooked functions
  • Add/remove assembly instructions to/from the trace
  • Add extra inputs for analysis modules (e.g. intended ROP chains for analysis module morion_rop_generator, see e.g. chapter Exploitation)

In our documented example of the binary circled, all relevant configurations have already been defined in the file circled.init.yaml, which during tracing got copied over to the file circled.yaml. Therefore, no further customizations are needed.

Run

Use the following step to symbolically execute a previously collected trace from a concrete execution run of binary circled:

  1. Use morion to execute the collected trace symbolically:
    • System: Analysis / Host (morion)
    • Command:
      cd morion/;             # Ensure to be within the correct directory
      morion -h;              # Optionally show usage help
      morion circled.yaml;    # Execute program trace symbolically
      

Remember that if you followed along the instructions in chapter Tracing, the trace was collected while the vulnerable binary processed a sample payload leading to a crasher/segfault (as might have been identified by a fuzzer).

DEMO Symbolic_Execution/Run - Click the image below to watch on YouTube: Demo Video

Analysis Modules

Morion implements different analysis modules that are based on symbolic execution. The chosen design attempts to make Morion easily extendable with new modules. The currently implemented ones are summarized in the table below:

Module Description
morion Perform symbolic execution on a binary's program trace.
morion_backward_slicer Symbolically execute a program trace for backward slicing. The analysis identifies backward slices for a specified register or memory address.
morion_branch_analyzer Symbolically execute a program trace for branch analysis. The analysis identifies multi-way branches along the trace and outputs concrete values of how to reach the non-taken branch. A specific branch is only evaluated once.
morion_path_analyzer Symbolically execute a program trace for path analysis. The analysis identifies unique paths along the trace and outputs concrete values of how to reach these paths. A path consists of a sequence of multi-way branches. The last multi-way branch in each outputted path is non- taken in the concrete execution of the trace.
morion_memory_hijacker Symbolically execute a program trace to identify potential memory hijacks. A memory hijack corresponds to the target of a memory read or write operation being (partly) symbolic.
morion_control_hijacker Symbolically execute a program trace to identify potential control flow hijacks. A control flow hijack corresponds to registers, influencing the control flow (such as pc), becoming (partly) symbolic.
morion_rop_generator Symbolically execute a program trace to help generating a ROP chain.

Discussion

In the following, we discuss some aspects of the symbolic execution process as implemented by Morion.

Loading the Trace File

As explained in section Tracing: Collecting the Trace, beside the executed assembly instructions, the initial concrete values of all registers and/or memory locations accessed within the trace are recorded (in our specific example in the file circled.yaml). Before starting the actual symbolic execution, these are used by Morion to initialize the corresponding concrete register and/or memory values within the context of the symbolic execution engine (which in our case is Triton). This assures that the symbolic execution of the recorded trace uses the correct concrete register and/or memory values. This initialization of registers and/or memory locations can be observed in Morion's debug output:

[2024-08-20 14:16:43] [INFO] Start loading file 'circled.yaml'...
[2024-08-20 14:16:45] [DEBG] Regs:
[2024-08-20 14:16:45] [DEBG] 	n=0x0
[2024-08-20 14:16:45] [DEBG] 	r0=0x21a90
[2024-08-20 14:16:45] [DEBG] 	r1=0x1
[2024-08-20 14:16:45] [DEBG] 	r10=0xbeffc514
[2024-08-20 14:16:45] [DEBG] 	r11=0xbeffcd14
[2024-08-20 14:16:45] [DEBG] 	r2=0x258
[2024-08-20 14:16:45] [DEBG] 	r3=0x0
[2024-08-20 14:16:45] [DEBG] 	r4=0x1b850
[2024-08-20 14:16:45] [DEBG] 	r5=0xbeffd078
[2024-08-20 14:16:45] [DEBG] 	r6=0xbeffcf14
[2024-08-20 14:16:45] [DEBG] 	r7=0xbeffce14
[2024-08-20 14:16:45] [DEBG] 	r8=0x21a90
[2024-08-20 14:16:45] [DEBG] 	r9=0xffff68a8
[2024-08-20 14:16:45] [DEBG] 	sp=0xbeffc080
[2024-08-20 14:16:45] [DEBG] 	v=0x0
[2024-08-20 14:16:45] [DEBG] 	z=0x0
[2024-08-20 14:16:45] [DEBG] Mems:
[2024-08-20 14:16:45] [DEBG] 	0x0000d230=0xbc  
[2024-08-20 14:16:45] [DEBG] 	0x0000d231=0x6a j
[2024-08-20 14:16:45] [DEBG] 	0x0000d232=0xff  
[2024-08-20 14:16:45] [DEBG] 	0x0000d233=0xff  
[...]
[2024-08-20 14:16:45] [DEBG] 	0x000120f8=0x25 %
[2024-08-20 14:16:45] [DEBG] 	0x000120f9=0x73 s
[2024-08-20 14:16:45] [DEBG] 	0x000120fa=0x20  
[2024-08-20 14:16:45] [DEBG] 	0x000120fb=0x25 %
[2024-08-20 14:16:45] [DEBG] 	0x000120fc=0x73 s
[2024-08-20 14:16:45] [DEBG] 	0x000120fd=0x00 
[...]
[2024-08-20 14:16:45] [DEBG] 	0xbeffc914=0x00  
[2024-08-20 14:16:45] [DEBG] 	0xbeffd07c=0x41 A
[2024-08-20 14:16:45] [DEBG] 	0xbeffd07d=0x41 A
[...]
[2024-08-20 14:16:45] [DEBG] 	0xbeffd09e=0x41 A
[2024-08-20 14:16:45] [DEBG] 	0xbeffd09f=0x41 A
[...]

In a similar manner, registers and/or memory locations can be assigned a new symbolic variable, before the actual symbolic execution of the trace begins. To for example mark the register r0 as being symbolic (alongside its concrete initial value of 0x21a90), the trace file circled.yaml (respectively the file circled.init.yaml) could contain an entry such as the one shown below:

[...]
states:
  entry:
    regs:
      'r0': ['0x21a90', '$$$$$$$$']   # Marking register r0 as being symbolic
      [...]
    mems:
      '0x000120f8': ['0x25', '$$']    # Marking memory location 0x120f8 as being symbolic
      [...]
[...]

As can be seen in the excerpt above, Morion uses the specifier $$ for referring to a symbolic byte. In the above example, where a symbolic register and a symbolic memory location were defined, Morion's debug output, while loading the trace file, would look like this:

[...]
[2024-08-20 14:16:45] [DEBG] 	r0=0x21a90
[2024-08-20 14:16:45] [DEBG] 	r0=$$$$$$$$
[...]
[2024-08-20 14:16:45] [DEBG] 	0x000120f8=0x25 %
[2024-08-20 14:16:45] [DEBG] 	0x000120f8=$$
[...]

Note that in our example of binary circled, we do not manually mark any register and/or memory location as being symbolic (there are no $$ specifiers in the entry state of file circled.init.yaml). Instead, and as will be explained in section Symbex: How Hooking Works below, all symbolic variables are automatically introduced by Morion and its model for the hooked libc function fgets.

After initializing concrete and/or symbolic values of all necessary registers and/or memory locations in the context of the symbolic execution engine, Morion sets up the defined function hooks:

[...]
[2024-08-20 14:16:45] [DEBG] Hooks:
[2024-08-20 14:16:45] [DEBG] 	0x0000d040: 'lib:func_hook (on=entry, mode=skip)'
[2024-08-20 14:16:45] [DEBG] 	0x0000d044: 'lib:func_hook (on=leave, mode=skip)'
[...]
[2024-08-20 14:16:45] [DEBG] 	0x0000c9c4: 'lib:func_hook (on=entry, mode=skip)'
[2024-08-20 14:16:45] [DEBG] 	0x0000c9c8: 'lib:func_hook (on=leave, mode=skip)'
[2024-08-20 14:16:45] [DEBG] 	0x0000cfe0: 'libc:fgets (on=entry, mode=model)'
[2024-08-20 14:16:45] [DEBG] 	0x0000cfe4: 'libc:fgets (on=leave, mode=model)'
[2024-08-20 14:16:45] [DEBG] 	0x0000d094: 'libc:fgets (on=entry, mode=model)'
[2024-08-20 14:16:45] [DEBG] 	0x0000d098: 'libc:fgets (on=leave, mode=model)'
[2024-08-20 14:16:45] [DEBG] 	0x0000cffc: 'libc:sscanf (on=entry, mode=model)'
[2024-08-20 14:16:45] [DEBG] 	0x0000d000: 'libc:sscanf (on=leave, mode=model)'
[2024-08-20 14:16:45] [INFO] ... finished loading file 'circled.yaml'.
[...]

How hooking works, while executing a trace symbolically, is explained next.

How Hooking Works

We already discussed how hooking works during tracing in section Tracing: How Hooking Works. Here, we discuss some aspects about hooking while executing a trace symbolically.

Abstract Function Hook with Mode Skip (Example libc:fclose)

In the chapter about tracing (see section Tracing: How Hooking Works), we have already seen that the file circled.init.yaml defines a hook for entry and leave addresses 0xd040 and 0xd044, respectively. The hook corresponds to a call to function fclose (synopsis: int fclose(FILE *stream);) from libc (or to be more specific, uclibc in case of the binary circled). Due to the hooking, the effective assembly instructions of the function itself have not been traced. Instead Morion injected some instructions to reproduce some of the function's side-effects. Since we used an abstract function hook (hooks:lib:func_hook:), the only modelled side-effect corresponds to setting the correct return value(s). For the ARMv7 architecture that we target, a function's return value is generally stored in register r0 (and potentially r1). This is exactly what instructions 0x1000 - 0x100c are used for. Morion injected assembly instructions to move the trace's effective return value of function fclose (here 0, meaning a successful closure of the corresponding file stream) to the return register(s).

[...]
[2024-08-20 14:16:46] [DEBG] 0x0000d03c (08 00 a0 e1): mov r0, r8
[2024-08-20 14:16:46] [DEBG] --> Hook: 'lib:func_hook (on=entry, mode=skip)'
[2024-08-20 14:16:46] [DEBG]           'func_hook'
[2024-08-20 14:16:46] [DEBG]     ---
[2024-08-20 14:16:46] [DEBG] 0x0000d040 (ee cf ff ea): b #0x1000               # // Hook: lib:func_hook (on=entry, mode=skip)    
[2024-08-20 14:16:46] [DEBG] 0x00001000 (00 00 a0 e3): mov r0, #0              # // Hook: lib:func_hook (on=leave, mode=skip)    
[2024-08-20 14:16:46] [DEBG] 0x00001004 (00 00 40 e3): movt r0, #0             # // Hook: lib:func_hook (on=leave, mode=skip)    
[2024-08-20 14:16:46] [DEBG] 0x00001008 (01 10 a0 e3): mov r1, #1              # // Hook: lib:func_hook (on=leave, mode=skip)    
[2024-08-20 14:16:46] [DEBG] 0x0000100c (00 10 40 e3): movt r1, #0             # // Hook: lib:func_hook (on=leave, mode=skip)    
[2024-08-20 14:16:46] [DEBG] 0x00001010 (0b 30 00 ea): b #0xd044               # // Hook: lib:func_hook (on=leave, mode=skip)    
[2024-08-20 14:16:46] [DEBG]     ---
[2024-08-20 14:16:46] [DEBG] <-- Hook: 'lib:func_hook (on=leave, mode=skip)'
[2024-08-20 14:16:46] [DEBG] 0x0000d044 (04 30 9d e5): ldr r3, [sp, #4]
[...]

When running a trace symbolically, Morion follows along the recorded instructions (including the ones injected by hooks during tracing) and executes them one by one using its underlying symbolic execution engine (Triton in our case). For hooks with mode skip, no additional modifications of the symbolic state are performed. As we will see in the next example, this might be different when using a hook with mode model.

Specific Function Hook with Mode Model (Example libc:fgets)

As mentioned in section Tracing: How Hooking Works, the file circled.init.yaml, beside others, also defines a hook for entry and leave addresses 0xcfe0 and 0xcfe4, respectively. The hook is intended to catch calls to function fgets (synopsis: char *fgets(char *s, int n, FILE *stream)) of library uclibc. Again, due to the hooking, the effective assembly instructions of the function itself have not been recorded during tracing. Instead, some instructions got injected to reproduce the side-effects that the function has on the memory and register contexts. When looking closely to the injected instructions, one observes that first the effective bytes of string s are set (0xbeffc914: 0x41 'A' - 0xbeffcd12: 0x58 'X', 0xbeffcd13: 0x00), and second the correct return value (0xbeffc914 - the address of string s) is placed into register r0. In contrast to an abstract function hook (hooks:lib:func_hook:) where only instructions to handle correct function return values are injected, specific function hooks (hooks:libc:fgets) handle additional function-specific side-effects to the memory and register contexts.

[...]
[2024-08-20 14:16:45] [INFO] Start symbolic execution...
[2024-08-20 14:16:45] [DEBG] 0x0000cfc0 (64 37 65 e5): strb r3, [r5, #-0x764]!
[...]
[2024-08-20 14:16:45] [DEBG] 0x0000cfdc (05 00 a0 e1): mov r0, r5
[2024-08-20 14:16:45] [DEBG] --> Hook: 'libc:fgets (on=entry, mode=model)'
[2024-08-20 14:16:45] [DEBG]           'char *fgets(char *restrict s, int n, FILE *restrict stream);'
[2024-08-20 14:16:45] [DEBG] 	 s = 0xbeffc914
[2024-08-20 14:16:45] [DEBG] 	 n = 1024
[2024-08-20 14:16:45] [DEBG] 	 stream = 0x00021a90
[2024-08-20 14:16:45] [DEBG]     ---
[2024-08-20 14:16:45] [DEBG] 0x0000cfe0 (06 d0 ff ea): b #0x1000               # // Hook: libc:fgets (on=entry, mode=model)      
[2024-08-20 14:16:45] [DEBG] 0x00001000 (14 09 0c e3): movw r0, #0xc914        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:45] [DEBG] 0x00001004 (ff 0e 4b e3): movt r0, #0xbeff        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:45] [DEBG] 0x00001008 (41 10 a0 e3): mov r1, #0x41           # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:45] [DEBG] 0x0000100c (00 10 40 e3): movt r1, #0             # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:45] [DEBG] 0x00001010 (00 10 c0 e5): strb r1, [r0]           # // Hook: libc:fgets (on=leave, mode=model)      
[...]
[2024-08-20 14:16:46] [DEBG] 0x00005fd8 (12 0d 0c e3): movw r0, #0xcd12        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005fdc (ff 0e 4b e3): movt r0, #0xbeff        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005fe0 (58 10 a0 e3): mov r1, #0x58           # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005fe4 (00 10 40 e3): movt r1, #0             # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005fe8 (00 10 c0 e5): strb r1, [r0]           # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005fec (13 0d 0c e3): movw r0, #0xcd13        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005ff0 (ff 0e 4b e3): movt r0, #0xbeff        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005ff4 (00 10 a0 e3): mov r1, #0              # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005ff8 (00 10 40 e3): movt r1, #0             # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00005ffc (00 10 c0 e5): strb r1, [r0]           # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00006000 (14 09 0c e3): movw r0, #0xc914        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00006004 (ff 0e 4b e3): movt r0, #0xbeff        # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 0x00006008 (f5 1b 00 ea): b #0xcfe4               # // Hook: libc:fgets (on=leave, mode=model)      
[2024-08-20 14:16:46] [DEBG] 	 s = 0xbeffc914
[2024-08-20 14:16:46] [DEBG] 	*s = 'AAA[...]AAA X'
[2024-08-20 14:16:46] [DEBG] 	0xbeffc914 = $$
[2024-08-20 14:16:46] [DEBG] 	...
[2024-08-20 14:16:46] [DEBG] 	0xbeffcd12 = $$
[2024-08-20 14:16:46] [DEBG]     ---
[2024-08-20 14:16:46] [DEBG] <-- Hook: 'libc:fgets (on=leave, mode=model)'
[2024-08-20 14:16:46] [DEBG] 0x0000cfe4 (00 00 50 e3): cmp r0, #0
[...]
[2024-08-20 14:16:46] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-08-20 14:16:46] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc
[2024-08-20 14:16:46] [INFO] ... finished symbolic execution (pc=0x41414140).
[...]

Since we defined a function-specific hook to be used with mode model, additional modifications to the symbolic state might be performed. These modifications can happen either at entry or when leaving the hook. The model implementation of fgets (see libc.py#L12 for more details), for instance, makes the string s symbolic. This means that new symbolic variables get assigned to each character/byte of the read string.

But why does one want to make s symbolic? Well, s is read in from a resource external to the targeted binary (here a file), which is potentially controllable by a (malicious) user. By making s symbolic we might conduct various analysis about how it, respectively an attacker, can influence our target program. As will be explained in the next section Analyzing Symbolic State, in the case of binary circled, we for instance immediately see that the program counter (pc) at the end of the trace is symbolic. Phrased differently, the pc can (somehow) be influenced by attacker-controllable values, potentially leading to control-flow hijacking attacks.

Note: In case you want to hook invocations of function fgets without making the read string symbolic, define the corresponding hook to use mode skip.

Note: Morion is a proof-of-concept (PoC) tool intended to be used for experimenting with symbolic execution on (real-world) (ARMv7) binaries. It currently implements (only) a handful of hooks for common libc functions. These should be extended in future work.

Analyzing Symbolic State

If we symbolize inputs that an attacker can control, symbolic execution not only allows us to see which parts of a target binary can be influenced, but also how this may happen. At the end of a symbolic execution run, Morion for instance analyzes the symbolic state (can be disabled using option --skip_state_analysis). This means that an overview is given about which registers and memory locations are based on symbolic variable(s). In the example of the collected trace of binary circled, one immediately sees that the program counter (register pc) is one of these candidates. Remember that we only marked inputs an attacker might control as being symbolic, meaning that an attacker can potentially influence the program's control-flow.

[...]
[2024-08-20 14:16:46] [INFO] Start analyzing symbolic state...
[2024-08-20 14:16:46] [INFO] Symbolic Regs:
[2024-08-20 14:16:46] [INFO] 	pc=$$$$$$$$
[2024-08-20 14:16:46] [INFO] 	r10=$$$$$$$$
[2024-08-20 14:16:47] [INFO] 	r11=$$$$$$$$
[2024-08-20 14:16:47] [INFO] 	r4=$$$$$$$$
[2024-08-20 14:16:47] [INFO] 	r5=$$$$$$$$
[2024-08-20 14:16:47] [INFO] 	r6=$$$$$$$$
[2024-08-20 14:16:47] [INFO] 	r7=$$$$$$$$
[2024-08-20 14:16:48] [INFO] 	r8=$$$$$$$$
[2024-08-20 14:16:48] [INFO] 	r9=$$$$$$$$
[2024-08-20 14:16:48] [INFO] Symbolic Mems:
[2024-08-20 14:17:56] [INFO] 	0xbeffc914=$$
[2024-08-20 14:17:56] [INFO] 	...
[2024-08-20 14:17:56] [INFO] 	0xbeffcd12=$$
[2024-08-20 14:17:56] [INFO] 	0xbeffce14=$$
[2024-08-20 14:17:56] [INFO] 	0xbeffcf14=$$
[2024-08-20 14:17:56] [INFO] 	...
[2024-08-20 14:17:56] [INFO] 	0xbeffd310=$$
[2024-08-20 14:17:56] [INFO] ... finished analyzing symbolic state.
[2024-08-20 14:17:56] [INFO] Start storing file 'circled.yaml'...
[2024-08-20 14:17:58] [INFO] ... finished storing file 'circled.yaml'.

In chapter Exploitation, we will see how symbolic execution might help us to decide whether the crasher we traced might be exploitable or not, i.e. corresponds to an actual security vulnerability or just an annoying bug. We will also see, how symbolic execution can quickly give us a first high-level intuition about what capabilities (e.g. arbitrary read/write, control-flow hijacking, etc.) one might gain with the underlying issue. Also, we will show how symbolic execution might help us during the process of generating a working exploit for the targeted vulnerability (CVE-2022-27646).


Back-to-Top