CVE-2025-60690

Overview of each section

  1. Ghidra

  • Disassembly of the get_merge_ipaddr function

  1. apply_cgi + validate_lan_ipaddr source code analysis + payload generation

  • Analysis of the source code to understand how we can reach the vulnerable function

  • Generation of the initial working payload to control values in the buffer

  1. strcat function analysis + manual breakpoints

  • httpd binary patching method to set a manual breakpoint

  • Identification of the argument values passed to the vulnerable strcat function

  • Calculation of the offset between buffer and return address

  1. Investigating buffer oveflow

  • Generation of payload to overwrite the return address and trigger a SIGSEGV

  1. Investigating ret2 methods + ROP

  • Overview of basic ROP gadgets

  • Enumerate gadgets from /lib/libc.so.0

  1. Exploit development (METHOD 1: ROP + ret2libc)

  2. Exploit development (METHOD 2: ROP to shell-code)

1. Ghidra

1.1 get_merge_ipaddr

1.2 Refined get_merge_ipaddr

1.3 Analysis

Keep in mind that we are working with the MIPS assembly for this binary. The instructions and concepts are different from common x86, x64 or ARM assembly

We can see that the param_1 and param_2 variables corresponds to the 1st and 2nd arguments passed to the get_merge_ipaddr function. As what we can understand from MIPS assembly, this relates to the a0 and a1 registers in the current function (get_merge_ipaddr)

However, this does not necessarily mean the same for the nested function calls within the current get_merge_ipaddr function (eg. strcat, get_cgi)

Hence, there may be a situation where the disassembly listing view shows a particular param_x variable (corresponding to ax register) passed in as an argument to a nested function call, but that does not actually correspond to the x-positioned argument to that function call

1.3.1 Other findings

We can discover that the 2nd argument to the get_merge_ipaddr function is passed in as the 1st argument to the nested strcat function. Take a look at the following disassembly code (objdump):

  1. (#1) Move a1 to s1

  • Moves the value of the 2nd argument to the get_merge_ipaddr function to the s1 register

  1. (#2) Move s1 to a0

  • a0 (value from s1) indicates the 1st argument to the following function call (strcat)

  1. (#3) Calls the strcat function (address stored in temporary register t9)

1.3.2 Important notes from each section (Ghidra)

  1. Disassembly

  • The a0 to aX registers indicates the exact values passed to each of the nested function calls

  1. Decompiled code

  • The param_1 and param_2 variables indicates the 1st and 2nd argument to the current function

  • The source of truth of argument passing should come from the a0, a1, an, etc. registers in the disassembly, rather than the param_X variable naming in decompiled code

2. apply_cgi source code analysis + initial HTTP request generation

In this section, I will present the following:

  1. Working HTTP POST request that will allow us to reach the vulnerable portion of the code

  2. The explanation of each data parameters used

2.1 Working request format to write to buffer

Note: The basic token value YWRtaW46YWRtaW4= is the base64-encoded string of the value admin:admin

cURL

  • Automatically handles the following request headers: Host and Content-Length

Netcat

2.2 Explanation of each data parameter

  1. action=Apply

  • Our intended flow (to call the validate_lan_ipaddr function) is enclosed by a few IF statements:

  • The following conditions must be met:

a. skip_amd_check must not be defined in the data parameter

b. action must be one of the following values: "Apply", "tmUnblock", "hndUnblock"

  • Returns <!--Invalid Value!<br> --> if the else block is taken instead

  1. lan_ipaddr=x

  • The value provided to this parameter does not require any significant values, as it is not provided as an argument to the final get_merge_ipaddr function

  • The following line represents the validate_lan_ipaddr function call in Ghidra:

  • Focus on the following code snippet (within our intended code flow)

  • Notice that the ppuVar5 variable is assigned to the pointer to variables

&variables

The following line calls the get_cgi function on the first value in &variables ("lan_ipaddr"):

  • Thus, the lan_ipaddr CGI parameter has to be specified in order for our code flow to reach our intended function

After we have passed the first IF statement:

We have to find a way to pass the second IF statement, to eventually reach our intended validate_lan_ipaddr function call:

We can simply set pcVar3 to a non-empty string (!= '\0') to pass the following checks:

Thus, any value provided to this field such as: lan_ipaddr=x will work

  1. lan_netmask=x (can be empty)

We are now able to call the validate_lan_ipaddr function. The following code snippet shows the first few lines:

  • The CGI data parameter lan_netmask will be checked for existence

    • Return from function if it does not exist

  • Hence, we simply have to specify this parameter (even with an empty value) in order to pass the checks, and proceed to the get_merge_ipaddr function

  1. lan_ipaddr_0=lan0&lan_ipaddr_1=lan1&lan_ipaddr_2=lan2&lan_ipaddr_3=lan3

Range of values that are directly inserted to the stack without bounds checking. Let's refer back to a code snippet from the (refined) get_merge_ipaddr function:

We are able to gather the following information:

  • snprintf function concats the string values with the specifier format: a0_i to the acStack_48 variable

  • the a0 variable is passed in as the 1st argument to get_merge_ipaddr, while the variable i iterates from 0 to 3

  • the concatenated string will be used as a CGI parameter key to retrieve a value, and store it into the __src variable

  • the __src variable will than be concatenated into the address pointed to by a1 (2nd argument to get_merge_ipaddr) in the vulnerable strcat function

How are we able to identify the parameter name (lan_ipaddr_n=X, eg. lan_ipaddr_0=X)?

a. lan_ipaddr=X parameter (from previous findings)

b. Analysisarrow-up-right of HTTP POST request generated by web form:

c. Analysisarrow-up-right of argument values passed to the strcat function using gdb+gdbserver setup

3. strcat function analysis + manual breakpoints

Refer to the GDB, gdbserver section under the Expoit research page, for more information on the setup process

Refer to the following disassembly snippet:

Disassembly (Ghidra):

Disassembly (gdb):

In order to identify the relevant values, we can set a breakpoint@0x0041f99c, and print the values of the following registers:

  1. ra at address $sp+96

  2. a0 and v0 to identify the 1st and 2nd argument to the strcat function

  • Notice that the value in the v0 register is moved to the a1 register in the delay slot

  • As we are not able to reliably predict when the delay slot on line 0x41f99c will run, we can set a breakpoint before the strcat function call (0x0041f99c), and retrieve the value directly from the v0 register instead

  1. s2 to identify the CGI parameter name

3.1 Set custom breakpoint (patch method)

3.1.1 Patch command

  • of=/tmp/httpd: output file

  • bs=1: byte-size set to 1

  • seek: offset from the httpd base (0x00400000). Refer to the sectionarrow-up-right

3.1.2 Run the patched binary (device) + attach to gdbserver

3.2 Analysis of register values (a0, v0,s2)

Next, we can send a HTTP POST request to the httpd web server using what we have discovered from previous findingsarrow-up-right:

  • Notice the parameter value of lan0 to the lan_ipaddr_0 key

From a separatearrow-up-right terminal, send a manual SIGSTOParrow-up-right signal to the httpd process:

Now, we can view the values:

Take note of the following:

  1. The return address for the current function (get_merge_ipaddr: $sp+96 = 0x7ffa4e68) is at a lower memory address than the buffer we are able to write from the strcat function (p/x $a0 = 0x7ffa4e88)

  • This presents us with an issue when we are attempting to control the ra register

  • Refer to the next section for more information on this issue

  1. Since the delay slot (line 0x41f99c) is not taken as a result of our custom breakpoint, the $a1 register will not contain the actual value to the strcat function

  • Instead, we can view the expected content of the $a1 register via the $v0 register, which will contain the value we supplied to the lan_ipaddr_0 parameter in our HTTP POST request (eg. "lan0" in our example)

  • note that the $v0 register contains the return value from the previous function that was called, which happens to be get_cgi

  1. We can also view the name of the CGI parameter that we can control by reading the value in the $s2 register

  • the value of this CGI parameter is directly concatenated to the buffer

  • with our breakpoint example, the value will be "lan_ipaddr_0"

4 Investigating buffer overflow (get_merge_ip_addr function)

The value of $sp is in context of the get_merge_ipaddr function

The value ra refers to the stack portion that is restored to the ra register

4.1 Stack layout constraints

Normally, we would want to overwrite the return address of the current function (get_merge_ipaddr). However, the buffer variable is passed in as an argument from the calling function (validate_lan_ipaddr), resulting buffer address to be placed higher than the return address of the current function:

  • The buffer address is higher than the stack portion that is used to restore to s0-s7 and ra

  • In the MIPS architecture, values are written towards higher memory address. Thus, this means that the stack layout physically prevents us from overwriting the return address of the current (get_merge_ipaddr) function

To bypass this issue, we attempt to overwrite stack portions (that restores to ra, s0-s7) of the calling function instead (validate_lan_ipaddr).

Refer to the following disassembly code snippet from the end of the validate_lan_ipaddr function:

We can understand the following:

  • Line 0x4285f4: The offset value $sp+132 is used to restore the $ra register

Let's perform a few calculations to calculate the offset values:

Stack values/offsets calculation:

a. Buffer ($a0) - $sp = 0x80 ⇒ buffer = $sp + 0x80

b. Stack address portion restored to ra in validate_lan_ipaddr: $sp+0x68+132 = $sp + 0xec

⇒ $sp+0x68 happens when merge_ip_addr function returns

132 is from the lw ra, 132(sp) instruction we have found before

c. Offset between buffer and ra (validate_lan_ipaddr): 0xec - 0x80 = 0x6c = 108 (decimal)

4.2 Working payload to overwrite & control ra register value (in validate_lan_ipaddr function)

Take note of the following:

  • The offset padding value used in the payload is 102, instead of 108 in the payload: "A" * 102

    • We will find out the reason for this in the explanation below

  • The values supplied to the lan_ipaddr_X CGI parameters

    • a, b, c, python3 -c ...

1. Testing overwrite of the ra register (validate_lan_ipaddr)

For this section, assume a break point is set on the return (0x00428614) line in the validate_lan_ipaddr function:

  • of=/tmp/httpd: output file

  • bs=1: byte-size set to 1

  • seek: offset from the httpd base (0x00400000). Refer to the sectionarrow-up-right

After the POST request is sent, we are able to use the offset $sp-0x68+0x80 to locate the buffer and the subsequent overwritten values on the stack

  • The value 0x68 represents the frame size (that is used to increment the $sp at the end of the get_merge_ipaddr function right before it returns). Thus, it is subtracted from the current $sp to bring us back to the context of the get_merge_ipaddr function

  • The value 0x80 is the relative offset between the buffer and $sp

  • Notice the custom payload value passed to the lan_ipaddr_3 parameter:

  • We have identified previously that the offset between the buffer and ra (validate_lan_ipaddr) is 0x6c = 108 (decimal)

  • From the stack value shown in the memory analysis, we can see that buffer contains the value "a.b.c." (length 6) and subsequent 'A's

    • this is because the first 3 lan_ipaddr_X parameters (lan_ipaddr_0 to lan_ipaddr2) has the value "a", "b" and "c" respectively, which are merged with a dot (.)

    • Thus, the payload willl accommodatesfor the length of the merged string ("a.b.c."), by decrementing the padding value by 6 ⇒ 108 - 6 = 102

  • "XXXX" will be stored to the ra register

We are also able to control the s0 to s6 register values (note: "X"=0xA):

2. Forcing SIGSEGV (segmentation fault)

Now, we can attempt to execute the request on the binary without any break points

  • A SIGSEGV error is encountered immediately after the request is sent:

Notice the error message:

  • 0x58585858 -> "XXXX", which is the value we have written

HOORAY! This tells us that we are able to successfully control the return address!

5. Investigating ret2 methods + ROP

5.1 Search locations

  1. Main binary (httpd)

  • We will not search for gadgets within the main binary as its address space only consists of 3 bytes

  • This means that there will be 0x00 for the 1st byte (eg. 0x00123456), which represents a null byte, and constitutes a bad character when working with our payload

  1. Loaded libraries

  • /lib/ld-uClibc.so.0

  • /usr/lib/libnvram.so

  • /usr/lib/libshared.so

  • /usr/lib/libpolarssl.so

  • /usr/lib/libexpat.so

  • /lib/libgcc_s.so.1

  • /lib/libc.so.0

5.2 Common search patterns

  1. Load from stack to registers a0 and t9 (or even ra) followed by jump to t9

Take note that some of the gadgets may have delay slots, which will execute BEFORE the jump instruction

  • This gadget allows us to directly call a function at a specified address (eg. libc), with control of the first argument (relative stack position)

  1. Load from stack position (relative to the stack pointer) to a register (eg. s0)

  • Load value from a predefined offset (eg. 96) from the stack into the s0 register

    • the value in the resulting register s0 can for example, store a controlled memory address.

    • this address can be directly moved into a register (eg. t9), before a subsequent jump is executed (refer to next gadget)

  1. Load from register (we can control) to t9, followed by jump to t9

  • This gadget allows us to jump to custom memory address values stored in known registers (eg. s0)

  1. Load predictable memory address values (offset from known stack pointer value) to a register (eg. s0)

5.3 Exploit methods

Before we continue, I would like to discuss 2 possible methods that we can use to achieve our final goal of Remote-Code Execution (RCE):

  1. ROP + ret2libc:

  • ROP: utilize gadgets to load a0 register with string argument to execute

  • ret2libc: directly call the system function (libc), with a controlled argument (from ROP)

  1. ROP to shell-code

  • series of ROP gadgets to load relevant register values

  • end goal will be to eventually call a shell-code stored on a known offset on the stack

  • shell-code can contain a call to relevant libc libraries, etc.

Tools: ROPGadget, manual search (disassembly)

  • Other possible tool(s): ropper

  1. First, lets run the ROPGadget tool on the /lib/libc.so.0 binary, and save the output to a file:

We can use the general search command:

5.3.1 Method 1: ROP + ret2libc

The 1st method (ROP + ret2libc) will be preferred over the 2nd, as it more straightforward, and does not have to deal with some known issues: of shell-code encoding, cache incoherency, etc. (discussed below)

Gadget 1: Increment value of $sp, to allow string argument ($a0) to appear at the end of payload

Recall that the $a0 register will store our string argument to the system function. Thus, we need to put the string at the end of our payload, to prevent the null byte (bad character) from interfering

  • For this to happen, we need to increment the $sp to allow future gadgets to be able to use a higher value $sp to locate the string argument (to write to $a0)

  • As we are looking for an instruction pattern that increments the stack-pointer, it will likely appear at the end of a function during the return statement (as a delay slot). Thus, we have to include the search for a jump instruction before our $sp increment instruction (likely instruction jr $ra)

  • Expected patterns to match:

Important instructions:

  1. lw ra,72(sp): Load the word (4 bytes) value from $sp+72 to $ra

  • Allows us to control the memory address that is written to $ra

  • To jump to next gadget

  1. jr ra: Jump to address stored in $ra

  2. addiu sp,sp,80: Delay slot that increments $sp by 80

Gadget 2: Load known stack offset value into register $a0 , and subsequently jump to the system function

  • Expected pattern to match:

Note that we will jump to address 0x158fc instead of 0x15900, as 0x15900 will result in a null character (0x00) when added with libc base, which constitutes a bad character

Important instructions:

  1. addiu a0,sp,24: increment stack-pointer by 24, and move the calculated value into the $a0 register

  2. move t9,s0: move the value of $s0 into $t9

  • Recall that we are able to control the value of the $s0 register from the return from the validate_lan_ipaddr function

  • This will aid us in calling the final system function

  1. jalr t9: jump to address in $t9

5.3.2 Method 2: ROP to shell-code (PENDING UPDATE)

We attempt to look for ROP gadgets within the lib.so.0 binary

Gadget 1: addiu from stack offset to a controlled register, jump to 2nd gadget (from controlled register)

  • Perl regex for pattern (grep -P flag): addiu $sX, $sp, 0xAAAA

...

  • Starting address of found code portion: 0x00481d1c (get_device_settings)

...

Important instructions:

  1. ...

Gadget 2: move from a controlled register to register t9, followed by a jump to t9

  • Perl regex for pattern (grep -P flag): move $t9, $sX ;

...

  • Starting address: 0x00474b44 (set_wlan_radio_security)

...

Important instructions:

  1. ...

6. Exploit development (METHOD 1: ROP + ret2libc)

6.1 Finding address of system function

Recall from previous findingsarrow-up-right that our device employs partial ASLR that does not randomize the addresses in our system library functions (libc). Thus, we will be able to find the fixed and predictable address of the system function

  1. Retrieve absolute system function address

  1. Retrieve relative address offset from start of libc

  • For this approach, we have to manually calculate the absolute system address by adding together the start address of libc and the offset value retrieved

The relative offset value of the system function from start of libc is 0x29ea0

6.2 Crafting the payload

Let's recall information that we have gathered:

  1. ROP gadgets

  1. libc system function address

    • 0x2ad61ea0

6.2.1 Important considerations

a. Stack frame size of each functions involved

  • get_merge_ipaddr: 0x68

  • validate_lan_ipaddr: 0x88

b. Function context in which each gadget will be executing (crucial for relative stack offset calculation

  • Gadget 1's execution context: validate_lan_ipaddr function

  • Gadget 2's execution context: validate_lan_ipaddr function

c. Bad characters

  • Characters that must not be included in the payload

    • eg. gadget memory address, string arguments, etc.

  • As we are inserting into the buffer via the strcat function, the only bad character will most likely only be the null terminator value (0x00)

6.2.2 Relative offset values from the buffer (to overwrite)

Before we continue, let's recall the following values:

  • Offset between buffer and sp: 0x80

Notice that both of the gadget's execution context is in the validate_lan_ipaddr function. We will also need to account for the frame size of both the validate_lan_ipaddr and get_merge_ipaddr (0x68 and 0x88)

  • The addiu function at the end of the validate_lan_ipaddr function will execute as a delay slot following the jump (jr ra) to the 1st gadget

The following presents the calculated offset values from the buffer to reach each respective payload values:

a. 0x6c: return address of the validate_lan_ipaddr function ⇒ address of gadget 1 (ra)

b. -0x80+0x68+0x88+0x48 = 0xb8 address of gadget 2

  • value of 0x48 is taken gadget 1

c. -0x80+0x68+0x88+0x50+0x18 = 0xd8 string argument to the system function (a0)

  • value of 0x50 taken from gadget 1

  • value of 0x18 taken from gadget 2

  • value of 0x88 taken from delay slot after jump at the end of validate_lan_ipaddr

d. 0x6c-28 = 0x6c-0x1c = 0x50 address of the system function (s0)

Refer to the folloiwng diagram for an illustration of the stack layout, along with the relevant payload values:

NOTE: we have to account for 6 characterss: x.x.x. from the first 3 lan_ipaddr parameters

(1) General payload format

(2) Ping attacker machine

(3) Reverse shell

cURL

Breakpoint analysis at respective locations

We will attempt to set breakpoints at specified locations, to aid us in the analysis of relevant portions of the stack and to verify the flow of the exploit:

  1. Return line in validate_lan_ipaddr (0x428614)

  • address in ra -> 0x2ad6a044 (gadget 1)

  • address in s0 -> 0x2ad61ea0 (system)

  • Instructions at each of the gadget addresses should be what we expect

  • address in $sp+0x88+72 -> gadget 2 (0x2ad4d8fc)

    • value of 0x88 from delay slot at the end of validate_lan_ipaddr

    • value of 72 from increment value used in gadget 1

  1. Start of system function (0x2ad61ea0)

  • string value in $a0 -> shell command to execute

Recall that the system offset address from libc base is 0x29ea0

Remember that the system function is found within the libc library. Thus, we have to patch the break instructions in /lib/libc.so.0 itself, and update the httpd binary to use it:

Next, we have to update the library path:

Additional steps to make the exploit more "stealthy"

Notice that our current exploit will crash the httpd web server. This is because the final system function that is called in our ROP+ret2libc chain attempts to load a memory address value from a certain offset from the stack to update the ra register, and subsequently execute the instructions at the memory address

However, this address is likely to be a padding value used in our payload (bunch of As), which will trigger a SIGSEGV (Segmentation Fault), and simply crash

To better understand this, lets take a look at the last few instructions in the system (0x2ad61ea0) function:

We can ignore the rest of the lw instructions that loads to the other registers, and focus on the following 2 lines:

We now know that the relative stack offset of 64 (0x40) is used to load the ra register, which is subsequently used in the jump instruction

Thus, we could attempt to extend our payload to add a few additional gadgets to place our next memory address (likely somewhere in the original get_merge_ipaddr function).

Alternatively, we can employ a simple method to add an additional command in our payload to restore the httpd process. Refer to the post-exploitation section for more detailsarrow-up-right

7. Exploit development (METHOD 2: ROP to shell-code)

7.1 Understanding payload layout on the stack

...

7.2 Building the exploit

7.2.1 Custom "ping" code

7.2.2 Explanation of the custom "ping" code:

  1. .set noreoder

  • ...

7.3 Possible issues to look out for

  1. Encoding: certain instructions may contain characters that may signify a null byte or similar type of byte that terminates the string early

  • eg. .asciz, nop, etc.

  • in the case of the strcat function, this may cause the final payload to be truncated when written to the stack

  1. Cache incoherency

https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/Unix/EN%20-%20Exploiting%20Buffer%20Overflows%20on%20MIPS%20Architectures%20-%20Lyon%20Yang.pdfarrow-up-right

There exists 2 different caches, namely the Instruction Cache (I-Cache) and Data Cache (D-Cache), that stores instructions and data respectively. This may cause mismatches between data written by us to the stack (subsequently stored to the cache), and the instruction we want the CPU to execute

Thus, when the CPU attempts to access the shellcode instructions at the specified memory addresses from the I-Cache, it may retrieve "stale" data (old data from stack frames of previous function calls) which may cause a crash when executed

2.1 Overview of I-Cache and D-Cache:

I-Cache: Dynamic buffer of whatever value (from a particular memory address) that the CPU was recently told to execute

  • It operates on the assumption that code is static and does not monitor memory writes

D-Cache: Dynamic buffer of what have been written and retrieved from the stack

  • this is where the buffer overflow payload (shellcode) initially resides after being written to the stack

2.2 Overview of steps to defeat "cache incoherency" issues

Let's assume that that the "write-back" cache policy is used:

  • data is written only to the D-Cache and marked as "dirty"

  • it only reaches the RAM (actual stack memory) when the cache line is evicted or manually flushed

Possible scenario:

  1. I-Cache currently stores the instruction of the httpd binary that was loaded at the start of the execution

  2. Execute sleep() function

  • A few context switches will happen during the sleep period

  1. When the OS switches away from the httpd process to another process, the current data needs to be saved:

  • the kernel may perform a D-Cache clean, which takes all the "dirty" line on the D-Cache (including shellcode) and pushes them to the physical RAM

  1. When the OS switches back to the httpd process, the I-Cache will need to be invalidated (to prevent leak of instructions between processes)

  • the I-Cache will be marked as invalid

  1. When the PC attempts to execute the shellcode (final portion of the ROP chain), the CPU will look in the I-Cache

  • an I-Cache miss will happen, since it was previously marked as invalid

  • the CPU will look for the required instructions from the physical RAM, which will pull our shellcode into the I-Cache

...

Last updated