From d1ad48155e0a3b1ef1b3418e8b6b8b0c506b2ffd Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 15 Mar 2026 20:24:40 +0200 Subject: [PATCH] init --- 99-jibo-rcm.rules | 18 + README.md | 206 ++++ __pycache__/jibo_automod.cpython-313.pyc | Bin 0 -> 48769 bytes guide.md | 268 +++++ jibo_automod.bat | 49 + jibo_automod.py | 1182 ++++++++++++++++++++++ jibo_automod.sh | 61 ++ windows_setup.bat | 127 +++ 8 files changed, 1911 insertions(+) create mode 100644 99-jibo-rcm.rules create mode 100644 README.md create mode 100644 __pycache__/jibo_automod.cpython-313.pyc create mode 100755 guide.md create mode 100644 jibo_automod.bat create mode 100644 jibo_automod.py create mode 100755 jibo_automod.sh create mode 100644 windows_setup.bat diff --git a/99-jibo-rcm.rules b/99-jibo-rcm.rules new file mode 100644 index 0000000..d8efae1 --- /dev/null +++ b/99-jibo-rcm.rules @@ -0,0 +1,18 @@ +# Jibo RCM Mode USB Rules +# This allows non-root users to access Jibo in RCM mode +# +# Installation: +# sudo cp 99-jibo-rcm.rules /etc/udev/rules.d/ +# sudo udevadm control --reload-rules +# sudo udevadm trigger +# +# Then unplug and replug Jibo (or re-enter RCM mode) + +# Nvidia APX (Tegra RCM mode) - Jibo uses 0955:7740 +SUBSYSTEM=="usb", ATTR{idVendor}=="0955", ATTR{idProduct}=="7740", MODE="0666", GROUP="plugdev", TAG+="uaccess" + +# Jetson TK1 RCM mode (just in case) +SUBSYSTEM=="usb", ATTR{idVendor}=="0955", ATTR{idProduct}=="7140", MODE="0666", GROUP="plugdev", TAG+="uaccess" + +# Shield TK1 RCM mode (just in case) +SUBSYSTEM=="usb", ATTR{idVendor}=="0955", ATTR{idProduct}=="7f40", MODE="0666", GROUP="plugdev", TAG+="uaccess" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ec608b --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# Jibo Auto-Mod Tool + +**Automatically enable developer mode on Jibo robots** + +This tool automates the process of modding a Jibo robot to enable SSH access and developer mode. It works on both **Linux** and **Windows**. + +## ⚠️ Warning + +**USE AT YOUR OWN RISK!** This tool modifies your Jibo's internal storage. While the process is generally safe: + +- **Always keep backups** - the tool creates them automatically +- **Don't disconnect during write operations** - this could brick your Jibo +- **Calibration data is unique** - your backup contains data specific to YOUR Jibo + +## Quick Start + +### Linux + +```bash +# Make the script executable +chmod +x jibo_automod.sh + +# Run the tool +./jibo_automod.sh +``` + +### Windows + +1. Install [Python 3.8+](https://www.python.org/downloads/) (check "Add to PATH") +2. Install [MSYS2](https://www.msys2.org/) for build tools +3. Double-click `jibo_automod.bat` + +Or use WSL (Windows Subsystem for Linux) and follow Linux instructions. + +## Requirements + +### Linux +- Python 3.8+ +- build-essential (gcc, make) +- libusb-1.0-dev +- arm-none-eabi-gcc (ARM toolchain) +- ~20GB free disk space + +**Ubuntu/Debian:** +```bash +sudo apt update +sudo apt install build-essential libusb-1.0-0-dev git python3 \ + gcc-arm-none-eabi libnewlib-arm-none-eabi +``` + +**Arch/CachyOS:** +```bash +sudo pacman -S --needed base-devel libusb git python \ + arm-none-eabi-gcc arm-none-eabi-newlib +``` + +### Windows +- Python 3.8+ +- MSYS2 with MinGW-w64 toolchain +- Zadig (for USB driver installation) +- ~20GB free disk space + +## What Does It Do? + +1. **Builds Shofel** - Compiles the exploit tool from source +2. **Dumps eMMC** - Reads the entire 15GB storage (~2-4 hours) +3. **Modifies Partition** - Changes `/var/jibo/mode.json` from "normal" to "int-developer" +4. **Writes Back** - Updates only the modified partition +5. **Verifies** - Reads back to confirm the write was successful + +## Usage + +### Full Automatic Mod +```bash +./jibo_automod.sh +``` + +### Just Dump (no modification) +```bash +./jibo_automod.sh --dump-only -o my_jibo_backup.bin +``` + +### Use Existing Dump +```bash +./jibo_automod.sh --dump-path /path/to/existing_dump.bin +``` + +### Write Pre-Modified Partition +```bash +./jibo_automod.sh --write-partition var_modified.bin --start-sector 0x7E9022 +``` + +## Command Line Options + +| Option | Description | +|--------|-------------| +| `--dump-only` | Only dump eMMC, don't modify | +| `--dump-path FILE` | Use existing dump instead of dumping | +| `--output, -o FILE` | Output file for dump | +| `--start-sector HEX` | Sector for write operation (default: 0x7E9022) | +| `--force-dump` | Re-dump even if file exists | +| `--rebuild-shofel` | Force rebuild of exploit tool | +| `--skip-detection` | Skip USB device detection | +| `--no-verify` | Skip write verification | + +## Entering RCM Mode + +To mod your Jibo, you need to put it in RCM (Recovery Mode): + +1. **Locate the buttons:** + - RCM button: Small button under the base + - Reset/Power button: Standard power button + +2. **Enter RCM:** + - Hold the RCM button + - Press the reset/power button + - Release both when you see a red LED (no boot animation) + +3. **Verify:** + - On Linux: `lsusb` should show `NVIDIA Corp. APX` + - On Windows: Device Manager shows "APX" device + +## After Modding + +Once the tool completes successfully: + +1. Unplug Jibo from USB +2. Hold power button until red LED goes off +3. Power on Jibo normally +4. Wait for boot - you should see a **checkmark** instead of the eye animation +5. SSH into Jibo: + ```bash + ssh root@ + # Password: jibo + ``` + +## Troubleshooting + +### "Jibo not found in RCM mode" +- Make sure you're holding RCM button while pressing reset +- Try a different USB cable (data cables, not charge-only) +- On Windows, install WinUSB driver using Zadig + +### "Permission denied" on Linux +- Run with sudo: `sudo ./jibo_automod.sh` +- Or add udev rules for the Nvidia APX device + +### Build fails +- Make sure ARM toolchain is installed +- On Arch: `pacman -S arm-none-eabi-gcc arm-none-eabi-newlib` +- On Ubuntu: `apt install gcc-arm-none-eabi libnewlib-arm-none-eabi` + +### Dump crashes near 99% +- This is often OK - the last partition may be empty space +- Check if your dump file is ~14-15GB, that's probably complete + +### SSH connection refused +- Make sure Jibo shows checkmark on boot +- Verify you're using the correct IP address +- Try `ssh -v` for debug output + +## File Structure + +``` +JiboAutoMod/ +├── jibo_automod.py # Main tool (Python) +├── jibo_automod.sh # Linux launcher +├── jibo_automod.bat # Windows launcher +├── README.md # This file +├── guide.md # Original manual guide +├── Shofel/ # Shofel exploit source +│ ├── Makefile +│ ├── shofel2_t124 # Built executable +│ └── ... +└── jibo_work/ # Working directory (created) + ├── jibo_full_dump.bin + ├── var_partition.bin + └── var_partition_backup.bin +``` + +## Technical Details + +### Partition Layout +| # | Size | Purpose | +|---|------|---------| +| 1 | 1GB | System A | +| 2 | 1GB | System B | +| 3 | 50MB | Boot config | +| 4 | 2GB | Root filesystem | +| 5 | 500MB | /var (we modify this) | +| 6 | ~10GB | Data | + +### Mode Values +- `"normal"` - Standard Jibo operation +- `"int-developer"` - Developer mode (SSH enabled, services disabled) + +## Credits + +- Shofel exploit based on fail0verflow's Fusee Gelée +- Katherine Temkin's research on Tegra vulnerabilities +- devsparx for the T124 port +- The Jibo preservation community + +## License + +This tool is provided as-is for educational and preservation purposes. See individual component licenses in the Shofel directory. diff --git a/__pycache__/jibo_automod.cpython-313.pyc b/__pycache__/jibo_automod.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09ba9d5d6dc50ffed57b26e6007891d3db99d3a8 GIT binary patch literal 48769 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 O&HgX>HaExg()u5WSLN;i literal 0 HcmV?d00001 diff --git a/guide.md b/guide.md new file mode 100755 index 0000000..af3c29e --- /dev/null +++ b/guide.md @@ -0,0 +1,268 @@ +- - - + +Before starting ANY work , lets get you up to speed with the enviroment and what you will need ... + +1. Some sort of linux device with at least 32Gb storage, that could be a : Spare laptop , Raspberry Pi , a PC etc... + +> [!info] +> 1. for the sake of this guide we will be using my main computer running on CachyOS with the 6.19... kernel but you dont have to replicate my setup +> 2. This guide **requires** you have basic knowledge of the linux terminal + +>[!Warning] +>I Throw around the word shofel , and i do mean the shofel version **SPECIFICALLY** in devsparx repo (using the improvements brach for now) i show later on how to clone it , if anything should change i will try my best to update this guide as fast as possible , but do note our build of shofel will be undergoing some updates! + +> [!WARNING] +> # Please do inform yourself +> we are NOT liable for any damage caused to your device if you proceed with this guide , if you are not sure remotely on what youre doing i recommend either wait for the easier installation method , or find someone on the discord to guide you!!! (they arent liable for any damage either) + +2. A micro usb cable ==**that you know is reliable**== +3. Like 4-5 Hours +4. A Jibo **(THAT IS SETUP TO YOUR NETWORK, AND YOU KNOW ITS IP)** +5. An the mindset of (everything will be fine!) + +- - - +# Part 1 | Connecting your jibo + +So first , go ahead and plug jibo in to your host device that you are going to dump the firmware into using your usb cable + +Hold the RCM button and press the reset button (or the power button if yours is off), and you should see a red light at his face but he **wont boot normally** + +![[Jibo RCM.jpg]] + + +in the terminal were gonna execute `lsusb` to check for `NVIDIA Corp. APX` + +```shell + +kevin  lsusb +Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub +Bus 001 Device 002: ID 1532:023f Uhhh no +Bus 001 Device 003: ID 0a12:0001 Ltd Bluetooth Dongle (HCI mode why tho) +Bus 001 Device 004: ID 08bb:2902 cant see me :) +Bus 001 Device 005: ID 1bcf:08b8 nope not me, its the guy below +Bus 001 Device 007: ID 0955:7740 NVIDIA Corp. APX <<<<<< LOOK FOR THIS ENTRY!!!! +Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub +Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub +Bus 003 Device 002: ID 0644:800f TEAC Corp. US-144 yes i use a 144 deal with it +Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub +Bus 004 Device 002: ID 0951:1666 Kingston wanna be drive + +kevin  +``` + +- - - + +# Part 2 | Now before building shofel , get your packages ready + +```bash + +#for cachy / Arch users: + sudo pacman -S --needed base-devel libusb git python python-pip + +#for Ubuntu / debian users: + sudo apt update + sudo apt install build-essential libusb-1.0-0-dev git python3 python3-pip + +#for fedora its whatever that is (i chatgpt'd it) + sudo dnf groupinstall "Development Tools" + sudo dnf install libusb1-devel python3-pip + +``` + +Now time to clone & build shofel! + +change to your home directory and use `git` to clone the branch and then use `make` to build it! + +```bash + + cd ~ + + git clone -b improvements/IncreasedUSBReadWriteSpeed https://github.com/devsparx/ShofEL2-for-T124.git + + cd ShofEL2-for-T124/ + + make +# if it exits with error code 1 dont be alarmed , if you have a shofel executable in your directory , it compiled fine :) +``` + + +# Part 3 | Dumping your jibo! + +Now , to get that full image (roughly about ==15Gb==) +we will run our newly build `shofel` using the `EMMC_READ` function , starting from 0 `0x0` to 30480896 `0x1D60000` + +```bash + sudo ./shofel2_t124 EMMC_READ 0x0 0x1D60000 full_jibo_dump.bin +``` + +this may crash due to that im not sure how large exactly each jibos storage is (they should be the same from what i have seen in the community) + +But if it crashes like 98.9% or 99.9% in then you have most of the image cloned so you should be good (we can repair it later , or you might not need to since the last partition is basically empty space!) + +>[!Info] +>Techically you dont need a full dump , BUT! I havent confirmed enough yet that all jibos have the same stuff stored in the same sectors , but its good to to have a back up to calculate your own sectors **and to most importantly , read below :** + + +>[!WARNING] +>Make sure you make a backup of the filesystem ... this is basically YOUR jibo , and it also contains YOUR jibos calibration data that might not be able to get restored by someone else... so keep a backup of the `.bin` in case of an emergency + +# Part 4 | Modifying the /var partition + +Now that you have your image `.bin` ready and backed up its time to edit the /var partition!, thats Partition 5 at around 500mb + + +we can use `fdisk` to list the partitions in our bin + +```bash + fdisk -l jibo_full_dump.bin` +``` + + +```shell + +kevin  fdisk -l jibo_full_dump.bin +GPT PMBR size mismatch (30777343 != 30777341) will be corrected by write. +The backup GPT table is corrupt, but the primary appears OK, so that will be used. +The backup GPT table is not on the end of the device. +Disk jibo_full_dump.bin: 14,68 GiB, 15757999104 bytes, 30777342 sectors +Units: sectors of 1 * 512 = 512 bytes +Sector size (logical/physical): 512 bytes / 512 bytes +I/O size (minimum/optimal): 512 bytes / 512 bytes +Disklabel type: gpt +Disk identifier: 00042021-0408-4601-9DCC-A8C51255994F + +Device Start End Sectors Size Type +jibo_full_dump.bin1 34 2048033 2048000 1000M Microsoft basic data +jibo_full_dump.bin2 2048034 4096033 2048000 1000M Microsoft basic data +jibo_full_dump.bin3 4096034 4198433 102400 50M Microsoft basic data +jibo_full_dump.bin4 4198434 8294433 4096000 2G Microsoft basic data +jibo_full_dump.bin5 8294434 9318433 1024000 500M Mic<<< This is the one! +jibo_full_dump.bin6 9318434 30777310 21458877 10,2G Microsoft basic data + +kevin  + +``` + +now lets chop off the partition... + +Look for the line ending in `p5` or labeled as the 5th partition. You need two numbers from that line: **Start** and **End** + +> My Numbers: +> - **Start:** `8294434` +> - **End:** `9318433` + +To tell the computer exactly how much data to "chop off," we need the total count of sectors + +The formula is: +$$(End - Start) + 1 = Count$$ + +**My Math:** + $$(9318433 - 8294434) + 1 = 1,024,000$$ + +Write your result down,this is your **Count**. We will now extract the Partition +We use `dd` (Disk Destroyer... but here, it's the Disk Dumb joke... i couldnt think of something funny). + + `skip` = Your **Start** sector. + `count` = Your calculated **Count**. + +```Bash +dd if=jibo_full_dump.bin of=var_partition.bin skip=8294434 count=1024000 +``` + + +## Part 4.2 | Mounting as a loop device + +Now lets make a "loop" device and mount the `var_partition.bin` to it! + +```Bash +mkdir jibo_var +sudo mount -o loop var_partition.bin jibo_var/ +``` + +you should see it appear as a mounted disk drive on your system!, now you have to navigate to `jibo_var/jibo/mode.json` + +Open it with any text editor (like `vim` or `vscode` or notepad i dont care) + +Find the line: `"mode": "normal"` (its legit the only line in there) + +**Change it to:** `"mode": "int-developer"` +Save the file and exit + +>[!IMPORTANT] +> **Unmount** the partition to save the changes to the `.bin` file!!!! + +Now mode `int-developer` basically disables everything the robot runs (including firewall , so you can just ssh into it .. maybe in later guides you might be informed to change this value to something else , but currently its the simplest & easiest way to get root shell in the robot , and from there since you have root you can do everything you could by manually rewriting the entire dump) + +Anyway you are free to mount the rest of the fs partitions to parouse the file system or if you want live editing , i will make a guide on how to connect to ftp... +but if you have legit reached this point you really should know by now how to setup ftp yourself :| + +```Bash +sudo umount jibo_var +``` + + +# Part 5 | Writing our modified var partition to jibo! + +We are ready to put the modified chunk back into the robot. To do this, we must convert your **Start Sector** from a normal number (Decimal) to a computer number (Hexadecimal) because thats what shofel requires at this point of time. + + + +### 1. The Conversion + +Take your Start Sector (mine was `8294434`) and use an online converter or your Linux calculator to get the **Hex** value. + +**My Hex was** `0x7E9022` + +here is a quick calculator site i found on the web, +https://www.inchcalculator.com/decimal-to-hex-converter/ + + +If you dont know hex you can validate your calculation by punching in my value and see if it returns the same hex as mine + +### 2. The Flash + +time to run the write command, this sends your modified `/var` partition directly to that specific starting point on Jibo's memory. + + +```shell + + ./shofel2_t124 EMMC_WRITE var_partition.bin + + #my version example + #./shofel2_t124 EMMC_WRITE 0x7E9022 var_partition.bin +``` + + + +# Part 6 | Almost there! + +Actually who am i tricking , you typed the write command , it hopefully succeeded so why are we not done? + +well its done but i like to do a check myself to make sure its done correctly, SO + +were gonna read back that part of memory: + +```Bash +./shofel2_t124 EMMC_READ 0x7E9022 0xFA000 verify_var.bin +``` + +and were gonna compare their hashes If the two files are identical, the math was right. + +```Bash +md5sum var_partition.bin verify_var.bin +``` + +**If the strings of letters/numbers match exactly WERE DONE!!!! + +Go and unplug your jibo , and to be safe hold the power button until the red LED goes off +then power him on , and wait for it to boot normally + +what we expect is to not start his eye but show a big check mark on his display, if thats the case , go ssh to jibos IP + +```shell + ssh root@ + password: jibo +``` + + +And Boom , you are IN! diff --git a/jibo_automod.bat b/jibo_automod.bat new file mode 100644 index 0000000..4855707 --- /dev/null +++ b/jibo_automod.bat @@ -0,0 +1,49 @@ +@echo off +setlocal enabledelayedexpansion + +echo. +echo ============================================================ +echo JIBO AUTO-MOD TOOL - Windows Launcher +echo ============================================================ +echo. + +:: Check for Python +where python >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Python not found! Please install Python 3.8+ from: + echo https://www.python.org/downloads/ + echo. + echo Make sure to check "Add Python to PATH" during installation. + pause + exit /b 1 +) + +:: Check Python version +for /f "tokens=2 delims= " %%a in ('python --version 2^>^&1') do set PYVER=%%a +echo [INFO] Found Python %PYVER% + +:: Check if running as admin (recommended for USB access) +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo [WARNING] Not running as Administrator. + echo USB access may be limited. Consider right-clicking + echo and selecting "Run as administrator". + echo. +) + +:: Change to script directory +cd /d "%~dp0" + +:: Run the Python tool +echo [INFO] Starting Jibo Auto-Mod Tool... +echo. + +python jibo_automod.py %* + +if %errorlevel% neq 0 ( + echo. + echo [ERROR] Tool exited with error code %errorlevel% + pause +) + +endlocal diff --git a/jibo_automod.py b/jibo_automod.py new file mode 100644 index 0000000..99f84cb --- /dev/null +++ b/jibo_automod.py @@ -0,0 +1,1182 @@ +#!/usr/bin/env python3 +""" +Jibo Auto-Mod Tool +================== +Automatically mods a Jibo robot by: +1. Building the Shofel exploit (if needed) +2. Dumping the eMMC +3. Extracting and modifying the /var partition +4. Writing the modified partition back + +Supports: Linux and Windows (with WSL or MinGW) +""" + +import os +import sys +import json +import struct +import shutil +import hashlib +import platform +import subprocess +import argparse +from pathlib import Path +from typing import Optional, Tuple, List +from dataclasses import dataclass + +# ============================================================================ +# Configuration +# ============================================================================ + +SCRIPT_DIR = Path(__file__).parent.resolve() +SHOFEL_DIR = SCRIPT_DIR / "Shofel" +WORK_DIR = SCRIPT_DIR / "jibo_work" + +# eMMC dump parameters +EMMC_TOTAL_SECTORS = 0x1D60000 # Total sectors to dump (~15GB) +EMMC_SECTOR_SIZE = 512 + +# Colors for terminal output +class Colors: + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + MAGENTA = '\033[95m' + CYAN = '\033[96m' + RESET = '\033[0m' + BOLD = '\033[1m' + +# Disable colors on Windows unless using Windows Terminal +if platform.system() == "Windows" and "WT_SESSION" not in os.environ: + for attr in dir(Colors): + if not attr.startswith('_'): + setattr(Colors, attr, '') + + +@dataclass +class PartitionInfo: + """GPT partition information""" + number: int + start_sector: int + end_sector: int + size_sectors: int + name: str + + +# ============================================================================ +# Utilities +# ============================================================================ + +def print_banner(): + """Print the tool banner""" + print(f""" +{Colors.CYAN}╔═══════════════════════════════════════════════════════════════════╗ +║ {Colors.BOLD}JIBO AUTO-MOD TOOL{Colors.RESET}{Colors.CYAN} ║ +║ Automatic Developer Mode Enabler for Jibo Robots ║ +╚═══════════════════════════════════════════════════════════════════╝{Colors.RESET} +""") + + +def print_step(step: int, total: int, message: str): + """Print a step indicator""" + print(f"\n{Colors.BLUE}[{step}/{total}]{Colors.RESET} {Colors.BOLD}{message}{Colors.RESET}") + + +def print_success(message: str): + """Print a success message""" + print(f"{Colors.GREEN}✓ {message}{Colors.RESET}") + + +def print_warning(message: str): + """Print a warning message""" + print(f"{Colors.YELLOW}⚠ {message}{Colors.RESET}") + + +def print_error(message: str): + """Print an error message""" + print(f"{Colors.RED}✗ {message}{Colors.RESET}") + + +def print_info(message: str): + """Print an info message""" + print(f"{Colors.CYAN}ℹ {message}{Colors.RESET}") + + +def run_command(cmd: List[str], cwd: Optional[Path] = None, + capture_output: bool = False, check: bool = True, + sudo: bool = False) -> subprocess.CompletedProcess: + """Run a command and handle errors""" + if sudo and platform.system() == "Linux": + cmd = ["sudo"] + cmd + + try: + result = subprocess.run( + cmd, + cwd=cwd, + capture_output=capture_output, + text=True, + check=check + ) + return result + except subprocess.CalledProcessError as e: + print_error(f"Command failed: {' '.join(cmd)}") + if e.stderr: + print(e.stderr) + raise + + +def get_system_info() -> dict: + """Get system information""" + return { + "os": platform.system(), + "arch": platform.machine(), + "python_version": platform.python_version(), + "is_wsl": "microsoft" in platform.release().lower() if platform.system() == "Linux" else False + } + + +def check_root_or_sudo() -> bool: + """Check if running as root or with sudo capability""" + if platform.system() == "Windows": + import ctypes + return ctypes.windll.shell32.IsUserAnAdmin() != 0 + else: + return os.geteuid() == 0 or shutil.which("sudo") is not None + + +def _check_payloads_exist() -> bool: + """Quick check if critical payload binaries exist (for dependency checking)""" + critical_payloads = ["emmc_server.bin"] + return all((SHOFEL_DIR / p).exists() for p in critical_payloads) + + +# ============================================================================ +# Dependency Checking +# ============================================================================ + +def check_linux_dependencies() -> Tuple[bool, List[str], List[str]]: + """Check for required Linux dependencies""" + missing = [] + warnings = [] + + # Required tools for host build + required_tools = { + "gcc": "build-essential or base-devel", + "make": "build-essential or base-devel", + } + + # Optional tools (have fallbacks) + optional_tools = { + "lsusb": "usbutils (optional, used for device detection)", + "fdisk": "util-linux (optional, has Python fallback)", + } + + for tool, package in required_tools.items(): + if not shutil.which(tool): + missing.append(f"{tool} ({package})") + + for tool, package in optional_tools.items(): + if not shutil.which(tool): + warnings.append(f"{tool} ({package})") + + # Check ARM toolchain only if payloads are missing + if not _check_payloads_exist(): + if not shutil.which("arm-none-eabi-gcc"): + missing.append("arm-none-eabi-gcc (arm-none-eabi-gcc or arm-none-eabi-toolchain)") + + # Check for libusb + try: + result = subprocess.run( + ["pkg-config", "--exists", "libusb-1.0"], + capture_output=True + ) + if result.returncode != 0: + missing.append("libusb-1.0-dev or libusb1-devel") + except FileNotFoundError: + # pkg-config not found, try alternative check + if not Path("/usr/include/libusb-1.0").exists() and \ + not Path("/usr/local/include/libusb-1.0").exists(): + missing.append("libusb-1.0-dev or libusb1-devel") + + return len(missing) == 0, missing, warnings + + +def check_windows_dependencies() -> Tuple[bool, List[str], List[str]]: + """Check for required Windows dependencies""" + missing = [] + warnings = [] + + # Check for MinGW or MSYS2 + if not shutil.which("gcc") and not shutil.which("x86_64-w64-mingw32-gcc"): + missing.append("MinGW-w64 or MSYS2") + + # Check for ARM toolchain only if payloads missing + if not _check_payloads_exist(): + if not shutil.which("arm-none-eabi-gcc"): + missing.append("ARM GNU Toolchain (arm-none-eabi-gcc)") + + # Check for make + if not shutil.which("make") and not shutil.which("mingw32-make"): + missing.append("GNU Make") + + return len(missing) == 0, missing, warnings + + +def print_install_instructions(system: str, missing: List[str], warnings: List[str] = None): + """Print installation instructions for missing dependencies""" + if missing: + print_error("Missing dependencies:") + for dep in missing: + print(f" - {dep}") + + if warnings: + print_warning("Optional dependencies (have fallbacks):") + for warn in warnings: + print(f" - {warn}") + + if missing: + print(f"\n{Colors.BOLD}Installation instructions:{Colors.RESET}") + + if system == "Linux": + # Detect distro + distro = "unknown" + if Path("/etc/arch-release").exists(): + distro = "arch" + elif Path("/etc/debian_version").exists(): + distro = "debian" + elif Path("/etc/fedora-release").exists(): + distro = "fedora" + + if distro == "arch": + print(f""" + {Colors.CYAN}# Arch/CachyOS/Manjaro:{Colors.RESET} + sudo pacman -S --needed base-devel libusb git arm-none-eabi-gcc arm-none-eabi-newlib +""") + elif distro == "debian": + print(f""" + {Colors.CYAN}# Ubuntu/Debian:{Colors.RESET} + sudo apt update + sudo apt install build-essential libusb-1.0-0-dev git gcc-arm-none-eabi libnewlib-arm-none-eabi +""") + elif distro == "fedora": + print(f""" + {Colors.CYAN}# Fedora:{Colors.RESET} + sudo dnf groupinstall "Development Tools" + sudo dnf install libusb1-devel arm-none-eabi-gcc-cs arm-none-eabi-newlib +""") + else: + print(f""" + {Colors.CYAN}# Generic:{Colors.RESET} + Install: build-essential, libusb-1.0-dev, git, arm-none-eabi-gcc +""") + + elif system == "Windows": + print(f""" + {Colors.CYAN}Option 1 - MSYS2 (Recommended):{Colors.RESET} + 1. Download MSYS2 from https://www.msys2.org/ + 2. Open MSYS2 MINGW64 terminal and run: + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-libusb make + pacman -S mingw-w64-x86_64-arm-none-eabi-gcc + + {Colors.CYAN}Option 2 - WSL (Windows Subsystem for Linux):{Colors.RESET} + 1. Install WSL2: wsl --install + 2. Run this tool inside WSL with Linux dependencies +""") + + +# ============================================================================ +# Shofel Building +# ============================================================================ + +def check_shofel_built() -> bool: + """Check if shofel2_t124 is already built""" + shofel_exe = SHOFEL_DIR / "shofel2_t124" + if platform.system() == "Windows": + shofel_exe = SHOFEL_DIR / "shofel2_t124.exe" + return shofel_exe.exists() + + +def check_payloads_built() -> Tuple[bool, List[str]]: + """Check if ARM payload binaries exist""" + required_payloads = ["emmc_server.bin"] # This is the critical one for EMMC operations + optional_payloads = ["boot_bct.bin", "mem_dumper_usb_server.bin", "intermezzo.bin"] + + missing_required = [] + missing_optional = [] + + for payload in required_payloads: + if not (SHOFEL_DIR / payload).exists(): + missing_required.append(payload) + + for payload in optional_payloads: + if not (SHOFEL_DIR / payload).exists(): + missing_optional.append(payload) + + return len(missing_required) == 0, missing_required + missing_optional + + +def build_shofel(force_rebuild: bool = False) -> bool: + """Build the shofel2_t124 exploit tool""" + print_step(1, 6, "Building Shofel exploit tool") + + payloads_ok, missing_payloads = check_payloads_built() + + if check_shofel_built() and payloads_ok and not force_rebuild: + print_success("Shofel already built, skipping...") + return True + + print_info("Compiling shofel2_t124...") + + try: + # Only clean host build (preserves payload .bin files) + if force_rebuild: + run_command(["make", "clean"], cwd=SHOFEL_DIR, capture_output=True, check=False) + + # Build (Makefile will skip existing payload .bin files) + result = run_command(["make"], cwd=SHOFEL_DIR, capture_output=True, check=False) + + # Check if the main executable was built + if check_shofel_built(): + print_success("Host tool (shofel2_t124) built successfully!") + + # Check payloads again + payloads_ok, missing_payloads = check_payloads_built() + if not payloads_ok: + print_error("ARM payload binaries are missing!") + print_info("Missing files: " + ", ".join(missing_payloads)) + print() + print(f"{Colors.YELLOW}The ARM toolchain (arm-none-eabi-gcc) is required to build payloads.{Colors.RESET}") + print() + + # Detect distro and provide instructions + if Path("/etc/arch-release").exists(): + print(f" {Colors.CYAN}Arch/CachyOS:{Colors.RESET} sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib") + elif Path("/etc/debian_version").exists(): + print(f" {Colors.CYAN}Ubuntu/Debian:{Colors.RESET} sudo apt install gcc-arm-none-eabi libnewlib-arm-none-eabi") + elif Path("/etc/fedora-release").exists(): + print(f" {Colors.CYAN}Fedora:{Colors.RESET} sudo dnf install arm-none-eabi-gcc-cs arm-none-eabi-newlib") + else: + print(f" Install arm-none-eabi-gcc for your distribution") + + print() + print("After installing, run: make -C Shofel") + return False + else: + print_success("All payload binaries present!") + + return True + else: + print_error("Shofel build failed") + if result.stderr: + print(result.stderr) + return False + + except Exception as e: + print_error(f"Build failed: {e}") + return False + + +# ============================================================================ +# Jibo Detection +# ============================================================================ + +def detect_jibo_rcm() -> bool: + """Detect if Jibo is connected in RCM mode""" + print_info("Looking for Jibo in RCM mode (NVIDIA APX device)...") + + if platform.system() == "Linux": + # Try lsusb first + if shutil.which("lsusb"): + try: + result = run_command(["lsusb"], capture_output=True) + # Jibo uses 0955:7740 (NVIDIA APX) + if "0955:7740" in result.stdout: + print_success("Found Jibo in RCM mode!") + return True + else: + print_warning("Jibo not found in RCM mode") + print_info("Make sure to:") + print(" 1. Hold the RCM button (small button under the base)") + print(" 2. Press the reset/power button") + print(" 3. Release after seeing red LED (no boot animation)") + return False + except Exception as e: + print_error(f"lsusb failed: {e}") + + # Fallback: check /sys/bus/usb/devices + try: + usb_devices = Path("/sys/bus/usb/devices") + if usb_devices.exists(): + for device in usb_devices.iterdir(): + vendor_file = device / "idVendor" + product_file = device / "idProduct" + if vendor_file.exists() and product_file.exists(): + vendor = vendor_file.read_text().strip() + product = product_file.read_text().strip() + if vendor == "0955" and product == "7740": + print_success("Found Jibo in RCM mode! (via sysfs)") + return True + except Exception: + pass + + # Final fallback: assume user will connect it + print_warning("Cannot detect USB devices. Please ensure Jibo is in RCM mode.") + print_info("The tool will attempt to connect anyway.") + return True # Let shofel try + + elif platform.system() == "Windows": + # On Windows, we need to use different methods + print_warning("Windows USB detection - please ensure Zadig drivers are installed") + print_info("Run Zadig and install WinUSB driver for 'APX' device") + # Try to proceed anyway, shofel will detect it + return True + + return False + + +def wait_for_jibo_rcm(timeout: int = 60) -> bool: + """Wait for Jibo to be connected in RCM mode""" + import time + + print_info(f"Waiting for Jibo in RCM mode (timeout: {timeout}s)...") + print_info("Hold RCM button + press reset/power to enter RCM mode") + + start_time = time.time() + while time.time() - start_time < timeout: + if detect_jibo_rcm(): + return True + time.sleep(1) + sys.stdout.write(".") + sys.stdout.flush() + + print() + print_error("Timeout waiting for Jibo") + return False + + +# ============================================================================ +# GPT Partition Parsing +# ============================================================================ + +def parse_gpt_partitions(dump_path: Path) -> List[PartitionInfo]: + """Parse GPT partition table from dump file""" + partitions = [] + + with open(dump_path, "rb") as f: + # Read MBR (sector 0) - skip it + f.seek(512) + + # Read GPT header (sector 1) + gpt_header = f.read(512) + + # Check GPT signature + signature = gpt_header[:8] + if signature != b'EFI PART': + print_warning("GPT signature not found, trying fdisk parsing...") + return parse_partitions_fdisk(dump_path) + + # Parse GPT header + # Offset 72: Partition entries start LBA (8 bytes) + # Offset 80: Number of partition entries (4 bytes) + # Offset 84: Size of partition entry (4 bytes) + partition_entries_lba = struct.unpack(" List[PartitionInfo]: + """Parse partitions using fdisk (Linux fallback)""" + partitions = [] + + try: + result = run_command( + ["fdisk", "-l", str(dump_path)], + capture_output=True, + check=False + ) + + # Parse fdisk output + for line in result.stdout.split('\n'): + # Look for lines like: dump.bin1 34 2048033 2048000 1000M Microsoft basic data + if dump_path.name in line and not line.startswith("Disk"): + parts = line.split() + if len(parts) >= 4: + try: + # Extract partition number from name (e.g., dump.bin5 -> 5) + part_name = parts[0] + part_num = int(''.join(c for c in part_name if c.isdigit()) or '0') + + start = int(parts[1]) + end = int(parts[2]) + + partitions.append(PartitionInfo( + number=part_num, + start_sector=start, + end_sector=end, + size_sectors=end - start + 1, + name=f"partition{part_num}" + )) + except (ValueError, IndexError): + continue + + except Exception as e: + print_error(f"fdisk parsing failed: {e}") + + return partitions + + +def find_var_partition(partitions: List[PartitionInfo]) -> Optional[PartitionInfo]: + """Find the /var partition (partition 5, ~500MB)""" + # The var partition is typically partition 5 with ~500MB size + for part in partitions: + if part.number == 5: + # Verify it's roughly the right size (450-550 MB) + size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024) + if 400 < size_mb < 600: + return part + + # Fallback: look for any ~500MB partition + for part in partitions: + size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024) + if 450 < size_mb < 550: + print_warning(f"Using partition {part.number} as var (size matches)") + return part + + return None + + +# ============================================================================ +# Partition Extraction and Modification +# ============================================================================ + +def extract_partition(dump_path: Path, partition: PartitionInfo, output_path: Path) -> bool: + """Extract a partition from the dump""" + print_info(f"Extracting partition {partition.number} ({partition.size_sectors} sectors)...") + + try: + with open(dump_path, "rb") as src: + src.seek(partition.start_sector * EMMC_SECTOR_SIZE) + data = src.read(partition.size_sectors * EMMC_SECTOR_SIZE) + + with open(output_path, "wb") as dst: + dst.write(data) + + print_success(f"Partition extracted to {output_path}") + return True + + except Exception as e: + print_error(f"Extraction failed: {e}") + return False + + +def modify_mode_json_direct(partition_path: Path) -> bool: + """ + Modify mode.json directly in the partition image by searching for the pattern. + This works on both Linux and Windows without mounting. + """ + print_info("Searching for mode.json in partition...") + + try: + with open(partition_path, "r+b") as f: + data = f.read() + + # Search for the mode.json content pattern + # The file contains: {"mode": "normal"} or similar + patterns_to_find = [ + b'"mode": "normal"', + b'"mode":"normal"', + b'"mode" : "normal"', + ] + + replacement = b'"mode": "int-developer"' + + modified = False + for pattern in patterns_to_find: + if pattern in data: + # Calculate padding needed + pad_len = len(pattern) - len(replacement) + if pad_len > 0: + # Original is longer, we need to pad replacement + # Actually, we need to be careful here - let's make them same size + replacement_padded = replacement + b' ' * pad_len + elif pad_len < 0: + # Replacement is longer - this is a problem + # "normal" (6 chars) vs "int-developer" (13 chars) + # Original: "mode": "normal" (16 chars) + # New: "mode": "int-developer" (23 chars) + # We need to find the full JSON object and replace it + continue + else: + replacement_padded = replacement + + # Find offset + offset = data.find(pattern) + print_info(f"Found pattern at offset {offset} (0x{offset:x})") + + # This simple replacement won't work due to size difference + # We need a smarter approach + modified = True + break + + if not modified: + # Try finding the full JSON object + json_patterns = [ + (b'{"mode":"normal"}', b'{"mode":"int-developer"}'), + (b'{"mode": "normal"}', b'{"mode": "int-developer"}'), + (b'{ "mode": "normal" }', b'{"mode":"int-developer"}'), + ] + + for old_json, new_json in json_patterns: + if old_json in data: + offset = data.find(old_json) + print_info(f"Found mode.json at offset {offset} (0x{offset:x})") + + # Check if there's enough space (look at surrounding nulls/padding) + end_offset = offset + len(old_json) + + # The new JSON is longer, so we need to check if there's padding + size_diff = len(new_json) - len(old_json) + + if size_diff > 0: + # Check if the bytes after the old JSON are nulls or whitespace + following_bytes = data[end_offset:end_offset + size_diff] + if all(b == 0 or b == 0x20 or b == 0x0a for b in following_bytes): + # Safe to overwrite + new_data = data[:offset] + new_json + data[end_offset + size_diff:] + else: + # Not safe, need to use filesystem modification + print_warning("Cannot safely modify in-place, using filesystem mount") + return False + else: + # Replacement is shorter or same size, pad with nulls + padding = b'\x00' * (-size_diff) + new_data = data[:offset] + new_json + padding + data[end_offset:] + + # Write modified data + f.seek(0) + f.write(new_data) + print_success("mode.json modified successfully!") + return True + + print_warning("mode.json pattern not found, trying filesystem mount...") + return False + + except Exception as e: + print_error(f"Direct modification failed: {e}") + return False + + +def modify_partition_mounted(partition_path: Path) -> bool: + """Modify mode.json by mounting the partition (Linux only)""" + if platform.system() != "Linux": + print_error("Filesystem mounting only supported on Linux") + return False + + mount_point = WORK_DIR / "jibo_var_mount" + mount_point.mkdir(parents=True, exist_ok=True) + + try: + # Mount the partition + print_info(f"Mounting partition at {mount_point}...") + run_command( + ["mount", "-o", "loop", str(partition_path), str(mount_point)], + sudo=True + ) + + # Find and modify mode.json + mode_json_path = mount_point / "jibo" / "mode.json" + + if not mode_json_path.exists(): + # Try alternative paths + for alt_path in [ + mount_point / "mode.json", + mount_point / "etc" / "jibo" / "mode.json", + ]: + if alt_path.exists(): + mode_json_path = alt_path + break + + if mode_json_path.exists(): + print_info(f"Found mode.json at {mode_json_path}") + + # Read current content + with open(mode_json_path, "r") as f: + content = json.load(f) + + print_info(f"Current mode: {content.get('mode', 'unknown')}") + + # Modify + content["mode"] = "int-developer" + + # Write back (need sudo) + temp_json = WORK_DIR / "mode_temp.json" + with open(temp_json, "w") as f: + json.dump(content, f) + + run_command( + ["cp", str(temp_json), str(mode_json_path)], + sudo=True + ) + + print_success("mode.json modified to 'int-developer'") + + else: + print_error(f"mode.json not found in mounted partition") + print_info("Listing mount contents:") + run_command(["ls", "-la", str(mount_point)], sudo=True) + return False + + return True + + except Exception as e: + print_error(f"Mount/modify failed: {e}") + return False + + finally: + # Always unmount + try: + run_command(["umount", str(mount_point)], sudo=True, check=False) + except: + pass + + +def modify_var_partition(partition_path: Path) -> bool: + """Modify the var partition to enable developer mode""" + print_step(4, 6, "Modifying var partition") + + # Try direct modification first (works on all platforms) + if modify_mode_json_direct(partition_path): + return True + + # Fall back to mounting (Linux only) + if platform.system() == "Linux": + return modify_partition_mounted(partition_path) + + print_error("Could not modify partition") + return False + + +# ============================================================================ +# eMMC Operations +# ============================================================================ + +def get_shofel_path() -> Path: + """Get the path to shofel2_t124 executable""" + if platform.system() == "Windows": + return SHOFEL_DIR / "shofel2_t124.exe" + return SHOFEL_DIR / "shofel2_t124" + + +def dump_emmc(output_path: Path, start_sector: int = 0, num_sectors: int = EMMC_TOTAL_SECTORS) -> bool: + """Dump the Jibo eMMC to a file""" + print_step(2, 6, "Dumping Jibo eMMC") + + shofel = get_shofel_path() + if not shofel.exists(): + print_error("shofel2_t124 not found. Please build it first.") + return False + + print_info(f"Dumping {num_sectors} sectors ({num_sectors * 512 / 1024 / 1024 / 1024:.1f} GB)...") + print_info("This will take approximately 2-4 hours. Please be patient.") + print_warning("DO NOT disconnect Jibo during this process!") + + try: + cmd = [ + str(shofel), + "EMMC_READ", + f"0x{start_sector:x}", + f"0x{num_sectors:x}", + str(output_path) + ] + + # Run with sudo on Linux + if platform.system() == "Linux": + cmd = ["sudo"] + cmd + + subprocess.run(cmd, cwd=SHOFEL_DIR, check=True) + + if output_path.exists(): + size_gb = output_path.stat().st_size / (1024 * 1024 * 1024) + print_success(f"eMMC dump complete: {output_path} ({size_gb:.2f} GB)") + return True + else: + print_error("Dump file not created") + return False + + except subprocess.CalledProcessError as e: + print_error(f"eMMC dump failed: {e}") + return False + except KeyboardInterrupt: + print_warning("Dump interrupted by user") + return False + + +def write_partition_to_emmc(partition_path: Path, start_sector: int) -> bool: + """Write a partition back to the Jibo eMMC""" + print_step(5, 6, "Writing modified partition to Jibo") + + shofel = get_shofel_path() + if not shofel.exists(): + print_error("shofel2_t124 not found") + return False + + print_info(f"Writing to sector 0x{start_sector:x}...") + print_warning("DO NOT disconnect Jibo during this process!") + + try: + cmd = [ + str(shofel), + "EMMC_WRITE", + f"0x{start_sector:x}", + str(partition_path) + ] + + if platform.system() == "Linux": + cmd = ["sudo"] + cmd + + subprocess.run(cmd, cwd=SHOFEL_DIR, check=True) + + print_success("Partition written successfully!") + return True + + except subprocess.CalledProcessError as e: + print_error(f"Write failed: {e}") + return False + + +def verify_write(partition_path: Path, start_sector: int, num_sectors: int) -> bool: + """Verify the write by reading back and comparing hashes""" + print_step(6, 6, "Verifying write") + + shofel = get_shofel_path() + verify_path = WORK_DIR / "verify_partition.bin" + + print_info("Reading back partition for verification...") + + try: + cmd = [ + str(shofel), + "EMMC_READ", + f"0x{start_sector:x}", + f"0x{num_sectors:x}", + str(verify_path) + ] + + if platform.system() == "Linux": + cmd = ["sudo"] + cmd + + subprocess.run(cmd, cwd=SHOFEL_DIR, check=True) + + # Compare hashes + with open(partition_path, "rb") as f: + original_hash = hashlib.md5(f.read()).hexdigest() + + with open(verify_path, "rb") as f: + verify_hash = hashlib.md5(f.read()).hexdigest() + + if original_hash == verify_hash: + print_success(f"Verification passed! Hash: {original_hash}") + return True + else: + print_error("Verification FAILED - hashes don't match!") + print(f" Original: {original_hash}") + print(f" Readback: {verify_hash}") + return False + + except Exception as e: + print_error(f"Verification failed: {e}") + return False + + +# ============================================================================ +# Main Workflow +# ============================================================================ + +def run_full_mod(args) -> bool: + """Run the complete modding workflow""" + print_banner() + + # Check system + sys_info = get_system_info() + print_info(f"System: {sys_info['os']} ({sys_info['arch']})") + + if sys_info['is_wsl']: + print_info("Running in WSL - USB passthrough may require additional setup") + + # Check dependencies + print_step(0, 6, "Checking dependencies") + + if sys_info['os'] == "Linux": + deps_ok, missing, warnings = check_linux_dependencies() + else: + deps_ok, missing, warnings = check_windows_dependencies() + + if not deps_ok: + print_install_instructions(sys_info['os'], missing, warnings) + return False + + if warnings: + for warn in warnings: + print_warning(f"Optional: {warn}") + + print_success("All required dependencies found!") + + # Create work directory + WORK_DIR.mkdir(parents=True, exist_ok=True) + + # Build Shofel + if not build_shofel(force_rebuild=args.rebuild_shofel): + return False + + # Detect or wait for Jibo + if not args.skip_detection: + if not detect_jibo_rcm(): + if not wait_for_jibo_rcm(timeout=120): + return False + + # Paths + dump_path = WORK_DIR / "jibo_full_dump.bin" + var_partition_path = WORK_DIR / "var_partition.bin" + backup_var_path = WORK_DIR / "var_partition_backup.bin" + + # Dump eMMC (or use existing dump) + if args.dump_path: + dump_path = Path(args.dump_path) + if not dump_path.exists(): + print_error(f"Specified dump file not found: {dump_path}") + return False + print_info(f"Using existing dump: {dump_path}") + elif dump_path.exists() and not args.force_dump: + print_info(f"Using existing dump: {dump_path}") + print_info("Use --force-dump to re-dump") + else: + if not dump_emmc(dump_path): + return False + + # Parse partitions + print_step(3, 6, "Analyzing partition table") + + partitions = parse_gpt_partitions(dump_path) + if not partitions: + print_error("No partitions found in dump") + return False + + print_info("Partitions found:") + for part in partitions: + size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024) + print(f" {part.number}: sectors {part.start_sector}-{part.end_sector} ({size_mb:.1f} MB) - {part.name}") + + # Find var partition + var_partition = find_var_partition(partitions) + if not var_partition: + print_error("Could not identify /var partition") + return False + + print_success(f"Identified /var partition: partition {var_partition.number}") + + # Extract var partition + if not extract_partition(dump_path, var_partition, var_partition_path): + return False + + # Create backup + shutil.copy(var_partition_path, backup_var_path) + print_info(f"Backup created: {backup_var_path}") + + # Modify partition + if not modify_var_partition(var_partition_path): + return False + + # Check if Jibo still connected (may need to re-enter RCM) + if not args.skip_detection: + print_info("Please ensure Jibo is still in RCM mode") + print_info("If Jibo rebooted, re-enter RCM mode now") + if not wait_for_jibo_rcm(timeout=60): + print_warning("Continuing anyway...") + + # Write modified partition + if not write_partition_to_emmc(var_partition_path, var_partition.start_sector): + return False + + # Verify + if args.verify: + if not verify_write(var_partition_path, var_partition.start_sector, var_partition.size_sectors): + print_warning("Verification failed, but write may still be successful") + + # Done! + print(f""" +{Colors.GREEN}╔═══════════════════════════════════════════════════════════════════╗ +║ {Colors.BOLD}MODDING COMPLETE!{Colors.RESET}{Colors.GREEN} ║ +╚═══════════════════════════════════════════════════════════════════╝{Colors.RESET} + +{Colors.BOLD}Next steps:{Colors.RESET} +1. Unplug Jibo from USB +2. Hold power button until red LED goes off +3. Power on Jibo normally +4. Wait for boot - you should see a checkmark instead of the eye +5. SSH into Jibo: + {Colors.CYAN}ssh root@{Colors.RESET} + Password: {Colors.YELLOW}jibo{Colors.RESET} + +{Colors.BOLD}Your backup is saved at:{Colors.RESET} +{backup_var_path} + +{Colors.YELLOW}Keep this backup safe - it contains your Jibo's calibration data!{Colors.RESET} +""") + + return True + + +def run_dump_only(args) -> bool: + """Only dump the eMMC without modding""" + print_banner() + print_info("Running in dump-only mode") + + WORK_DIR.mkdir(parents=True, exist_ok=True) + + # Build Shofel + if not build_shofel(force_rebuild=args.rebuild_shofel): + return False + + # Wait for Jibo + if not args.skip_detection: + if not wait_for_jibo_rcm(timeout=120): + return False + + output_path = Path(args.output) if args.output else WORK_DIR / "jibo_full_dump.bin" + return dump_emmc(output_path) + + +def run_write_only(args) -> bool: + """Write a pre-modified partition to Jibo""" + print_banner() + print_info("Running in write-only mode") + + partition_path = Path(args.partition) + if not partition_path.exists(): + print_error(f"Partition file not found: {partition_path}") + return False + + # Build Shofel if needed + if not build_shofel(): + return False + + # Wait for Jibo + if not args.skip_detection: + if not wait_for_jibo_rcm(timeout=120): + return False + + return write_partition_to_emmc(partition_path, args.start_sector) + + +# ============================================================================ +# CLI +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + description="Jibo Auto-Mod Tool - Automatically enable developer mode on Jibo robots", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s # Run full modding workflow + %(prog)s --dump-only # Only dump eMMC + %(prog)s --write-partition var.bin --start-sector 0x7E9022 + %(prog)s --dump-path existing_dump.bin # Use existing dump + """ + ) + + # Operation modes + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument("--dump-only", action="store_true", + help="Only dump the eMMC without modifying") + mode_group.add_argument("--write-partition", metavar="FILE", + help="Write a partition file to Jibo (requires --start-sector)") + + # Options + parser.add_argument("--dump-path", metavar="FILE", + help="Use existing dump file instead of dumping") + parser.add_argument("--output", "-o", metavar="FILE", + help="Output file for dump (default: jibo_work/jibo_full_dump.bin)") + parser.add_argument("--start-sector", type=lambda x: int(x, 0), default=0x7E9022, + help="Start sector for write operation (hex, default: 0x7E9022)") + parser.add_argument("--force-dump", action="store_true", + help="Force re-dump even if dump file exists") + parser.add_argument("--rebuild-shofel", action="store_true", + help="Force rebuild of Shofel exploit") + parser.add_argument("--skip-detection", action="store_true", + help="Skip USB device detection (useful for debugging)") + parser.add_argument("--verify", action="store_true", default=True, + help="Verify write by reading back (default: True)") + parser.add_argument("--no-verify", action="store_false", dest="verify", + help="Skip write verification") + + args = parser.parse_args() + + # Validate arguments + if args.write_partition and not args.start_sector: + parser.error("--write-partition requires --start-sector") + + # Run appropriate mode + try: + if args.dump_only: + success = run_dump_only(args) + elif args.write_partition: + args.partition = args.write_partition + success = run_write_only(args) + else: + success = run_full_mod(args) + + sys.exit(0 if success else 1) + + except KeyboardInterrupt: + print("\n") + print_warning("Operation cancelled by user") + sys.exit(130) + except Exception as e: + print_error(f"Unexpected error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/jibo_automod.sh b/jibo_automod.sh new file mode 100755 index 0000000..2cc2fbd --- /dev/null +++ b/jibo_automod.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Jibo Auto-Mod Tool - Linux/macOS Launcher +# This script checks dependencies and runs the auto-mod tool + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +echo "" +echo "============================================================" +echo " JIBO AUTO-MOD TOOL - Linux Launcher" +echo "============================================================" +echo "" + +# Check for Python 3 +if ! command -v python3 &> /dev/null; then + echo "[ERROR] Python 3 not found!" + echo " Install with: sudo apt install python3" + exit 1 +fi + +PYVER=$(python3 --version) +echo "[INFO] Found $PYVER" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "[INFO] Not running as root. sudo will be used when needed." + # Check if we can sudo + if ! sudo -v &> /dev/null; then + echo "[WARNING] Cannot use sudo. USB operations may fail." + fi +fi + +# Check for required tools +check_tool() { + if ! command -v "$1" &> /dev/null; then + echo "[WARNING] $1 not found. Install: $2" + return 1 + fi + return 0 +} + +MISSING=0 +check_tool "lsusb" "sudo apt install usbutils" || MISSING=1 +check_tool "make" "sudo apt install build-essential" || MISSING=1 +check_tool "gcc" "sudo apt install build-essential" || MISSING=1 +check_tool "arm-none-eabi-gcc" "sudo apt install gcc-arm-none-eabi" || MISSING=1 + +if [ $MISSING -eq 1 ]; then + echo "" + echo "[WARNING] Some dependencies are missing. The tool will try to continue" + echo " but some features may not work." + echo "" +fi + +# Run the tool +echo "[INFO] Starting Jibo Auto-Mod Tool..." +echo "" + +python3 jibo_automod.py "$@" diff --git a/windows_setup.bat b/windows_setup.bat new file mode 100644 index 0000000..9094910 --- /dev/null +++ b/windows_setup.bat @@ -0,0 +1,127 @@ +@echo off +setlocal enabledelayedexpansion + +echo. +echo ============================================================ +echo JIBO AUTO-MOD - Windows Development Environment Setup +echo ============================================================ +echo. + +:: Check if running as admin +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo [WARNING] Not running as Administrator. + echo Some installations may require admin rights. + echo. +) + +echo This script will help you set up the development environment +echo for building and running the Jibo Auto-Mod tool on Windows. +echo. + +:: Check for Python +echo [1/4] Checking Python installation... +where python >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Python not found! + echo. + echo Please install Python 3.8+ from: + echo https://www.python.org/downloads/ + echo. + echo IMPORTANT: Check "Add Python to PATH" during installation! + echo. + echo After installing Python, run this script again. + pause + exit /b 1 +) else ( + for /f "tokens=2 delims= " %%a in ('python --version 2^>^&1') do set PYVER=%%a + echo [OK] Python !PYVER! found +) + +:: Check for MSYS2 +echo. +echo [2/4] Checking MSYS2 installation... +if exist "C:\msys64\usr\bin\bash.exe" ( + echo [OK] MSYS2 found at C:\msys64 + set MSYS2_PATH=C:\msys64 +) else if exist "C:\msys32\usr\bin\bash.exe" ( + echo [OK] MSYS2 found at C:\msys32 + set MSYS2_PATH=C:\msys32 +) else ( + echo [ERROR] MSYS2 not found! + echo. + echo Please install MSYS2 from: + echo https://www.msys2.org/ + echo. + echo After installing: + echo 1. Open "MSYS2 MINGW64" from Start Menu + echo 2. Run: pacman -Syu + echo 3. Run this script again + pause + exit /b 1 +) + +:: Install MSYS2 packages +echo. +echo [3/4] Installing required packages via MSYS2... +echo. +echo This will install: gcc, make, libusb, arm-none-eabi-gcc +echo. + +set MSYS2_BASH=%MSYS2_PATH%\usr\bin\bash.exe + +:: Create a temporary script for MSYS2 +set TEMP_SCRIPT=%TEMP%\jibo_setup.sh +echo #!/bin/bash > "%TEMP_SCRIPT%" +echo echo "Updating package database..." >> "%TEMP_SCRIPT%" +echo pacman -Sy --noconfirm >> "%TEMP_SCRIPT%" +echo echo "Installing MinGW toolchain..." >> "%TEMP_SCRIPT%" +echo pacman -S --noconfirm --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-make >> "%TEMP_SCRIPT%" +echo echo "Installing libusb..." >> "%TEMP_SCRIPT%" +echo pacman -S --noconfirm --needed mingw-w64-x86_64-libusb >> "%TEMP_SCRIPT%" +echo echo "Installing ARM toolchain..." >> "%TEMP_SCRIPT%" +echo pacman -S --noconfirm --needed mingw-w64-x86_64-arm-none-eabi-gcc >> "%TEMP_SCRIPT%" +echo echo "Done!" >> "%TEMP_SCRIPT%" + +"%MSYS2_BASH%" -l -c "source '%TEMP_SCRIPT%'" +del "%TEMP_SCRIPT%" + +:: Install Zadig info +echo. +echo [4/4] USB Driver Setup (Zadig)... +echo. +echo To communicate with Jibo in RCM mode, you need the WinUSB driver. +echo. +echo Steps: +echo 1. Download Zadig from: https://zadig.akeo.ie/ +echo 2. Put Jibo in RCM mode (hold RCM + press power) +echo 3. Run Zadig +echo 4. Options ^> List All Devices +echo 5. Select "APX" from the dropdown +echo 6. Select "WinUSB" as the target driver +echo 7. Click "Replace Driver" +echo. + +:: Final instructions +echo. +echo ============================================================ +echo SETUP COMPLETE +echo ============================================================ +echo. +echo To build Shofel and run the mod tool: +echo. +echo 1. Open "MSYS2 MINGW64" from Start Menu +echo 2. Navigate to this folder: +echo cd /c/path/to/JiboAutoMod +echo 3. Build Shofel: +echo cd Shofel ^&^& make +echo 4. Run the mod tool: +echo python ../jibo_automod.py +echo. +echo Or use the batch launcher: +echo jibo_automod.bat +echo. +echo NOTE: Some USB operations may require running as Administrator. +echo. +pause +endlocal