CVE-2025-60690

1. Ghidra

get_merge_ipaddr

Refined get_merge_ipaddr

...

Analysis

Keep in mind that we are working with the MIPS assembly for this binary. The insrtuctions 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

Example 1

Lets take the following disassembly + decompiled code snippet:

From the instruction:

We can see that the value in register v0 is moved to the variable param_2, and this variable is then passed as the 1st argument to the strcat function (as seen from the decompiled code)

We might think that the value in register v0 is used as the 1st argument to the strcat function. However, in reality, this register is actually moved to the a1 register instead, which relates to the 2nd argument passed to the strcat function

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)

Important notes from each section

  1. Disassembly

  • The a0 to an 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 get_merge_ipaddr functions

  • However, even though the 2nd argument (param_2) to the get_merge_ipaddr function is shown to be passed as the 1st argument to the strcat function, this is not actually the case

    • param_2 refers to a1, which instead relates to the 2nd argument passed to the nested strcat function call instead

Hence, the source of truth of argument passing should come from the a0, a1, an, etc. registers in the disassembly, rather than the decompiled code

2. gdb, gdbserver

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

2.1 Attempting to overwrite return address

Refer to the following disassembly snippet:

In an attempt to overwrite the return address to invoke an RCE, we first have to identify the address stored in the s1 (location we write) register, and the location where ra (return address to overwrite) is saved

To achieve this, we can can print the values of each register at specific instructions:

  1. ra can be found at address $sp+96 as seen from instruction 41f900

  2. s1 @ right before 41f994

  • The value stored in s1 is written to the a0 register (1st argument to strcat)

Calculate the distance between address stored in s1 and location where ra is saved:

3. Further enumeration

3.1 Ghidra

  1. Window -> Function call graph

  2. Window -> Function call tree

  3. Window -> Defined strings

    1. Right-click -> Data -> ... eg. TerminatedCString

    2. Right-click -> References -> show TerminatedCstring

  4. Right-click -> References -> show references to ...

  5. ...

3.2 Fuzzing

From UART console

  • Listen with gdbserver

  • It appears that sending a VALID POST request to /apply.cgi triggers a certain system reset (as seen from the UART console), with the following logs:

  • Only a VALID POST request parameter will trigger this behavior, while an invalid one (such as --data "action=Apple") will not:

Interesting observation

  • For the POST request with an invalid action parameter passed to the --data "action=xxxx" option, we get a response string:

  • Searching for the value "invalid" in the apply_cgi function (Ghidra), we find the following code snippet:

  • A more comprehensive code snippet:

  • Other valid values appears to be "Restore" and "Reboot"

Breakpoint does not work

  • There appears to be some form of protection from the device kernel or other feature that prevents breakpoints:

  • Using hardware breakpoint fails too:

...

...

4. Source code analysis + payload generation

4.1 Working payload 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

Explanation for 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=4

  • The value appears to specify the number of data parameters

    • Eg. value of 4 -> index up to lan_ipaddr_3

    • This is simply a speculation from what have been observed from the HTTP traffic when interacting with the web page

  • The following line represents the validate_lan_ipaddr function in Ghidra:

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

&variables

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

  • This CGI parameter has to be specified in order for our code flow to reach our intended function

  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 = WIN!

4.2 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

Normally, we would want to overwrite the return address of the current function. However, in our case, we have encountered the following issue:

  1. Buffer address is higher than the stack portion that is used to restore to s0-s7 and ra -> physically impossible to overwrite the address (values are written to higher memory addresses)

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

  3. Useful stack values/offsets discovered:

a. Buffer - $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

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

4.3 Working payload to overwrite & control ra register value (in validate_ip_addr function)

  1. Testing overwrite of ra register

For this section, assume a break point is set on the final return line in the validate_lan_ipaddr function:

  • 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 it 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 python3 payload simply accommodates for the length of the merged string ("a.b.c.")

⇒ 6 + 102 = 108

  • "XXXX" will be stored to the ra register

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

  1. 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/JOP

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

  • libc.so

  • libuClibc

  • libshared.so

  • libnvram.so

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 Search 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. JOP/ROP + ret2libc:

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

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

  1. JOP/ROP to shell-code

  • series of JOP/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.

5.3.1 Method 1: JOP/ROP + ret2libc

The 1st method (JOP/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)

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:

  1. Next, we can perform a search to look for the following pattern that aims to provide all the necessary functions in a single gadget:

  • due to the delay slot, the addiu instruction is expected to appear after the jalr

Command:

  • The -P option to grep allows us to specify a Perl regex pattern that provides the following features:

    • Allow arbitrary number of instructions in between our searched instructions

    • Handles newlines

From the results, we can see that we are able to find a back-to-back jalr and addiu instructions. However, the next closest instruction that modifies the $t9 register (preceding the 2 instructions mentioned above), deals with the $gp register, which is unreliable to overwrite

Thus, we have to find a solution that employs a JOP/ROP gadgets chain that gradually loads specific known register values, and works towards the final aim of being able to control the final a0 register and jump to a specified libc function

...

[....attempt to search in libc]

Gadget 1: Load zero value into a known register

Gadget 2: Load zero from known register to controlled offset on stack

...

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

  • Expected pattern to match:

...

Gadget 2: Load known stack offset value into register $a0

  • Expected pattern to match:

  • We will attempt to find a high stack offset value (end of payload) to control the $a0 register

  • 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

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

Important instructions:

a. ...

Gadget : ...

  • Expected pattern to match:

Important instructions:

a.

..

...

5.3.2 Method 2: JOP/ROP to shell-code

We attempt to look for JOP/ROP gadgets within the httpd binary. Given that we are able to find appropriate gadgets, we can entirely skip the need to even search within the other libraries such as libc

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:

a. addiu s1,sp,24

  • Stores the calculated value of sp+24 into the register s1

  • Since we are able to control this portion of the stack, we can control what is stored into s1, and eventually the jump address in the later steps

b. move t9,s3

  • Move register s3 into t9

c. jalr t9

  • Jump to instruction in t9

  • We know from previous analysis that we are also able to control register s3. Thus, this allows us to control the jump to gadget 2

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:

a. move t9, s1

  • From gadget 1, we are able to control the value at a calculated stack offset, where this particular stack address is stored in register s1

  • Thus, we are able to control the value in t9

b. jalr t9

  • Jump to t9, which stores our custom shell-code instructions

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

6.1 Finding address of 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

6.2 Crafting the payload

Let's take a look at the information that we have gathered:

  1. ROP/JOP gadgets

  1. libc system function address

    • 0x2ad61ea0

Important considerations

a. Stack frame size of each function 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 (0x20, 0x00, 0x3a, 0x0a, 0x3f)

  • Characters that must not be included in the payload

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

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. Thus, we will only need to account for the frame size of the get_merge_ipaddr (positive offset value of 0x68 from the sp)

  • the frame size from the validate_lan_ipaddr function will not need to be added since the particular addiu instruction will not be reached before the jump to our 1st gadget

a. 0x6c: return address of the get_merge_ipaddr function -> address of gadget 1 (ra)

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

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

  • value of 0x50 from gadget 1

  • value of 0x18 from gadget 2

  • take note of the frame size of the functions involved

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

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

...

~~~~

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

~~~~

(1) Python3 payload (ping attacker machine)

(2) Python3 payload (Netcat reverse shell)

Preparations on the attacker machine:

Breakdown of the payload commands:

$0a represents the line feed (newline) character in URL-encoding. This can be used to separate the commands (in shell).

Note that We can also use the shell operators ; , && or | too, but it may not be as reliable, as it may be filtered by the web server

...

cURL

Break-point analysis at respective locations

We will attempt to set break-points at specified locations, and analyse relevant portions of the stack, to verify the flow of the exploit:

  1. Return line in validate_lan_ipaddr (jr ra)

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

  • address in s0 -> 0x2ad4d8fc (system)

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

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

  1. Start of system function (0x2ad61ea0)

  • address in $sp+80+24 -> string argument to system function

...

Set breakpoint within libc

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

7.1 Understanding payload layout on the stack

...

7.2 Building the exploit

7.2.1 Custom "ping" code

Explanation of the custom "ping" code:

  1. .set noreoder

  • ...

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

...

Last updated