Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rssor committed Aug 7, 2023
0 parents commit 0c2233c
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.so
payload/*.tgz
__pycache__/
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CC := mipsel-linux-gnu-gcc-10
CCFLAGS := -fPIC -std=c11 -L./shim/deps/ -Ishim/ -ldl-2.23 -lc-2.23 -nostdlib -Os -s -msoft-float -mips1

.PHONY: clean

fs_xgspon_mod_release.tgz: payload/payload.tgz fs_xgspon_mod.py README.md
tar --owner==- --group=0 --numeric-owner -cvzf $@ payload/ fs_xgspon_mod.py README.md

payload/payload.tgz: rwdir/payload/libvos_shim.so rwdir/dangerous_payload.sh rwdir/stage0.sh Makefile
tar --owner=0 --group=0 --numeric-owner -cvzf $@ -C rwdir/ payload/ dangerous_payload.sh stage0.sh

rwdir/payload/libvos_shim.so: shim/shim.c shim/deps/libstub.so
$(CC) -shared -o $@ $< $(CCFLAGS) -Wl,--no-as-needed -lstub

shim/deps/libstub.so: shim/stub.c
$(CC) -shared -o $@ $< $(CCFLAGS) -Wl,-soname=/tmp/payload/libvos.so

clean:
rm payload/payload.tgz
rm shim/deps/libstub.so
rm rwdir/payload/libvos_shim.so
rm fs_xgspon_mod_release.tgz
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# FS.com XGS-ONU-25-20NI AT&T Modification Utility
This utility makes the necessary changes to the FS.com XGS-PON module to allow it to operate on AT&T's XGS-PON fiber offerings. It attempts to do so as safely as possible, reverting automatically to a stock state if it is ever power cycled twice in quick succession.

## Disclaimers
Bypass the provided BGW320 your own risk -- no matter how you go about it it is detectable and AT&T will find you if they go looking. This modification makes the minimum set of changes that I believe are necessary to get online but is intentionally trivially detectable. My rationale for this is that if anybody goes looking for these then chances are the devices are misbehaving and I'm not interested in making anybody's job harder than it needs to be.

Bypasses are detectable regardless of whether this device is used or any other (e.g. Azores D20 or WAS-110) -- the BGW320 hosts a variety of management services that won't be accessible from any customer bypassing provided equipment.

This particular FS.com device is actually a CIG XG-99S which is available under a variety of different brands. There is an entire family of devices running very similar firmwares that can likely also be modified in roughly the same way, though modifications to this utility would be required to support them. Of particular interest are the CIG XG-99M devices (best known as the FOX-222) which can be found for $30-$50 online as of writing and which I plan to look at in the near future.

While this modification attempts to be as safe _as possible_ it's much less safe than running an unmodified device. Although the device has two firmware slots they share the userdata partition that gets mounted to `/mnt/rwdir/`. During boot all CIG firmwares check for `/mnt/rwdir/setup.sh` and run it if it exists. This is done before the PON stack starts, so if anything goes wrong *it will never come online* and you will need UART access and micro-soldering skills to recover the device. For this reason, the modification disarms itself as the first action it takes during every boot -- that is the _only_ safety mechanism available.

## Requirements
- Python 3.6+
- The `install` command requires layer 2 adjacency to the stick (e.g. NO ROUTERS between you and the device, you have an address in `192.168.100.0/24`)
- The `GPONxxxxxxxx` serial of the stick (you generally need to ask your FS.com rep for this, it's not in included with the device as of August 2023)
- Stick running firmware `R4.4.20.018` or `R4.4.20.022` (other versions _may_ be safe)

This utility has been tested on:
- openSUSE Leap 15.4
- Windows 11
- macOS 13.5

This utility is built on:
- Ubuntu 22.04.2 LTS

## Features
- Ethernet UNI moved from slot 10 to slot 1 in MIB entities, thus becoming compatible with AT&T bridge pack configurations
- Disables traffic filtering when the Dot1X Port Extension Package (ME 290) is configured to filter all traffic
- Uses serial provided to mod instead of the serial in EEPROM to allow the device to revert cleanly if the fail-safe triggers
- Sets appropriate equipment id for NOKA/HUMA BGW320 devices depending on the provided serial
- Starts `dropbear` 2 minutes or so after device boot for more convenient administration (no idle timeout)

## Usage

By convention the documentation below uses `GPON227000fe` to refer to the serial of the FS.com device and `HUMA12ab34cd` to refer to the ONT ID of your AT&T BGW320 device found on the bottom label.

Skip to [Installation](#installation) and [Enabling Persistence](#enabling-persistence) if you just want to get online.

### Password Generation

Helper command to generate both sets of credentials from a provided serial. This is helpful if FS.com assigned you a sales rep that doesn't know how to get users online with these sticks and you're forced to brute force your stick credentials. By default only the telnet credentials are usable as nothing starts `dropbear` on stock firmwares.

```
./fs_xgspon_mod.py genpw GPON227000fe
Creds for FS.com XGS-PON stick with serial GPON227000fe:
Telnet: GPON227000fe / mbdu7pVX
SSH: ONTUSER / vjyKsHYsU2Aym5Nn
```

The HMAC key used to derive passwords appears to be different between the various OEM customers, so I don't think this works for anything except the FS.com sticks.

I suspect the serials take the form `GPONyymsssss` where `yy` is year, `m` is month, and `sssss` is number within run, but `yym` could be just batch numbers. Production runs seem to be very small so brute force won't take much time if you can guess roughly when your stick was manufactured. I have yet to see any serial with `sssss` greater than `000fe`.

### Telnet

Helper command so that you only need to remember the serial. Drops you immediately to an enabled `#ONT>` prompt.

```
./fs_xgspon_mod.py telnet GPON227000fe
enable
#ONT>
```

Automatically connects to the device, runs the `enable` command, and drops you do the ONT command prompt directly. If you want access to a shell, run `/s/s`. Ctrl-c to exit.

### Installation

Ensure that you're sitting adjacent to the stick on the network and that you have an address in the `192.168.100.0/24` subnet. The stick is at `192.168.100.1`. Ensure that your machine is configured to allow conncections on port `8172`. Activating the mod for a single boot only requires one command:

```
./fs_xgspon_mod.py install GPON227000fe HUMA12ab34cd
```

If installation fails chances are the stick isn't able to connect back to the machine you're running the utility from. It runs an HTTP server on port `8172` for the duration of installation that the stick needs to be able to connect to. I recommend punching a hole for all connections from `192.168.100.1` to port `8172`, at least when you need to run the installation command.

This will install everything necessary into `/mnt/rwdir/` on the device and set it up so that the next boot will take place with the mod active. By default every boot will delete the file necessary to support persistence so you'll need to run this command again if there's more than one power cycle.

After reboot the device will come up with the serial you provided as the second argument, and therefore you would use that new serial to connect via telnet. This also provides a quick way to determine what state your stick is in: see which serial needs to be passed to get dropped to a prompt, then you know if the mod is active or not.

After the stick has rebooted and you've confirmed that you can get online you may then want to move on to [enabling persistence](#enabling-persistence).

### Enabling Persistence

Persistence allows the modification to automatically re-arm itself for the next boot after a ~100 second timer expires.

```
./fs_xgspon_mod.py persist HUMA12ab34cd
```

Several minutes after the device has been booted with the mod active it becomes possible to enable persistent mode. The wait is implemented in order to attempt to make it impossible to enable persistence without proving the device can come online _enough_ to be recoverable. While this is implemented as a 100 second wait in the `libvos` shim, realistically it winds up being a bit over 2 minutes.

This will fail if the device wasn't booted with the mod active or if you haven't waited long enough since it booted with the mod active.

If you are able to connect to the device via SSH then this command should be functional. The shim starts dropbear at the same time as persistence becomes allowed.

### Fail-safe Recovery

In the event the failsafe triggers due to poorly timed power outages recovery is possible with only a few commands. Remember to use the serial in the device's EEPROM, either the one provided by FS.com reps if you didn't change it, or whatever you changed it to if you used the built-in commands to do so. Simply remove `/mnt/rwdir/disarmed` and reboot and the modification should be active again.

```
./fs_xgspon_mod.py telnet GPON227000fe
enable
#ONT> /s/s
/s/s
#ONT/system/shell>rm /mnt/rwdir/disarmed
rm /mnt/rwdir/disarmed
#ONT/system/shell>reboot
reboot
```

Wait a few minutes and it should come back online, responding to your AT&T NOKA/HUMA serial and getting you back online.

## Thanks To
- [miguemely](https://github.com/miguemely) - Initial testing, firmware dumps
- [YuukiJapanTech](https://github.com/YuukiJapanTech) - Assembling the spectacular resources at https://github.com/YuukiJapanTech/CA8271x/
- SipWannabe - Testing

## References
- https://github.com/YuukiJapanTech/CA8271x
- https://hack-gpon.org/xgs/ont-nokia-xs-010x-q/
216 changes: 216 additions & 0 deletions fs_xgspon_mod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler
from itertools import islice, chain
from threading import Thread
from telnetlib import Telnet
from pathlib import Path
import hmac

# if you add any new fields that need to be customed, you will
# also need to adjust PayloadHandler.do_GET()
CONFIG_TEMPLATE="""ETH10GESLOT=1
EepEqVendorID=GPON
EepEqSerialNumber=GPON12345678
EepVDSL2SerialNumber= VDSLSerialNumberGPON12345678
EepEqVersionID=GPON
EepEqID=XG-99S
"""

VENDOR_SPECIFIC = {
"HUMA": "iONT320500G",
"NOKA": "iONT320505G",
}

# 0x36 byte array, each byte of the output digest is used as an index % 0x36
# for the next output char; chars that can be easily confused are missing
output_base = "2345679abcdefghijkmnpqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ"
def extract_chars(key_base, serial, extract_len, total_len):
# the number of bytes being extracted is appended to the 15 byte fixed
# key to round it out to an even 0x10 byte hmac key
key = key_base + total_len.to_bytes(1, 'little')
digest = hmac.HMAC(key, serial.encode("utf-8"), "md5").digest()
return map(lambda x: output_base[x % len(output_base)], islice(digest, extract_len))

# when over 0x10 bytes are requested, the first 0x10 characters of output
# use a different key than all remaining bytes.
# suspect that the Nokia XS-010X-Q is exactly the same with the exception
# of the HMAC keys baked into libvos.so being different
keys = [b"\x01\x03\n\x10\x13\x05\x17d\xc8\x06\x14\x19\xb4\x9d\x05",
b"\x05\x11:`{\xfb\x0fC\\!\xbe\x86A2\x1c"]
def VOS_HmacMD5(serial, required_len):
it1 = extract_chars(keys[0], serial, min(0x10, required_len ), required_len)
it2 = extract_chars(keys[1], serial, max( 0, required_len - 0x10), required_len)
return ''.join(chain(it1, it2))

class CigTimeout(Exception):
pass

class CigTelnet(Telnet):
def __init__(self, serial):
serial = serial[:4].upper() + serial[4:].lower()

super().__init__("192.168.100.1", 23)
self._in_shell = False

self.read_until(b"Login as:")
self.write(f"{serial}\n".encode("utf-8"))
self.read_until(b":")
self.write(f"{VOS_HmacMD5(serial.upper(), 8)}\n".encode("utf-8"))
self.read_until(b"ONT>")
self.write(b"enable\n")

def sh_cmd(self, cmd, timeout=2):
if not self._in_shell:
self._in_shell = True
self.write(b"/s/s\n")
self.read_until(b"shell>", timeout)

if not cmd.endswith("\n"):
cmd += "\n"

self.write(cmd.encode("utf-8"))
res = self.read_until(b"shell>", timeout)
if b"shell>" not in res:
raise CigTimeout("CigTelnet command timed out")
return res.decode("utf-8")

class PayloadHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, serial=None, **kwargs):
self.vendor = serial[:4]
self.serial = serial[4:].lower()
self.eqid = VENDOR_SPECIFIC[self.vendor]
super().__init__(*args, directory=Path(__file__).parent / "payload", **kwargs)

def do_GET(self):
if self.path=="/config":
self.send_response(200)
self.send_header("Content-type", "html")
self.end_headers()
self.wfile.write(CONFIG_TEMPLATE \
.replace("GPON", self.vendor) \
.replace("12345678", self.serial) \
.replace("XG-99S", self.eqid) \
.encode("utf-8"))
else:
return super().do_GET()

def genpw(args):
serial = args.serial[:4].upper() + args.serial[4:].lower()

# the CigLogin binary relies on VOS_HmacMD5 (from libvos.so) to generate
# the telnet password from the raw serial with output length 8
# dropbearmulti, Console, and MecMgr all use VOS_HmacMD5 to generate
# the password in the form of {SERIAL}-ONTUSER with output length 16

print(f"Creds for FS.com XGS-PON stick with serial {serial}:")
print(f" Telnet: {serial} / {VOS_HmacMD5(serial.upper(), 8)}")
print(f" SSH: ONTUSER / {VOS_HmacMD5(serial + '-ONTUSER', 16)}")

def telnet(args):
with CigTelnet(args.serial) as tn:
tn.interact()

def install(args):
assert args.att_serial[:4] in VENDOR_SPECIFIC

class PayloadServer(HTTPServer):
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self, serial=args.att_serial)

print("Connecting via telnet...")
with CigTelnet(args.gpon_serial) as tn:
(addr, _) = tn.get_socket().getsockname()

with PayloadServer(("", 8172), PayloadHandler) as ps:
(_, port) = ps.socket.getsockname()
Thread(target=ps.serve_forever, daemon=True).start()
print(f"Webserver listening on {addr}:{port}")
print("If this doesn't complete almost immediately, ensure there is no router between you and the device!")

# ensure that if this goes Poorly we can power cycle our way out of it
tn.sh_cmd("touch /mnt/rwdir/disarmed")
tn.sh_cmd("[ -f /mnt/rwdir/setup.sh ] && rm /mnt/rwdir/setup.sh")

# prevent a bad update from incorrectly persisting based on a safe prior version that
# was persisting successfully by forcing people to re-enable it the long way
tn.sh_cmd("[ -f /mnt/rwdir/payload_auto_rearm ] && rm /mnt/rwdir/payload_auto_rearm")

try:
assert "100%" in tn.sh_cmd(f"wget -O - {addr}:{port}/config > /mnt/rwdir/payload.cfg", 10)
print("Payload configuration sent")

assert "100%" in tn.sh_cmd(f"wget -O - {addr}:{port}/payload.tgz | tar xvzf - -C /mnt/rwdir/", 10)
except (CigTimeout, AssertionError):
print(f"Error: Stick was not able to connect back and download payload! Check firewall!")
return

assert "stage0.sh" in tn.sh_cmd("ls /mnt/rwdir/")

tn.sh_cmd("ln -sf /mnt/rwdir/stage0.sh /mnt/rwdir/setup.sh")
tn.sh_cmd("[ -f /mnt/rwdir/disarmed ] && rm /mnt/rwdir/disarmed")
tn.sh_cmd("sync")

print("Payload extracted -- press enter to reboot!")

tn.write(b"reboot") # missing newline on purpose
tn.interact()

def persist(args):
print("Connecting via telnet...")
with CigTelnet(args.att_serial) as tn:
if "payload_postboot_dropbear" not in tn.sh_cmd("ls -l /tmp/"):
print("Persistence not allowed yet -- has it been 3+ minutes since boot?")
print("If it's been more than 3 minutes and the device is not listening for SSH connections, the mod is not active!")
return

tn.sh_cmd("[ -f /tmp/payload_postboot_dropbear ] && touch /mnt/rwdir/payload_auto_rearm")
tn.sh_cmd("[ -f /mnt/rwdir/disarmed ] && rm /mnt/rwdir/disarmed")
tn.sh_cmd("[ ! -f /mnt/rwdir/setup.sh ] && ln -s /mnt/rwdir/stage0.sh /mnt/rwdir/setup.sh")
tn.sh_cmd("sync")

print("Persistence now enabled -- as a fail safe, a power cycle between ~30 seconds and ~120 seconds after boot should restore to stock")

if __name__=="__main__":
import argparse

def parse_serial(serial):
serial = serial.upper()

if len(serial) != 12:
raise argparse.ArgumentError("serial must be 12 characters")

if serial[:4] not in ("GPON", "NOKA", "HUMA"):
raise argparse.ArgumentError("vendor must be one of GPON, NOKA, HUMA")

try:
numeric = serial[4:].strip()
assert len(numeric) == 8
int(numeric, 16)
except (ValueError, AssertionError):
raise argparse.ArgumentError("numeric portion of serial must be valid hexadecimal")

return serial

p = argparse.ArgumentParser()
s = p.add_subparsers()

parse_genpw = s.add_parser("genpw")
parse_genpw.add_argument("serial", type=parse_serial)
parse_genpw.set_defaults(func=genpw)

parse_telnet = s.add_parser("telnet")
parse_telnet.add_argument("serial", type=parse_serial)
parse_telnet.set_defaults(func=telnet)

parse_install = s.add_parser("install")
parse_install.add_argument("gpon_serial", type=parse_serial)
parse_install.add_argument("att_serial", type=parse_serial)
parse_install.set_defaults(func=install)

parse_persist = s.add_parser("persist")
parse_persist.add_argument("att_serial", type=parse_serial)
parse_persist.set_defaults(func=persist)

args = p.parse_args()
args.func(args)

7 changes: 7 additions & 0 deletions rwdir/dangerous_payload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

mkdir -p /tmp/payload/
touch /tmp/payload/libvos.so

mount -o bind /usr/lib/libvos.so /tmp/payload/libvos.so
mount -o bind /mnt/rwdir/payload/libvos_shim.so /usr/lib/libvos.so
18 changes: 18 additions & 0 deletions rwdir/stage0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh

BASEDIR=/mnt/rwdir

if [ ! -f $BASEDIR/disarmed ]; then
touch $BASEDIR/disarmed

# if we're not supposed to be persistent, nuke symlink
[ ! -f $BASEDIR/payload_auto_rearm ] && rm $BASEDIR/setup.sh

sync

$BASEDIR/dangerous_payload.sh
touch /tmp/payload_stage0
fi

# always return error so that we never halt /sbin/setup.sh
exit 1
1 change: 1 addition & 0 deletions shim/deps/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Copy libc-2.23.so and libdl-2.23.so from the device into this directory for linking purposes.
Loading

0 comments on commit 0c2233c

Please sign in to comment.