Skip to content

Commit 0c2233c

Browse files
committed
Initial commit
0 parents  commit 0c2233c

File tree

10 files changed

+585
-0
lines changed

10 files changed

+585
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.so
2+
payload/*.tgz
3+
__pycache__/

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
CC := mipsel-linux-gnu-gcc-10
2+
CCFLAGS := -fPIC -std=c11 -L./shim/deps/ -Ishim/ -ldl-2.23 -lc-2.23 -nostdlib -Os -s -msoft-float -mips1
3+
4+
.PHONY: clean
5+
6+
fs_xgspon_mod_release.tgz: payload/payload.tgz fs_xgspon_mod.py README.md
7+
tar --owner==- --group=0 --numeric-owner -cvzf $@ payload/ fs_xgspon_mod.py README.md
8+
9+
payload/payload.tgz: rwdir/payload/libvos_shim.so rwdir/dangerous_payload.sh rwdir/stage0.sh Makefile
10+
tar --owner=0 --group=0 --numeric-owner -cvzf $@ -C rwdir/ payload/ dangerous_payload.sh stage0.sh
11+
12+
rwdir/payload/libvos_shim.so: shim/shim.c shim/deps/libstub.so
13+
$(CC) -shared -o $@ $< $(CCFLAGS) -Wl,--no-as-needed -lstub
14+
15+
shim/deps/libstub.so: shim/stub.c
16+
$(CC) -shared -o $@ $< $(CCFLAGS) -Wl,-soname=/tmp/payload/libvos.so
17+
18+
clean:
19+
rm payload/payload.tgz
20+
rm shim/deps/libstub.so
21+
rm rwdir/payload/libvos_shim.so
22+
rm fs_xgspon_mod_release.tgz

README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# FS.com XGS-ONU-25-20NI AT&T Modification Utility
2+
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.
3+
4+
## Disclaimers
5+
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.
6+
7+
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.
8+
9+
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.
10+
11+
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.
12+
13+
## Requirements
14+
- Python 3.6+
15+
- 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`)
16+
- 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)
17+
- Stick running firmware `R4.4.20.018` or `R4.4.20.022` (other versions _may_ be safe)
18+
19+
This utility has been tested on:
20+
- openSUSE Leap 15.4
21+
- Windows 11
22+
- macOS 13.5
23+
24+
This utility is built on:
25+
- Ubuntu 22.04.2 LTS
26+
27+
## Features
28+
- Ethernet UNI moved from slot 10 to slot 1 in MIB entities, thus becoming compatible with AT&T bridge pack configurations
29+
- Disables traffic filtering when the Dot1X Port Extension Package (ME 290) is configured to filter all traffic
30+
- Uses serial provided to mod instead of the serial in EEPROM to allow the device to revert cleanly if the fail-safe triggers
31+
- Sets appropriate equipment id for NOKA/HUMA BGW320 devices depending on the provided serial
32+
- Starts `dropbear` 2 minutes or so after device boot for more convenient administration (no idle timeout)
33+
34+
## Usage
35+
36+
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.
37+
38+
Skip to [Installation](#installation) and [Enabling Persistence](#enabling-persistence) if you just want to get online.
39+
40+
### Password Generation
41+
42+
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.
43+
44+
```
45+
./fs_xgspon_mod.py genpw GPON227000fe
46+
Creds for FS.com XGS-PON stick with serial GPON227000fe:
47+
Telnet: GPON227000fe / mbdu7pVX
48+
SSH: ONTUSER / vjyKsHYsU2Aym5Nn
49+
```
50+
51+
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.
52+
53+
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`.
54+
55+
### Telnet
56+
57+
Helper command so that you only need to remember the serial. Drops you immediately to an enabled `#ONT>` prompt.
58+
59+
```
60+
./fs_xgspon_mod.py telnet GPON227000fe
61+
enable
62+
#ONT>
63+
```
64+
65+
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.
66+
67+
### Installation
68+
69+
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:
70+
71+
```
72+
./fs_xgspon_mod.py install GPON227000fe HUMA12ab34cd
73+
```
74+
75+
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.
76+
77+
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.
78+
79+
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.
80+
81+
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).
82+
83+
### Enabling Persistence
84+
85+
Persistence allows the modification to automatically re-arm itself for the next boot after a ~100 second timer expires.
86+
87+
```
88+
./fs_xgspon_mod.py persist HUMA12ab34cd
89+
```
90+
91+
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.
92+
93+
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.
94+
95+
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.
96+
97+
### Fail-safe Recovery
98+
99+
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.
100+
101+
```
102+
./fs_xgspon_mod.py telnet GPON227000fe
103+
enable
104+
#ONT> /s/s
105+
/s/s
106+
#ONT/system/shell>rm /mnt/rwdir/disarmed
107+
rm /mnt/rwdir/disarmed
108+
#ONT/system/shell>reboot
109+
reboot
110+
```
111+
112+
Wait a few minutes and it should come back online, responding to your AT&T NOKA/HUMA serial and getting you back online.
113+
114+
## Thanks To
115+
- [miguemely](https://github.com/miguemely) - Initial testing, firmware dumps
116+
- [YuukiJapanTech](https://github.com/YuukiJapanTech) - Assembling the spectacular resources at https://github.com/YuukiJapanTech/CA8271x/
117+
- SipWannabe - Testing
118+
119+
## References
120+
- https://github.com/YuukiJapanTech/CA8271x
121+
- https://hack-gpon.org/xgs/ont-nokia-xs-010x-q/

fs_xgspon_mod.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
#!/usr/bin/env python3
2+
from http.server import HTTPServer, SimpleHTTPRequestHandler
3+
from itertools import islice, chain
4+
from threading import Thread
5+
from telnetlib import Telnet
6+
from pathlib import Path
7+
import hmac
8+
9+
# if you add any new fields that need to be customed, you will
10+
# also need to adjust PayloadHandler.do_GET()
11+
CONFIG_TEMPLATE="""ETH10GESLOT=1
12+
EepEqVendorID=GPON
13+
EepEqSerialNumber=GPON12345678
14+
EepVDSL2SerialNumber= VDSLSerialNumberGPON12345678
15+
EepEqVersionID=GPON
16+
EepEqID=XG-99S
17+
"""
18+
19+
VENDOR_SPECIFIC = {
20+
"HUMA": "iONT320500G",
21+
"NOKA": "iONT320505G",
22+
}
23+
24+
# 0x36 byte array, each byte of the output digest is used as an index % 0x36
25+
# for the next output char; chars that can be easily confused are missing
26+
output_base = "2345679abcdefghijkmnpqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ"
27+
def extract_chars(key_base, serial, extract_len, total_len):
28+
# the number of bytes being extracted is appended to the 15 byte fixed
29+
# key to round it out to an even 0x10 byte hmac key
30+
key = key_base + total_len.to_bytes(1, 'little')
31+
digest = hmac.HMAC(key, serial.encode("utf-8"), "md5").digest()
32+
return map(lambda x: output_base[x % len(output_base)], islice(digest, extract_len))
33+
34+
# when over 0x10 bytes are requested, the first 0x10 characters of output
35+
# use a different key than all remaining bytes.
36+
# suspect that the Nokia XS-010X-Q is exactly the same with the exception
37+
# of the HMAC keys baked into libvos.so being different
38+
keys = [b"\x01\x03\n\x10\x13\x05\x17d\xc8\x06\x14\x19\xb4\x9d\x05",
39+
b"\x05\x11:`{\xfb\x0fC\\!\xbe\x86A2\x1c"]
40+
def VOS_HmacMD5(serial, required_len):
41+
it1 = extract_chars(keys[0], serial, min(0x10, required_len ), required_len)
42+
it2 = extract_chars(keys[1], serial, max( 0, required_len - 0x10), required_len)
43+
return ''.join(chain(it1, it2))
44+
45+
class CigTimeout(Exception):
46+
pass
47+
48+
class CigTelnet(Telnet):
49+
def __init__(self, serial):
50+
serial = serial[:4].upper() + serial[4:].lower()
51+
52+
super().__init__("192.168.100.1", 23)
53+
self._in_shell = False
54+
55+
self.read_until(b"Login as:")
56+
self.write(f"{serial}\n".encode("utf-8"))
57+
self.read_until(b":")
58+
self.write(f"{VOS_HmacMD5(serial.upper(), 8)}\n".encode("utf-8"))
59+
self.read_until(b"ONT>")
60+
self.write(b"enable\n")
61+
62+
def sh_cmd(self, cmd, timeout=2):
63+
if not self._in_shell:
64+
self._in_shell = True
65+
self.write(b"/s/s\n")
66+
self.read_until(b"shell>", timeout)
67+
68+
if not cmd.endswith("\n"):
69+
cmd += "\n"
70+
71+
self.write(cmd.encode("utf-8"))
72+
res = self.read_until(b"shell>", timeout)
73+
if b"shell>" not in res:
74+
raise CigTimeout("CigTelnet command timed out")
75+
return res.decode("utf-8")
76+
77+
class PayloadHandler(SimpleHTTPRequestHandler):
78+
def __init__(self, *args, serial=None, **kwargs):
79+
self.vendor = serial[:4]
80+
self.serial = serial[4:].lower()
81+
self.eqid = VENDOR_SPECIFIC[self.vendor]
82+
super().__init__(*args, directory=Path(__file__).parent / "payload", **kwargs)
83+
84+
def do_GET(self):
85+
if self.path=="/config":
86+
self.send_response(200)
87+
self.send_header("Content-type", "html")
88+
self.end_headers()
89+
self.wfile.write(CONFIG_TEMPLATE \
90+
.replace("GPON", self.vendor) \
91+
.replace("12345678", self.serial) \
92+
.replace("XG-99S", self.eqid) \
93+
.encode("utf-8"))
94+
else:
95+
return super().do_GET()
96+
97+
def genpw(args):
98+
serial = args.serial[:4].upper() + args.serial[4:].lower()
99+
100+
# the CigLogin binary relies on VOS_HmacMD5 (from libvos.so) to generate
101+
# the telnet password from the raw serial with output length 8
102+
# dropbearmulti, Console, and MecMgr all use VOS_HmacMD5 to generate
103+
# the password in the form of {SERIAL}-ONTUSER with output length 16
104+
105+
print(f"Creds for FS.com XGS-PON stick with serial {serial}:")
106+
print(f" Telnet: {serial} / {VOS_HmacMD5(serial.upper(), 8)}")
107+
print(f" SSH: ONTUSER / {VOS_HmacMD5(serial + '-ONTUSER', 16)}")
108+
109+
def telnet(args):
110+
with CigTelnet(args.serial) as tn:
111+
tn.interact()
112+
113+
def install(args):
114+
assert args.att_serial[:4] in VENDOR_SPECIFIC
115+
116+
class PayloadServer(HTTPServer):
117+
def finish_request(self, request, client_address):
118+
self.RequestHandlerClass(request, client_address, self, serial=args.att_serial)
119+
120+
print("Connecting via telnet...")
121+
with CigTelnet(args.gpon_serial) as tn:
122+
(addr, _) = tn.get_socket().getsockname()
123+
124+
with PayloadServer(("", 8172), PayloadHandler) as ps:
125+
(_, port) = ps.socket.getsockname()
126+
Thread(target=ps.serve_forever, daemon=True).start()
127+
print(f"Webserver listening on {addr}:{port}")
128+
print("If this doesn't complete almost immediately, ensure there is no router between you and the device!")
129+
130+
# ensure that if this goes Poorly we can power cycle our way out of it
131+
tn.sh_cmd("touch /mnt/rwdir/disarmed")
132+
tn.sh_cmd("[ -f /mnt/rwdir/setup.sh ] && rm /mnt/rwdir/setup.sh")
133+
134+
# prevent a bad update from incorrectly persisting based on a safe prior version that
135+
# was persisting successfully by forcing people to re-enable it the long way
136+
tn.sh_cmd("[ -f /mnt/rwdir/payload_auto_rearm ] && rm /mnt/rwdir/payload_auto_rearm")
137+
138+
try:
139+
assert "100%" in tn.sh_cmd(f"wget -O - {addr}:{port}/config > /mnt/rwdir/payload.cfg", 10)
140+
print("Payload configuration sent")
141+
142+
assert "100%" in tn.sh_cmd(f"wget -O - {addr}:{port}/payload.tgz | tar xvzf - -C /mnt/rwdir/", 10)
143+
except (CigTimeout, AssertionError):
144+
print(f"Error: Stick was not able to connect back and download payload! Check firewall!")
145+
return
146+
147+
assert "stage0.sh" in tn.sh_cmd("ls /mnt/rwdir/")
148+
149+
tn.sh_cmd("ln -sf /mnt/rwdir/stage0.sh /mnt/rwdir/setup.sh")
150+
tn.sh_cmd("[ -f /mnt/rwdir/disarmed ] && rm /mnt/rwdir/disarmed")
151+
tn.sh_cmd("sync")
152+
153+
print("Payload extracted -- press enter to reboot!")
154+
155+
tn.write(b"reboot") # missing newline on purpose
156+
tn.interact()
157+
158+
def persist(args):
159+
print("Connecting via telnet...")
160+
with CigTelnet(args.att_serial) as tn:
161+
if "payload_postboot_dropbear" not in tn.sh_cmd("ls -l /tmp/"):
162+
print("Persistence not allowed yet -- has it been 3+ minutes since boot?")
163+
print("If it's been more than 3 minutes and the device is not listening for SSH connections, the mod is not active!")
164+
return
165+
166+
tn.sh_cmd("[ -f /tmp/payload_postboot_dropbear ] && touch /mnt/rwdir/payload_auto_rearm")
167+
tn.sh_cmd("[ -f /mnt/rwdir/disarmed ] && rm /mnt/rwdir/disarmed")
168+
tn.sh_cmd("[ ! -f /mnt/rwdir/setup.sh ] && ln -s /mnt/rwdir/stage0.sh /mnt/rwdir/setup.sh")
169+
tn.sh_cmd("sync")
170+
171+
print("Persistence now enabled -- as a fail safe, a power cycle between ~30 seconds and ~120 seconds after boot should restore to stock")
172+
173+
if __name__=="__main__":
174+
import argparse
175+
176+
def parse_serial(serial):
177+
serial = serial.upper()
178+
179+
if len(serial) != 12:
180+
raise argparse.ArgumentError("serial must be 12 characters")
181+
182+
if serial[:4] not in ("GPON", "NOKA", "HUMA"):
183+
raise argparse.ArgumentError("vendor must be one of GPON, NOKA, HUMA")
184+
185+
try:
186+
numeric = serial[4:].strip()
187+
assert len(numeric) == 8
188+
int(numeric, 16)
189+
except (ValueError, AssertionError):
190+
raise argparse.ArgumentError("numeric portion of serial must be valid hexadecimal")
191+
192+
return serial
193+
194+
p = argparse.ArgumentParser()
195+
s = p.add_subparsers()
196+
197+
parse_genpw = s.add_parser("genpw")
198+
parse_genpw.add_argument("serial", type=parse_serial)
199+
parse_genpw.set_defaults(func=genpw)
200+
201+
parse_telnet = s.add_parser("telnet")
202+
parse_telnet.add_argument("serial", type=parse_serial)
203+
parse_telnet.set_defaults(func=telnet)
204+
205+
parse_install = s.add_parser("install")
206+
parse_install.add_argument("gpon_serial", type=parse_serial)
207+
parse_install.add_argument("att_serial", type=parse_serial)
208+
parse_install.set_defaults(func=install)
209+
210+
parse_persist = s.add_parser("persist")
211+
parse_persist.add_argument("att_serial", type=parse_serial)
212+
parse_persist.set_defaults(func=persist)
213+
214+
args = p.parse_args()
215+
args.func(args)
216+

rwdir/dangerous_payload.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
mkdir -p /tmp/payload/
4+
touch /tmp/payload/libvos.so
5+
6+
mount -o bind /usr/lib/libvos.so /tmp/payload/libvos.so
7+
mount -o bind /mnt/rwdir/payload/libvos_shim.so /usr/lib/libvos.so

rwdir/stage0.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/sh
2+
3+
BASEDIR=/mnt/rwdir
4+
5+
if [ ! -f $BASEDIR/disarmed ]; then
6+
touch $BASEDIR/disarmed
7+
8+
# if we're not supposed to be persistent, nuke symlink
9+
[ ! -f $BASEDIR/payload_auto_rearm ] && rm $BASEDIR/setup.sh
10+
11+
sync
12+
13+
$BASEDIR/dangerous_payload.sh
14+
touch /tmp/payload_stage0
15+
fi
16+
17+
# always return error so that we never halt /sbin/setup.sh
18+
exit 1

shim/deps/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Copy libc-2.23.so and libdl-2.23.so from the device into this directory for linking purposes.

0 commit comments

Comments
 (0)