-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0c2233c
Showing
10 changed files
with
585 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.so | ||
payload/*.tgz | ||
__pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.