Fios G3100 / E3200 Research

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
Hello all, I have posted the same information on the hashcat forums, but I wanted to share here too.

I recently discovered how easy it was to crack my Netgear default password. That thrill led me to turn my attention to my Fios G3100. However, this has turned out to be a much worthier adversary. Boredom and a bit of tenacity has led me down a winding path, but here is where I am at so far in my research.


Backref.jpeg

The G3100 and E3200 routers are manufactured by Arcadyan distributed by Verizon. Per usual, the sticker on the back of the unit has the necessary information. I wrote a small python script to scrape Ebay and FB listings and collect all of the associated images. A second script sorts the images using computer vision and OCR to detect the QR code or relevant text. I then personally process the good images to collect the useful information, whenever possible I use the QR code as it is the most trustworthy data to read. Thus far, I have collected over 230+ complete records, as well as saving the images for verification. The dataset is in .xlsx format because I use conditional formatting to check for things like proper length and duplicates, if anyone prefers that I upload it as .csv please let me know.

Link to data set: FiosG3100andE3200.xlsx - (Also attached below - Dawbs)
Link to images: Ref_Images.zip - (Link to my post below - Dawbs)

From this sample we can gain some info on the G3100 key space:
  • MAC address starting with 04.A2.22 are the oldest and have 16 character passwords
    SSID is Fios-XXXXX where X is any char <a-z><A-Z><0-9>
    SSID Passwords follow <word><number><word><number><word> format (ex: met8sonata868elm)
    Admin Passwords are 16 characters and follow a <word><number><word> format (ex: stubble16crowded)
  • MAC address starting with B8:F8:53 are mixed and may have 15 or 16 character passwords
    SSID is Fios-XXXXX where X is any char <a-z><A-Z><0-9>
    SSID Passwords follow <word><number><word><number><word> format (ex: moat288nit48pug)
    Admin Passwords are 16 characters and follow a <word><number><word> format (ex: chopper86notably)
  • MAC address starting with 3C.BD.C5 are the newest and have 15 character passwords
    SSID is Fios-XXXXX where X is any char <a-z><A-Z><0-9>
    or Verizon_XXXXXX where X is any char <A-Z><0-9>
    SSID Passwords for “Fios” networks follow <word><number><word><number><word> format (ex: range36vex77toy)
    or “Verizon” networks follow <word>-<word>-<word> with a single digit at the end of one word (ex: miry9-elm-north)
    Admin Passwords for “Fios” network are 16 characters and follow a <word><number><word> format (ex: unusual53smelter)
    or “Verizon” networks are 9 characters that are <A-Z><0-9> (ex: Z79KGSX4T)
    Note: 0 and 1 are not seen in sample

From this sample we can gain some info on the E3200 key space:
  • MAC address starting with 04.A2.22 are the oldest and have 16 character passwords
    SSID is E3200-XXXXX where X is any char <a-z><A-Z><0-9>
    SSID Passwords follow <word><number><word><number><word> format (ex: nylon88wit657aye)
    Admin Passwords are 16 characters and follow a <word><number><word> format (ex: ritual236auction)
  • MAC address starting with B8:F8:53 are mixed and may have 15 or 16 character passwords
    SSID is E3200-XXXXX where X is any char <a-z><A-Z><0-9>
    SSID Passwords follow <word><number><word><number><word> format (ex: mach92see36flat)
    Admin Passwords are 16 characters and follow a <word><number><word> format (ex: seraph497lantern)
  • MAC address starting with 3C.BD.C5 have 15 character passwords
    SSID is Verizon_XXXXXX where X is any char <A-Z><0-9>
    SSID Passwords follow <word>-<word>-<word> with a single digit at the end of one word (ex: tenth-ben6-vend)
    Admin Passwords are are 9 characters that are <A-Z><0-9> (ex: 3JB94H6CQ)
    Note: 0 and 1 are not seen in sample
  • MAC address starting with DC.F5.1B are the newest and have 15 character passwords
    SSID is Verizon_XXXXXX where X is any char <A-Z><0-9>
    SSID Passwords follow <word>-<word>-<word> with a single digit at the end of one word (ex: plush-fast3-con)
    Admin Passwords are are 9 characters that are <A-Z><0-9> (ex: QVB734TKL)
    Note: 0 and 1 are not seen in sample
From this sample we can gain some other info:
  • Password <word> are between 3-7 characters for SSID Password
  • Password <number> are between 1-4 digits
  • There are 3 HW version (1102, 1103, 1104)
  • Serial #’s are 16 digits (except for the most recent E3200 which have 11)
  • Shipped firmware ranges from 1.3.5.1 to 3.1.1.16
  • There are the 568 unique words extracted from the passwords:

Code:
add
aft
ago
aim
air
ait
alp
ape
ark
art
ash
ask
awe
aye
bat
bay
bed
bee
beg
ben
bet
bid
biz
boa
bog
bot
bow
bug
bun
bus
bye
cat
caw
cif
cob
con
cot
cub
cud
cup
cut
dab
dad
dam
daw
day
del
dew
dia
did
dig
dit
doe
dos
due
dun
ear
eeg
ego
eke
elk
elm
end
fad
fat
fax
fay
fed
fee
fen
few
fez
fib
fig
fin
fir
fit
fob
fog
fop
for
fox
fro
gad
gap
gel
gem
gen
gig
gin
gnp
gnu
got
gut
had
han
has
hat
hew
hey
hid
hie
him
hin
hit
hod
hub
hue
hum
ice
icy
jab
jag
jam
jaw
jet
jib
jog
joy
jus
lab
law
lay
let
mad
may
met
mil
mix
mod
mow
mud
mug
mum
nag
naw
new
nib
nip
nit
nod
non
not
now
oak
oar
odd
ode
oil
one
ope
opt
ork
out
owl
pal
paw
pay
pea
pet
pit
pod
pug
pun
pup
put
ram
ran
rap
raw
ray
ree
ret
rid
rna
roe
rug
run
rut
rye
sat
say
see
set
sew
sir
sit
six
sly
sou
sow
soy
spa
sum
sun
sup
tag
tap
tax
tee
too
tot
toy
tun
ush
vex
vie
vim
wad
was
wax
web
wed
why
wig
win
wit
woe
won
woo
wry
yak
yam
yea
yes
yet
yon
you
zap
zoo
abbe
aery
agog
alas
alga
allo
arms
atom
back
bake
beak
been
beep
bits
boar
bolt
bone
book
boss
bred
brew
brow
cafe
cape
cart
cast
cene
cere
cham
char
cloy
copy
crib
cuff
dark
dear
deny
dewy
dial
dine
dint
dock
doff
dory
doth
drub
dump
dust
each
ever
exam
fade
fame
fare
fast
fawn
feet
felt
fine
flat
flaw
flit
form
fund
fuss
gage
gain
gall
gate
gent
golf
grab
gray
grey
grim
hair
hake
halt
hasp
have
hawk
held
hide
high
holm
hone
hoot
hour
huff
hung
ibis
iron
jibe
jill
june
kale
kidd
kirk
knit
knot
lack
lead
lean
lend
lens
less
lump
mach
mama
mass
meat
mica
mint
miry
moat
mood
myth
nail
name
nice
nigh
nite
oboe
oily
ouch
over
paid
pail
pant
pelf
pell
pelt
pert
plan
plot
plus
pool
pram
push
quiz
raze
rill
ripe
roar
rome
roof
rook
ruby
rush
sage
sale
self
shed
sign
sill
skim
slop
slue
slug
soap
solo
spin
stir
swam
swap
tare
tele
tell
than
then
they
tidy
tier
ting
tout
tram
trod
tron
tune
type
upon
vain
vane
vend
vide
vine
wain
wait
wake
wane
want
wash
wavy
what
whom
will
wind
wing
wire
wisp
wood
yard
yeah
yell
yelp
yond
zest
acute
amaze
angel
apace
basic
begot
bough
brush
camel
carry
chase
clean
clump
coach
cocky
combe
comet
coney
could
crate
creak
credo
cress
crock
crone
demur
deter
divan
douse
drily
eater
elope
enact
endow
favor
fifth
fifty
finny
flock
floor
floss
flown
focal
focus
forte
froth
fuzzy
games
gorse
guise
hoary
hobby
hutch
inapt
inner
jewel
mayor
meant
mense
mixed
moose
muddy
mulct
niter
north
nylon
order
papal
pivot
plait
plumy
plush
poser
price
quard
quell
quest
range
rapid
rayon
sales
salon
salty
scend
scope
scour
sense
shack
sixty
smack
snips
snort
spark
spent
steep
stiff
swell
synod
taper
tarry
tempt
tenth
thank
tinge
today
trace
track
tract
trade
trawl
trend
tweet
tyler
vague
verse
vetch
vital
whose
witty
woman
worse
wrist
behove
bethel
german
iodine
pallor
remove
sonata
bloated
sweater

Although there is a lot of useful information collected in the sample, it is still a fairly large key space. With that in mind I decided to take my first dives into firmware analysis, which of course requires some firmware. Looking online, I was able to find a single reddit post that linked to g3100 firmware version 3.2.0.15. With a lot more digging, I was able to find posts with links to firmware for other devices. Using this information I wrote another script to try to find additional firmware. Here’s what I've found, many of these are the first time posted online I believe.

Code:
G3100:
https://cpe-ems34.verizon.com/firmware/g3100_fw_2.0.0.6.bin
https://cpe-ems34.verizon.com/firmware/g3100_fw_3.1.1.17.bin
https://cpe-ems34.verizon.com/firmware/g3100_fw_3.1.1.18.bin
https://cpe-ems34.verizon.com/firmware/g3100_fw_3.2.0.11.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.2.0.13.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.2.0.14.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.2.0.15.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.4.0.4_loader.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.4.0.6_loader.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.4.0.8_loader.bin
https://cpe-ems34.verizon.com/firmware/BHRx/g3100_fw_3.4.0.9_loader.bin

E3200:
https://cpe-ems34.verizon.com/firmware/e3200_fw_3.1.1.17.bin
https://cpe-ems34.verizon.com/firmware/e3200_fw_3.1.1.18.bin
https://cpe-ems34.verizon.com/firmware/e3200_fw_3.2.0.1.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.2.0.11.trapeze.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.2.0.12.trapeze.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.4.0.8.trapeze.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.2.0.11.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.2.0.12.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.4.0.7_loader.bin
https://cpe-ems34.verizon.com/firmware/BHRx_Ext/e3200_fw_3.4.0.8_loader.bin

All of the links I found online for Verizon G3100, E3200, and CR1000 all used the cpe-ems34 link. I did find some other routers that were using different servers such as cpe-ems20 and cpe-ems31. Further investigation lead to this site showing all of the Verizon subdomains, which there are a ton of cpe-ems domains. I tried my script with a few such as 31, 33, 43, however nothing new was turned up.

Code:
<Mirrors>
https://cpe-ems33.verizon.com/firmware/g3100_fw_3.1.1.17.bin
https://cpe-ems33.verizon.com/firmware/g3100_fw_3.2.0.15.bin

I tried binwalk on the first firmware I found (3.2.0.15) and, and while it extracts the file system, none of the files were readable for me. The entropy graph shows that only a small part is encrypted, so I am a bit confused. My next step is to try to mount it in a VM Linux since I only have Mac and RPI for testing. This is what led me to looking for older firmware, however using binwalk on 2.0.0.6 gives me similar results. I know that there should be at least 3more older firmware 1.3.6.27, 1.5.0.10, and 2.0.0.5 but I have not been able to locate them.

g3100_fw_3.2.0.15.bin.png g3100_fw_2.0.0.6.bin.png

The possibility of firmware encryption led me to look at physical access of the device. After some quick soldering, I connected to the UART. Unfortunately this did not lead to a shell either, but did provide a bit more information. Referencing some of the output online, I found someone else who also connected this way and had a longer output (possibly because of older firmware?).
Code:
BTRM
V1.0
R1.0
L1CD
MMUI
MMU9
DATA
ZBBS
MAIN
OTP?
REF?
REFP
RTF?
RTFP
OTPP
FSBT
NAND
IMG?
IMGL
UHD?
UHDP
RLO?
RLOP
AHD?
ROT?
ROTA
MID?
MIDP
AHDP
SBI?
SBIA
PASS
----

U-Boot SPL 2019.07 (Oct 31 2023 - 03:52:42 -0400)
Strap register: 0x53008176
Board is FLD secure
$SPL: 5.04L.02@419765 $
nand flash device id 0x98d39126, total size 1024MB
block size 256KB, page size 4096 bytes, spare area 216 bytes
ECC BCH-8
FFinit done
find magic number 0x75456e76 at address 0x100000
FFinit find magic number 0xcb00cb at address 0x114000
reading blob from 0x114000 offset 0x26c len 608
digest sha256 OK
FFinit find magic number 0x64447233 at address 0x105000
reading blob from 0x105000 offset 0xc len 59888
digest sha256 OK
mcb selector 0x1427 checksum 0x722c322d safe_mode 0

U-Boot DDR standalone 2019.07 (Jul 25 2021 - 18:43:37 -0700) Build: 5.04L.02@348603

MemsysInit hpg0_generic_aarch64 3.5.1.1 20171009
DDR3
8267D980 80180000 801A0000 00000000 00000000 0020476E
MCB rev=0x00000501 Ref ID=0x0476E Sub Bld=0x002
Dram Timing 11-11-11

start of memsys_begin
mc_cfg_init(): Initialize the default values on mc_cfg
init_memc_dram_profile(): Initializing MEMC DRAM profile
---------------------------------------------------------------
MEMC DRAM profile (memc_dram_profile_struct) values:
  dram_type    = DDR3
====================================================
PART values:
  part_speed_grade    = 1600 CL11
  part_size_Mbits    = 4096 (DRAM size in MegaBits)
  part_row_bits      = 15 (number of row bits)
  part_col_bits      = 10 (number of column bits)
  part_ba_bits        = 3 (number of bank bits)
  part_width_bits    = 16 (DRAM width in bits)
NUMER OF PARTS:
  part_num            = 1 (Number of parts)
TOTAL values:
  total_size_Mbits    = 4096 (DRAM size in MegaBits)
  total_cs_bits      = 0 (number of cs bits, for dual_rank mode)
  total_width_bits    = 16 (DRAM width in bits)
  total_burst_bytes  = 16 (Number of bytes per DRAM access)
  total_max_byte_addr = 0x1fffffff (Maximum/last DRAM byte address)
                        (Number of bits in total_max_byte_addr is 29)
                        (i.e. total_max_byte_addr goes from bit 0 to bit 28)
  ddr_2T_mode        = 0
  ddr_hdp_mode        = 1
  large_page          = 1
  ddr_dual_rank      = 0
  cs_mode            = 0
MEMC timing (memc_dram_timing_cfg_struct) values:
====================================================
  MC_CHN_TIM_TIM1_0 register fields:
    tCwl  = 8
    tRP    = 11
    tCL    = 11
    tRCD  = 11
  MC_CHN_TIM_TIM1_1 register fields:
    tCCD_L = 4
    tCCD  = 4
    tRRD_L = 6
    tRRD  = 6
  MC_CHN_TIM_TIM1_2 register fields:
    tFAW  = 32
    tRTP  = 6
    tRCr  = 39
  MC_CHN_TIM_TIM1_3 register fields:
    tWTR_L = 6
    tWTR  = 6
    tWR_L  = 12
    tWR    = 12
  MC_CHN_TIM_TIM2 register fields:
    tR2R  = 0
    tR2W  = 2
    tW2R  = 2
    tW2W  = 0
    tAL    = 0
    tRFC  = 208
====================================================
%1 SSC enabled

Poll PHY Status register
PHY Status= 1
Disable Auto-Refresh
[0000000080180200] = 0x00000305
End of memsys_begin
Add/Ctl Alignment
Coarse Adj=0x087 deg, cmd steps=0x0DC
reg 0x801A0090 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A0094 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A0098 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A009C set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00A0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00A4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00A8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00AC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00B0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00B4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00B8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00BC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00C0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00C4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00C8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00CC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00D0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00D4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00D8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00DC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00E0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00E4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00E8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00EC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00F0 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00F4 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00F8 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A00FC set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A0100 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A0108 set to VDL 0x054 with Fine Adj=0x01 deg
reg 0x801A010C set to VDL 0x054 with Fine Adj=0x01 deg
HP RX TRIM
itrim = 0x0
lstrim = 0x9

ZQ Cal HP PHY
R in Ohm
P: Finger=0x318 Term=0x71 Drv=0x28
N: Finger=0x2A6 Term=0x71 Drv=0x28

PLL Ref(Hz)=0x02FAF080 UI STEPS=0x06E
DDR CLK(MHz)=0x31B WL CLK dly(ps)=0x0C8 bitT(ps)=0x274 VDLsize(fs)=0x164D CLK_VDL=0x023
start of memc_init
[0000000080180004] = 0x0110061f
[0000000080180234] = 0x00001101
Enable Auto-Refresh
[0000000080180110] = 0x11100f0e
[0000000080180114] = 0x15141312
[0000000080180118] = 0x19181716
[000000008018011c] = 0x001c1b1a
[0000000080180124] = 0x04000000
[0000000080180128] = 0x08070605
[000000008018012c] = 0x00000a09
[0000000080180134] = 0x000d0c0b
Writing to MC_CHN_CFG_CNFG reg; data=0x00000000
[0000000080180100] = 0x00000000
cfg_memc_timing_ctrl() Called
[0000000080180214] = 0x080b0b0b
[0000000080180218] = 0x04040606
[000000008018021c] = 0x20000627
[0000000080180220] = 0x06060c0c
[0000000080180224] = 0x120000d0
End of memc_init
start of pre_shmoo
[0000000080180004] = 0xc110071f
end of pre_shmoo

SHMOO 28nm
801A0000 80180800 00000000 00020000 00000000

Shmoo WL

One UI Steps : 0x7B

auto-clk result = 01B (filter=0C steps)
initial CLK shift = 023
final CLK shift  = 01B

  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 S-------------------X------------------------------------------------------------------------------------------------------
01 S-----------X--------------------------------------------------------------------------------------------------------------

Shmoo RD En
FORCED WR ODT = 0x00001800
DQSN DRIVE PAD CONTROL (from) (to)
B0 00039A91 00079A91
B1 00039A91 00079A91
B0 RISE UI=1 VDL=1B PICK UI=2 VDL=1B
B1 RISE UI=1 VDL=28 PICK UI=2 VDL=28
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 --S-----------------+---+++X+++++++++++++++--------------------------------------------------------------------------------
01 --S-----------------------------+----++-X+++++++++++++++-------------------------------------------------------------------

Shmoo RD DQ NP
DQS :
B0 VDL=6E ok
B1 VDL=6E ok
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 ---------------------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++-------
01 ---------------+++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++---------
02 ------------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---------
03 ----------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++-----------------
04 --------------+++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++-----------------
05 ------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---------------
06 ------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-------------
07 --------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++--------------------
08 ------------------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---
09 -----------------------++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---
10 -------------------+++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-----
11 --------------------+++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++---
12 -----------------+++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++------
13 ----------------++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++-----
14 --------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-----------
15 ------------------++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++---

Shmoo RD DQ P
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 ---------------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++------
01 ---------------+++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++-----
02 -------------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++------
03 ---------++++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++-------------
04 --------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++-------------
05 ------------+++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++-----------
06 ------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-------------
07 --------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-----------------
08 ------------------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-
09 -----------------------++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---
10 -------------------+++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-----
11 --------------------+++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++---
12 -----------------+++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-------
13 ---------------++++++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++++---
14 --------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-----------
15 -----------------+++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++---

Shmoo RD DQ N
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 ------------------+-+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++-------
01 ----------------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++---------
02 ------------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---------
03 ---------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++------------------
04 ------------++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++-----------------
05 -----------++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++--------------
06 -----------+++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++-------------
07 -----++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++--------------------
08 ------------------------++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++--
09 ---------------------++++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++-
10 ------------------+++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++-----
11 ------------------+++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++--
12 ---------------++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++------
13 ----------------++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++-----
14 ------------++++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++++----------
15 ------------------++++++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++++++---

RD DQS adjustments :
BL0: Start: 0x6E Final: 0x6E
BL1: Start: 0x6E Final: 0x6E

Shmoo WR DQ
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 ------------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++---------------
01 ----------+++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++---------------------
02 ------------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++----------------
03 ---+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++------------------------
04 ---------+++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++-----------------------
05 --------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++--------------------
06 -----------++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++------------------
07 ---+++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++-----------------------------
08 ---------------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++-------------
09 ---------------++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++---------------
10 -----------+++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++----------------
11 -----------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++-----------------
12 -----------+++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++--------------------
13 -----------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++-----------------
14 ----+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++------------------------
15 ------------+++++++++++++++++++++++++++++++++++++++++++++++X+++++++++++++++++++++++++++++++++++++++++++++++----------------

Shmoo WR DM
WR DM
  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111
  000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222
  012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012
00 -------++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++-----------------------
01 --------++++++++++++++++++++++++++++++++++++++++++++++++X++++++++++++++++++++++++++++++++++++++++++++++++------------------
start of memsys_end
[0000000080180004] = 0x8110071f
[0000000080180010] = 0x00000009
end of memsys_end
DDR test done successfully
FFinit find magic number 0x75456e76 at address 0x100000
FFinit find magic number 0x74506c21 at address 0x140000
reading blob from 0x140000 offset 0xc len 163741
digest sha256 OK

U-Boot TPL 2019.07 (Oct 31 2023 - 03:52:39 -0400)
Board is FLD secure
$TPL: 5.04L.02@419765 $
CPU Clock: 1500MHz
IMAGE is NAND
Trying to boot from NAND
nand flash device id 0x98d39126, total size 1024MB
block size 256KB, page size 4096 bytes, spare area 216 bytes
ECC BCH-8
image from 2097152 to 315621376
brcmnand_read_buf(): Attempt to read bad nand block 760
brcmnand_read_buf(): Attempt to read bad nand block 762
brcmnand_read_buf(): Attempt to read bad nand block 768
brcmnand_read_buf(): Attempt to read bad nand block 770
brcmnand_read_buf(): Attempt to read bad nand block 772
brcmnand_read_buf(): Attempt to read bad nand block 780
brcmnand_read_buf(): Attempt to read bad nand block 782
RESET STATUS is 0x80000000
SELECTED Image 1 FIT_VOL_ID is 3
brcmnand_read_buf(): Attempt to read bad nand block 760
brcmnand_read_buf(): Attempt to read bad nand block 762
brcmnand_read_buf(): Attempt to read bad nand block 768
brcmnand_read_buf(): Attempt to read bad nand block 770
brcmnand_read_buf(): Attempt to read bad nand block 772
brcmnand_read_buf(): Attempt to read bad nand block 780
brcmnand_read_buf(): Attempt to read bad nand block 782
Found FIT format U-Boot
tpl_load_read: sector 7000000, count 3194, buf 0000000007000000
tpl_load_read: sector 7000000, count 4192, buf 0000000007000000
fit read sector 7000000, sectors=16786, dst=0000000007000000, count=16786, size=0x4192
FIT Header Authentication Successfull!
INFO: Found disabled /trust/anti-rollback node!
INFO: Found /trust/hw_state node in fit
tpl_load_read: sector 7003680, count 8028, buf 0000000000004000
## Checking hash(es) for Image atf ... sha256+ OK
tpl_load_read: sector 700b680, count 27fc80, buf 0000000001000000
## Checking hash(es) for Image uboot ... sha256+ OK
tpl_load_read: sector 76ea1c0, count c5be, buf 000000000127fc80
## Checking hash(es) for Image fdt_VERIZON-G3100 ... sha256+ OK
INFO: Creating //trust
INFO: Creating /trust/antirollback_lvl
INFO: Adding exported item node antirollback_lvl to dtb, size:4
INFO: Creating /trust/brcm_pub_key
INFO: Adding exported item node brcm_pub_key to dtb, size:256


U-Boot 2019.07 (Oct 31 2023 - 03:52:45 -0400), Build: 5.04L.02@419765

Model: VERIZON-G3100
DRAM:  512 MiB
max supported leds 32[32]
Serial LED interface found num shifters 2 [2] serial data polarity low 0
BCA LED Controller initialized
HW led 3 registered
HW led 4 registered
HW led 5 registered
HW led 6 registered
HW led 7 registered
HW led 8 registered
HW led 9 registered
HW led 10 registered
SW led 0 registered
SW led 1 registered
SW led 2 registered
SW led 11 registered
SW led 12 registered
SW led 13 registered
SW led 14 registered
SW led 15 registered
Dump Current setting of SWREGs
1.0D, reg=0x00, val=0xc690
1.0D, reg=0x01, val=0x0d06
1.0D, reg=0x02, val=0xcb12
1.0D, reg=0x03, val=0x5372
1.0D, reg=0x04, val=0x0000
1.0D, reg=0x05, val=0x0702
1.0D, reg=0x06, val=0xb000
1.0D, reg=0x07, val=0x0029
1.0D, reg=0x08, val=0x0c02
1.0D, reg=0x09, val=0x0071
1.8 , reg=0x00, val=0xc690
1.8 , reg=0x01, val=0x0d06
1.8 , reg=0x02, val=0xcb12
1.8 , reg=0x03, val=0x5370
1.8 , reg=0x04, val=0x0000
1.8 , reg=0x05, val=0x0702
1.8 , reg=0x06, val=0xb000
1.8 , reg=0x07, val=0x0029
1.8 , reg=0x08, val=0x0c02
1.8 , reg=0x09, val=0x0071
1.5 , reg=0x00, val=0xc690
1.5 , reg=0x01, val=0x0d06
1.5 , reg=0x02, val=0xcb12
1.5 , reg=0x03, val=0x5370
1.5 , reg=0x04, val=0x0000
1.5 , reg=0x05, val=0x0702
1.5 , reg=0x06, val=0xb000
1.5 , reg=0x07, val=0x0029
1.5 , reg=0x08, val=0x0c02
1.5 , reg=0x09, val=0x0071
1.0A, reg=0x00, val=0xc690
1.0A, reg=0x01, val=0x0d06
1.0A, reg=0x02, val=0xcb12
1.0A, reg=0x03, val=0x5370
1.0A, reg=0x04, val=0x0000
1.0A, reg=0x05, val=0x0702
1.0A, reg=0x06, val=0xb000
1.0A, reg=0x07, val=0x0029
1.0A, reg=0x08, val=0x0c02
1.0A, reg=0x09, val=0x0071
Take PMC out of reset
waiting for PMC finish booting
PMC rev: 3.4.1.427360 running
pmc_init:PMC using DQM mode
Chip ID: BCM68369_B1
Broadcom B53 Dual Core: 1500MHz
RDP: 1400MHz
$Uboot: 5.04L.02@419765 $
WDT:  Started with servicing (80s timeout)
NAND:  1024 MiB
MMC:  sdhci: 0
Loading Environment from BOOT_MAGIC... ENV_BOOT_MAGIC_LOAD
found magic at 100000
good crc
resize from 16384 to 8192
OK
In:    serial0
Out:  serial0
Err:  serial0
Board is FLD secure
INFO: Can't find /trust/fit-aes1 node in boot DTB!
Now we are in UBOOT proper
HTTPD: ready for starting
boot_device is NAND
Net:  Using MAC Address b8:f8:53:0b:1d:01
eth0: switch0
No size specified -> Using max size (7300992)
Read 7300992 bytes from volume bootfs1 to 0000000002000000
FIT Header Authentication Successfull!
Read 4 bytes from volume rootfs1 to 000000001dd40664
## Loading kernel from FIT Image at 02000000 ...
  Using 'conf_lx_VERIZON-G3100' configuration
  Verifying Hash Integrity ... OK
  Trying 'kernel' kernel subimage
    Description:  4.19 kernel
    Type:        Kernel Image
    Compression:  lzma compressed
    Data Start:  0x0228c800
    Data Size:    3461392 Bytes = 3.3 MiB
    Architecture: AArch64
    OS:          Linux
    Load Address: 0x00100000
    Entry Point:  0x00100000
    Hash algo:    sha256
    Hash value:  77e40836ec218fa969f9d2bd572115ed9a7ef008cc75bfec4912354ce78a6349
  Verifying Hash Integrity ... sha256+ OK
## Loading fdt from FIT Image at 02000000 ...
  Using 'conf_lx_VERIZON-G3100' configuration
  Verifying Hash Integrity ... OK
  Trying 'fdt_VERIZON-G3100' fdt subimage
    Description:  dtb
    Type:        Flat Device Tree
    Compression:  uncompressed
    Data Start:  0x026ea1c4
    Data Size:    50618 Bytes = 49.4 KiB
    Architecture: AArch64
    Hash algo:    sha256
    Hash value:  c50470d2e693ebcd7dd68e42cc1de0ace24ccc30766e9c36d08c6b4462fa2e53
  Verifying Hash Integrity ... sha256+ OK
  Booting using the fdt blob at 0x26ea1c4
ARCADYAN: Authenticating vmlinux ...
ARCADYAN: Authenticating vmlinux pass
ARCADYAN: Decrypting kernel image ...
ARCADYAN: Decrypting kernel image done
  Uncompressing Kernel Image ... OK
ERROR: reserving fdt memory region failed (addr=1b400000 size=4c00000)
  Loading Device Tree to 0000000007f73000, end 0000000007fff5b9 ... OK
RSVD: not found enrty for adsl
RSVD: not found enrty for bufmem
RSVD: not found enrty for rnrmem
RSVD: Allocated for rdp1    64MB
RSVD: Allocated for rdp2    8MB
RSVD: Allocated for dhd0    11MB
RSVD: Allocated for dhd1    11MB
RSVD: Allocated for dhd2    11MB
RSVD: Total 0x06c00000 bytes CMA reserved memory @ 0x19400000
appending extra boot args to linux boot command line:
  mtdparts=brcmnand.0:2097152(loader),313524224@2097152(image),8388608@315621376(misc1),1048576@324009984(misc3),709885952@325058560(data),28311552@1034944512(owl),1048576@1063256064(mtdoops),2097152@1064304640(license),2097152@1066401792(certificate),1048576@1068498944(pri
Starting kernel ...

D%G

My device is currently running firmware 3.4.0.9, which I tried to revert to any previous version. I found a reference to a “hidden” admin page to update firmware at https://192.168.1.1/#/firmware_upgrade, but none of the firmware I downloaded would work (I think due to anti rollback).

I had a bit of time over the weekend to play with the data, and I am happy to report that I made some progress! With a little more data collection/processing I should be able to generate all of the Serial numbers based on MAC address. Without an algorithm, it isn’t super exciting... but it does allow me to at least validate the MAC and serials I collect are accurate since the characters 0 8 B all look similar in blurry photos. If we ever do discover the algorithm, the serial number could likely be part of it.

Here’s my analysis...

I noticed that there seemed to be a pattern in the last digit of the MAC address, with many ending in 0/8, or 4/C especially when grouped together by serial number. So I separated the data by MAC address using the first 3 octets (ie B8.F8.53.XX.XX.XX), and then combined the last 3 octets (ie DD.4A.98) removed the decimals (DD4A98) and converted the hex to decimal value (14502552). This gave me a numerical value for the MAC that I could plot vs the serial number. We can see there is a very strong correlation, but we can’t accurately calculate the serial number.

B8F853.png

To try to determine the relationship, I kept the serial numbers sorted in order. Then I took the hex2dec and serial number of one entry, and subtracted it from the next. I then took the (Mac difference / Serial difference). Seeing the same whole digit pop up several times gives us a clue that we’re on to something!

Example MACs.png

However, there are still several entries that don’t make any sense at all. I tried to look at the serial numbers individually, but there were huge jumps that didn’t seem explainable. I fed the data to our favorite AI, and it couldn’t make much sense of it either. However, it did suggest that perhaps the digits 21 in all of the serial numbers (E302120090812958) was a date code. I investigated this a little bit, but it didn’t seem to work out. Eventually I broke the digits into smaller numbers, which then allowed me to recognize there is a date code, just in a slightly different spot! The format is 2-digit year, 2 digit month, 2 digit day starting on the 6th character (E302120090812958 = 09/08/2020). I was able to verify this because the month spot is never greater than 12, the day spot is never greater than 31 and is only 31 on months that have that many days, and the years are between 19-23. The first 5 characters in the serial always stay the same, so that leaves us with the last 5 digits as the incremental serial (E302120090812958). With this information in hand, I sorted the list by date codes, and everything starts to line up more. The number 8 that shows up is how many steps there are between MAC addresses, so if the MAC is 3C.BD.C5.42.22.50, the next address in the sequence will be 3C.BD.C5.42.22.58. Using this information, I can actually back calculate the starting MAC address for each date code if we assume each block starts @ 00000 serial. Once all of the starting blocks are calculated, we can calculate the end of the previous block by subtracting 1 step. The E3200 MACS step by 6 digits instead of 8, some of the earlier g3100 jump by 11. Several of the ending blocks stop on seemingly non-random numbers, which is another clue that we're on the right track. I have gone though and done this for the 3C.BD.C5 MACs I have on hand, and ended up covering most of the address space. I was also able to identify and correct several places where the MAC or Serial was slightly off in my original data set (and validated using the saved photos)

Date Block Example.png

With this bit of success, I created a python script to calculate the serial based on a given MAC. There are still a few “UNKNOWN” blocks when there seems to be a large gap of MACs between blocks. I will work on adding the other MAC prefixes, so that we can enumerate all possible devices. I also stopped on serial 00001 earlier instead of 00000. I am still not sure which is correct honestly. Eventually I will correct them all to 00000 add code to detect these edges and give you both Serials. If you test the code, please report back. If you want to give me a MAC address and have me try to guess the serial I can. Otherwise, I have another batch of recently scraped images that I still need to validate and add to the database. I will be testing these on the script. My next post should have an updated data set!

Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
from datetime import datetime

# Your MAC ranges data
mac_data = """
3C.BD.C5.00.00.00:3C.BD.C5.01.F5.6A,G3100,8,Unknown
3C.BD.C5.01.F5.70:3C.BD.C5.09.3B.F8,G3100,8,121011516659
3C.BD.C5.09.3C.00:3C.BD.C5.0C.30.38,G3100,8,121030100001
3C.BD.C5.0C.30.40:3C.BD.C5.16.4A.C8,G3100,8,121021700001
3C.BD.C5.16.4A.D0:3C.BD.C5.1C.C1.58,G3100,8,121031200001
3C.BD.C5.1C.C1.5E:3C.BD.C5.22.C3.EA,G3100,8,Unknown
3C.BD.C5.22.C3.F0:3C.BD.C5.2B.5B.E8,G3100,8,121033100001
3C.BD.C5.2B.5B.F0:3C.BD.C5.31.1E.60,G3100,8,121041400001
3C.BD.C5.31.1E.66:3C.BD.C5.37.5C.92,G3100,8,Unknown
3C.BD.C5.37.5C.98:3C.BD.C5.3D.D3.40,G3100,8,121051800001
3C.BD.C5.3D.D3.48:3C.BD.C5.42.22.58,G3100,8,121060700001
3C.BD.C5.42.22.60:3C.BD.C5.46.3E.C4,G3100,8,121062200000
3C.BD.C5.46.3E.CC:3C.BD.C5.47.F6.C4,G3100,8,121071300000
3C.BD.C5.47.F6.CC:3C.BD.C5.49.AE.C4,G3100,8,121072100000
3C.BD.C5.49.AE.CC:3C.BD.C5.50.05.44,G3100,8,121070100000
3C.BD.C5.50.05.4C:3C.BD.C5.56.95.D4,G3100,8,121072000000
3C.BD.C5.56.95.DA:3C.BD.C5.60.3B.72,E3200,6,121072000000
3C.BD.C5.60.3B.74:3C.BD.C5.69.B8.CC,G3100,8,121081100000
3C.BD.C5.69.B8.D4:3C.BD.C5.71.07.FC,G3100,8,121082000000
3C.BD.C5.71.08.00:3C.BD.C5.76.8D.C8,G3100,8,121082600000
3C.BD.C5.76.8D.CC:3C.BD.C5.7D.27.9C,G3100,8,121090800000
3C.BD.C5.7D.27.A2:3C.BD.C5.85.4C.98,G3100,8,Unknown
3C.BD.C5.83.03.D4:3C.BD.C5.85.4C.9E,E3200,6,121091600001
3C.BD.C5.85.4C.A4:3C.BD.C5.8C.CB.C6,E3200,6,Unknown
3C.BD.C5.8C.CB.CC:3C.BD.C5.93.FF.8C,G3100,8,121101500000
3C.BD.C5.93.FF.92:BD.C5.93.9B.67.6C,G3100,8,Unknown
3C.BD.C5.9B.67.72:3C.BD.C5.A1.71.B2,G3100,8,121110500000
3C.BD.C5.A1.71.BA:3C.BD.C5.A3.FE.1C,E3200,6,121110500001
3C.BD.C5.A3.FE.22:3C.BD.C5.AD.16.64,E3200,6,Unknown
3C.BD.C5.AD.16.6A:3C.BD.C5.AE.AE.2E,E3200,6,122010100001
3C.BD.C5.AE.AE.34:3C.BD.C5.B4.2D.5E,E3200,6,121122100000
3C.BD.C5.B4.2D.62:3C.BD.C5.B5.09.9A,G3100,8,122021400000
3C.BD.C5.B5.09.A2:3C.BD.C5.B5.E5.9A,G3100,8,122031000000
3C.BD.C5.B5.E5.A2:3C.BD.C5.B8.0B.9A,G3100,8,122031100000
3C.BD.C5.B8.0B.A2:3C.BD.B8.C1.ED.A2,G3100,8,122031500000
3C.BD.C5.C1.ED.A4:3C.BD.C5.C2.40.1E,E3200,6,122031300000
3C.BD.C5.C2.40.24:3C.BD.C5.C2.89.E0,E3200,6,122031400000
3C.BD.C5.C2.89.E6:3C.BD.C5.CD.A1.1A,E3200,6,Unknown
3C.BD.C5.CD.A1.20:3C.BD.C5.CF.39.86,E3200,6,122052200000
3C.BD.C5.CF.39.8C:3C.BD.C5.D2.C8.06,E3200,6,122091400000
3C.BD.C5.D2.C8.0C:3C.BD.C5.D3.41.F2,E3200,6,122091500000
3C.BD.C5.D3.41.F8:3C.BD.C5.DA.E1.12,E3200,6,Unknown
3C.BD.C5.DA.E1.18:3C.BD.C5.DD.EE.80,E3200,6,122110900000
3C.BD.C5.DD.EE.86:3C.BD.C5.E5.83.8A,E3200,6,Unknown
3C.BD.C5.E5.83.90:3C.BD.C5.E8.44.B0,E3200,6,123011000000
3C.BD.C5.E8.34.96:3C.BD.C5.E8.44.B6,E3200,6,123030400000
3C.BD.C5.E8.44.BC:3C.BD.C5.F1.11.4A,E3200,6,Unknown
3C.BD.C5.F1.11.50:3C.BD.C5.F1.E1.E2,E3200,6,123042100000
3C.BD.C5.F1.E1.E8:3C.BD.C5.F4.B3.C2,E3200,6,123042600000
3C.BD.C5.F4.B3.C8:3C.BD.C5.F8.7A.CA,E3200,6,123052000000
3C.BD.C5.F8.7A.D0:3C.BD.C5.F9.F9.FE,E3200,6,123080600000

""".strip().splitlines()

# Helper to convert MAC string to integer
def mac_to_int(mac):
    return int(mac.replace(".", ""), 16)

# Helper to extract the last 3 octets as integer
def last_3_octets_to_int(mac):
    return int(mac.replace(".", "")[-6:], 16)

def format_date_from_serial(serial):
    if serial.lower() == 'unknown' or not serial.isdigit():
        return "Unknown"
    try:
        year = int(serial[1:3])
        month = int(serial[3:5])
        day = int(serial[5:7])
        # Normalize year
        year += 2000 if year < 100 else 0
        date = datetime(year, month, day)
        return date.strftime("%m-%d-%y")
    except Exception:
        return "Invalid"

def calculate_serial(mac_input):
    mac_input_clean = mac_input.upper().replace(":", ".").replace("-", ".")
    mac_value = mac_to_int(mac_input_clean)
    mac_last = last_3_octets_to_int(mac_input_clean)

    for line in mac_data:
        mac_range, model, step_str, serial_start = line.split(",")
        mac_start_str, mac_end_str = mac_range.split(":")
        step = int(step_str)
 
        mac_start_val = mac_to_int(mac_start_str)
        mac_end_val = mac_to_int(mac_end_str)

        if mac_start_val <= mac_value <= mac_end_val and serial_start != "Unknown":
            mac_start_last = last_3_octets_to_int(mac_start_str)
            delta = (mac_last - mac_start_last) // step
            calculated_serial = str(int(serial_start) + delta).zfill(12)
            date_str = format_date_from_serial(calculated_serial)
            return {
                "Given MAC": mac_input_clean,
                "Start MAC": mac_start_str,
                "Calculated Serial": calculated_serial,
                "Model": model,
                "Date": date_str
            }

    return {"error": "MAC not found in any known range or serial unknown."}

# Main loop
if __name__ == "__main__":
    user_mac = input("Enter a MAC address (e.g., 3C:BD:C5:09:3C:12): ")
    result = calculate_serial(user_mac)

    if "error" in result:
        print(result["error"])
    else:
        for k, v in result.items():
            print(f"{k}: {v}")



So this is where I am currently stuck. I doubt that the key generation algorithm is on the device. However, if anyone is able to make sense of the serial output that might help unlock the firmware, I would love to just have a look around for curiosity sake. The files such as 04a222.txt in usr>share>tr143 could be interesting. I will try to periodically scrape and update the password file, I originally thought it would have more entries by now.

Next Steps:
  • Calculate all MAC addresses for currently known date codes and update the python code
  • Analyze the data set to try to reduce key space (unused characters, common words, find the wordlist?)
  • Collect more complete entries for the dataset
  • Try to mount file system (ubi.img) in a proper Linux environment
  • Try to find older firmware
 

Attachments

  • Example MACs.png
    Example MACs.png
    291.9 KB · Views: 6
Last edited by a moderator:

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
I wanted to mention that the posted data set and python code both contain minor errors.

For the data set: I will be posting a new data set with revalidated data, additional columns, and new entries.
For the python code: I realized since the info in the date blocks is the same, we can output a lot of details about a specific device, including the correct keyspace for a dictionary attack based on MAC. I will also be sharing the various scripts that I’ve discussed once I clean them up a little bit.

For now if anyone can help with the firmware it would be greatly appreciated. Here is a bit more info in that regard.

I found a nice teardown of the device here: https://fccid.io/RAXG3100/Internal-Photos/Internal-Photos-4330446.pdf

Here we see the CPU chip is a BROADCOM BCM43684KRFBG. (product page)
G3100 Chip.png

From the UART output posted previously we know that it is running AArch64 Linux. Is the sha256 hash value just a check, or something that can be cracked?
Code:
## Loading kernel from FIT Image at 02000000 ...
  Using 'conf_lx_VERIZON-G3100' configuration
  Verifying Hash Integrity ... OK
  Trying 'kernel' kernel subimage
    Description:  4.19 kernel
    Type:        Kernel Image
    Compression:  lzma compressed
    Data Start:  0x0228c800
    Data Size:    3461392 Bytes = 3.3 MiB
    Architecture: AArch64
    OS:          Linux
    Load Address: 0x00100000
    Entry Point:  0x00100000
    Hash algo:    sha256
    Hash value:  77e40836ec218fa969f9d2bd572115ed9a7ef008cc75bfec4912354ce78a6349
  Verifying Hash Integrity ... sha256+ OK


The memory is TOSHIBA TH58NVG3S0HTA10 (data sheet). It looks like there test are pads to access the memory. Figuring out the layout, and dump directly from the chip is probably a bit above my skillset currently.
G3100 Memory.png

Part of the UART output posted earlier:
Code:
MEMC DRAM profile (memc_dram_profile_struct) values:
  dram_type    = DDR3
====================================================
PART values:
  part_speed_grade    = 1600 CL11
  part_size_Mbits    = 4096 (DRAM size in MegaBits)
  part_row_bits      = 15 (number of row bits)
  part_col_bits      = 10 (number of column bits)
  part_ba_bits        = 3 (number of bank bits)
  part_width_bits    = 16 (DRAM width in bits)
NUMER OF PARTS:
  part_num            = 1 (Number of parts)
TOTAL values:
  total_size_Mbits    = 4096 (DRAM size in MegaBits)
  total_cs_bits      = 0 (number of cs bits, for dual_rank mode)
  total_width_bits    = 16 (DRAM width in bits)
  total_burst_bytes  = 16 (Number of bytes per DRAM access)
  total_max_byte_addr = 0x1fffffff (Maximum/last DRAM byte address)
                        (Number of bits in total_max_byte_addr is 29)
                        (i.e. total_max_byte_addr goes from bit 0 to bit 28)


There are 2 boards inside the device. Each has an obvious UART, however I was only able to get output from 1. Unfortunately I don’t remember the pin layout, but I used a multimeter to find (+) and (-). I think RX/TX were right my first try, otherwise swap them. There is also possibly a JTAG connector, but I don’t have much experience with that.

Bad UART: Board without COAX connector.
Bad UART.png

Good UART: Board with the COAX connector
Good UART.png

There are several other chips on the boards such as, ZM5101A-CME3, Broadcom B50212E, ERF32, SEC 907(?), MXL3711 which I know very little about.
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
Just wanted to say fantastic work and please keep us updated! Not sure how we can help, as you seem to have all the skills necessary to see this project through. But if you find the keygen algorithm and are struggling to reverse it, post it here so we can all give it a go!
I'd typically trace the factory reset function back through the firmware, but it in this very specific case, perhaps a grep for %c%s%c or %d%s%d might bare fruit. (to find the printf function call at the end of the keygen)
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
Thanks @drsnooker, the for now words of encouragement and liking posts are helpful! I will be posting an update to my dataset in the next few days, when I make that post I will include some fun avenues to explore. For now here's the update python code that I promised! I worked on calculating the Start/End of the known Date Code blocks in the data set. I had a script to help with the actual calculations, but I did each one individually to oversee how everything was lining up.

Here’s the code I ended up with, I have named it Fios-F1nDr as a play on their SSID 😀

Python:
import pandas as pd
import os
from tabulate import tabulate


def parse_ref_data(ref_data_lines):
    ref_data = []
    for line in ref_data_lines:
        parts = line.split(',')
        
        # Ensure parts has enough elements
        if len(parts) < 11:
            print(f"Skipping line due to insufficient data: {line}")
            continue
        
        ref_data.append({
            'MAC_start': parts[0],
            'MAC_end': parts[1],
            'Serial_start': parts[2],
            'Serial_end': parts[3],
            'M2HR': parts[4] if parts[4] else "Unknown",
            'W_format': int(parts[5]) if parts[5].isdigit() else None,
            'W_len': int(parts[6]) if parts[6].isdigit() else None,
            'A_format': int(parts[7]) if parts[7].isdigit() else None,
            'A_len': int(parts[8]) if parts[8].isdigit() else None,
            'Date': parts[9] if len(parts) > 9 else '',
            'HW': parts[10] if len(parts) > 10 else '',
            'Model': parts[11] if len(parts) > 11 else ''
        })
    return ref_data

def mac_to_hex(mac):
    # Strip separators and convert to lowercase
    mac = mac.replace(':', '').replace('.', '').lower()
    return int(mac[-6:], 16)

def process_single_mac(mac, ref_data):
    # Strip separators and convert to lowercase
    mac_cleaned = mac.replace(':', '').replace('.', '').replace('-', '').lower()
    
    # Check for valid prefix
    valid_prefixes = ["04a222", "b8f853", "3cbdc5", "dcf51b"]
    if mac_cleaned[:6] not in valid_prefixes:

        return {"Model": "Unknown Device", "Calculated Serial": "Unknown Device"}
    # Skip calculation for DCF51B prefix
    if mac_cleaned[:6] == "dcf51b":
        return {"Calculated Serial": "Not enough data", "Model": "E3200", "HW": 1104,
                "W_format": 2, "W_len": "15", "A_format": 2, "A_len": "9"}
        
    
    # Find the relevant block
    for block in ref_data:
        mac_start_cleaned = block['MAC_start'].replace(':', '').replace('.', '').replace('-', '').lower()
        mac_end_cleaned = block['MAC_end'].replace(':', '').replace('.', '').replace('-', '').lower()
        
      
        # Compare cleaned MAC addresses
        if mac_start_cleaned <= mac_cleaned <= mac_end_cleaned:
            if block['Serial_start'] == "Unknown Block":
                return {"Calculated Serial": "Unknown Block", "Model": block['Model'], "Hardware": block['HW'],
                        "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len']}
            
          
            # Skip calculation if M2HR is unknown
            if block['M2HR'] == "Unknown":
                return {"Calculated Serial": "Unknown", "M2HR": "Unknown", "Model": block['Model'], "HW": block['HW'],
                        "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len']}
            
            # Calculate Serial
            serial_prefix = block['Serial_start'][0]
            serial_start_cleaned = block['Serial_start'][1:]
            
            mac_dec = mac_to_hex(mac)
            mac_start_dec = mac_to_hex(block['MAC_start'])
            serial_difference = (mac_dec - mac_start_dec) / int(block['M2HR'])
            calculated_serial = serial_prefix + str(int(serial_start_cleaned) + serial_difference)
            calculated_serial = calculated_serial[:16]
            
            return {"MAC_start": block['MAC_start'], "MAC_end": block['MAC_end'], "M2HR": block['M2HR'], "Calculated Serial": calculated_serial,
                    "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len'],
                    "Model": block['Model'], "Date": block['Date'], "HW": block['HW']}
    
    return {"Model": "Unknown Device", "Calculated Serial": "Unknown Device"}

def process_csv_file(file_path, ref_data, save_location):
    # Read the CSV file containing MAC addresses
    df = pd.read_csv(file_path)

    # Define the column titles for the output CSV
    columns = ["MAC_input", "MAC_start", "MAC_end", "M2HR", "Calculated Serial", "W_format", "W_len", "A_format", "A_len", "Model", "Date Code (YYMMDD)", "HW"]

    # Prepare a list to hold results
    results = []

    # Process each MAC address
    for mac in df:
        result = process_single_mac(mac.strip(), ref_data)  # Ensure whitespace is stripped
        result['MAC'] = mac  # Include the original MAC address

        # Format W_len and A_len using format_description
        w_len_formatted = format_description(result.get("W_format", ""), is_wifi=True)
        a_len_formatted = format_description(result.get("A_format", ""), is_wifi=False)
        
        # Append the values in the order of the defined columns
        results.append([
            result.get("MAC", ""),
            result.get("MAC_start", ""),
            result.get("MAC_end", ""),
            result.get("M2HR", ""),
            result.get("Calculated Serial", ""),
            w_len_formatted,
            result.get("W_len", ""),
            a_len_formatted,
            result.get("A_len", ""),
            result.get("Model", ""),
            result.get("Date", ""),
            result.get("HW", "")
        ])

    # Convert results to DataFrame
    results_df = pd.DataFrame(results, columns=columns)
    
    # Save the DataFrame to a CSV file
    results_df.to_csv(save_location, index=False)


def format_description(format_type, is_wifi=True):
    if is_wifi:
        return "wnwnw" if format_type == 1 else "w?-w?-w? " if format_type == 2 else ""
    else:
        return "wnw" if format_type == 1 else "<A-Z><0-9>" if format_type == 2 else ""


def print_formatted_table(result):
    
    print("                      Fios-F1nDr")
    # Organize the data into a 4x3 table
    table_data = [
        ["MAC Input", result.get("MAC", "")],
        ["MAC Block Start", result.get("MAC_start", "")],
        ["MAC Block End", result.get("MAC_end", "")],
        ["MAC 2 Hex Ratio", result.get("M2HR", "")],
        ["Calculated Serial", result.get("Calculated Serial", "")],
        ["Keyspace", "w = <word>  n = <number>\n? = single digit at end of random word"],
        ["WiFi Pass Format", format_description(result.get("W_format", ""), is_wifi=True)],
        ["WiFi Pass Length", result.get("W_len", "")],
        ["Admin Pass Format", format_description(result.get("A_format", ""), is_wifi=False)],
        ["Admin Pass Length", result.get("A_len", "")],
        ["Model Type", result.get("Model", "")],
        ["Date Code (YYMMDD)", result.get("Date", "")],
        ["Hardware", result.get("HW", "")]
    ]

    formatted_table = tabulate(table_data, tablefmt="grid")
    print(formatted_table)
    


def main():
    ref_data_lines = [
"04.A2.22.00.00.00,04.A2.22.AE.B8.7E,0,0,Unknown,1,0,1,0,0,0",
"04.A2.22.AE.B8.89,04.A2.22.BA.30.A2,G401119042900000,G401119042968331,11,1,16,1,16,190429,1102",
"04.A2.22.BA.30.A6,04.A2.22.C3.3A.BB,G401119061100000,G401119061153855,11,1,16,1,16,190611,1103",
"04.A2.22.C3.3A.C6,04.A2.22.CB.A8.3C,G401119061800000,G401119061850210,11,1,16,1,16,190618,1103",
"04.A2.22.CB.5B.F0,04.A2.22.CB.5B.F2,E301119062000000,0,Unknown,1,16,1,16,190620,1102",
"04.A2.22.CB.E9.6C,04.A2.22.DB.74.3E,G401119062600000,G40111906292598,11,1,16,1,16,190626,1103",
"04.A2.22.D3.FF.3A,04.A2.22.DC.D2.DE,G401119071600000,G401119071652588,11,1,16,1,16,190716,1103",
"04.A2.22.DC.D2.E0,04.A2.22.DC.D2.E2,E301119072900000,0,Unknown,1,16,1,16,190729,1102",
"04.A2.22.DF.45.32,04.A2.22.DF.45.34,E301119073010000,0,Unknown,1,16,1,16,190730,1102",
"04.A2.22.DF.DB.E0,04.A2.22.DF.DB.E2,E301119073000000,0,Unknown,1,16,1,16,190730,1102",
"04.A2.22.E1.1D.12,04.A2.22.EA.27.27,G401119081000000,G401119081053855,11,1,16,1,16,190810,1103",
"04.A2.22.E2.B5.72,04.A2.22.E5.5E.A1,G401119073100000,G401119073115853,11,1,16,1,16,190731,1103",
"04.A2.22.E5.5E.A2,04.A2.22.F3.31.E2,G401119081300000,G401119081382368,11,1,16,1,16,190813,1103",
"04.A2.22.F3.31.E2,04.A2.22.F8.0B.14,G401119082300000,G401119082328886,11,1,16,1,16,190823,1103",
"04.A2.22.F8.0B.1F,04.A2.22.F8.3E.DB,G401119081300000,G401119081396619,0.000115284648277019,1,16,1,16,190813,1103",
"04.A2.22.F8.3E.DC,04.A2.22.FA.2B.90,0,0,Unknown,1,0,1,0,0,0",
"04.A2.22.FA.2B.9A,04.A2.22.FB.F4.BF,G401119092000000,G401119092010639,11,1,16,1,16,190920,1103",
"04.A2.22.FB.F4.CA,04.A2.22.FF.FF.FF,0,0,Unknown,1,0,1,0,0,0",
"B8.F8.53.00.00.00,B8.F8.53.02.AD.9B,0,0,Unknown,1,0,,0,0,0",
"B8.F8.53.02.8D.41,B8.F8.53.05.93.61,G401119092620000,G401119092638016,11,1,16,1,16,190926,1103",
"B8.F8.53.05.93.61,B8.F8.53.07.27.6A,G401119101100000,G401119101109403,11,1,16,1,16,191011,1103",
"B8.F8.53.07.27.75,B8.F8.53.08.BF.CA,G401119101400000,G401119101409503,11,1,16,1,16,191014,1103",
"B8.F8.53.08.BF.D5,B8.F8.53.09.CF.B2,G401119101600000,G401119101606327,11,1,16,1,16,191016,1103",
"B8.F8.53.09.D0.15,B8.F8.53.0A.B2.DF,G401119101700000,G401119101705278,11,1,16,1,16,191017,1103",
"B8.F8.53.0A.B2.D6,B8.F8.53.0A.E0.54,0,0,Unknown,1,0,1,0,0,0",
"B8.F8.53.0A.E0.55,B8.F8.53.0B.49.17,G401119101800000,G401119101802438,11,1,16,1,16,191018,1103",
"B8.F8.53.0B.49.0E,B8.F8.53.1C.67.5C,0,0,Unknown,1,0,1,0,0,0",
"B8.F8.53.1C.67.5D,B8.F8.53.20.20.3D,G401119111200000,G401119111222176,11,1,16,1,16,191112,1103",
"B8.F8.53.20.20.3D,B8.F8.53.24.7C.D3,G401119110800000,G401119110825986,11,1,16,1,16,191108,1104",
"B8.F8.53.25.5A.AF,B8.F8.53.25.5A.BB,E301119111100000,0,Unknown,1,16,1,16,191111,1102",
"B8.F8.53.25.C1.09,B8.F8.53.28.DC.42,G401119111500000,G401119111518507,11,1,16,1,16,191115,1103",
"B8.F8.53.28.DC.49,B8.F8.53.2C.FC.75,G401119120200000,G401119120224580,11,1,16,1,16,191202,1103",
"B8.F8.53.2C.FC.75,B8.F8.53.2F.1D.C6,G401119120500000,G401119120512691,11,1,16,1,16,191205,1103",
"B8.F8.53.2F.1D.CD,B8.F8.53.34.F7.22,G401119120700000,G401119120734848,11,1,16,1,16,191207,1103",
"B8.F8.53.34.F7.2D,B8.F8.53.35.B1.B4,G401119121700000,G401119121704341,11,1,16,1,16,191217,1103",
"B8.F8.53.35.B1.BD,B8.F8.53.3B.0B.27,G401119120900000,G401119120931870,11,1,16,1,16,191209,1103",
"B8.F8.53.3B.0B.31,B8.F8.53.3C.A3.86,G401120010700000,G401120010709503,11,1,16,1,16,200107,1103",
"B8.F8.53.3C.A3.8B,B8.F8.53.3E.3B.EB,G401120010900000,G401120010909504,11,1,16,1,16,200109,1103",
"B8.F8.53.3E.3B.F1,B8.F8.53.40.1F.62,G401120011000000,G401120011011251,11,1,16,1,16,200110,1103",
"B8.F8.53.40.1F.65,B8.F8.53.43.05.09,G401120011300000,G401120011317260,11,1,16,1,16,200113,1103",
"B8.F8.53.43.05.11,B8.F8.53.44.90.98,G401120011600000,G401120011609205,11,1,16,1,16,200116,1103",
"B8.F8.53.44.90.9F,B8.F8.53.4C.AE.CE,G401120010200000,G401120010248365,11,1,16,1,16,200102,1103",
"B8.F8.53.4C.AE.D6,B8.F8.53.51.F0.93,G402120021000000,0,Unknown,1,16,1,16,200210,1104",
"B8.F8.53.51.B9.01,B8.F8.53.51.F7.F2,G402120022400000,0,Unknown,1,16,1,16,200224,1104",
"B8.F8.53.52.19.01,B8.F8.53.52.9E.82,G402120022500000,0,Unknown,1,16,1,16,200225,1104",
"B8.F8.53.52.E2.01,B8.F8.53.52.F7.12,G402120022600000,0,Unknown,1,16,1,16,200226,1104",
"B8.F8.53.54.6E.01,B8.F8.53.54.B6.FA,G402120022900000,0,Unknown,1,16,1,16,200229,1104",
"B8.F8.53.55.34.01,B8.F8.53.55.58.52,G402120030300000,0,Unknown,1,16,1,16,200303,1104",
"B8.F8.53.57.8C.41,B8.F8.53.57.D8.C2,G402120031000000,0,Unknown,2,15,2,16,200310,1104",
"B8.F8.53.59.7B.41,B8.F8.53.59.87.62,G402120031300000,0,Unknown,1,16,1,16,200313,1104",
"B8.F8.53.5A.41.41,B8.F8.53.5A.8A.C0,G402120031400000,0,Unknown,1,16,1,16,200314,1004",
"B8.F8.53.5B.CD.41,B8.F8.53.5F.4B.89,G402120031800000,G402120031828617,8,2,15,2,16,200318,1104",
"B8.F8.53.5F.4B.90,B8.F8.53.60.11.88,G402120040800000,G402120040806335,8,2,15,2,16,200408,1104",
"B8.F8.53.60.11.90,B8.F8.53.62.00.88,G402120040900000,G402120040915839,8,2,15,2,16,200409,1104",
"B8.F8.53.62.00.90,B8.F8.53.62.C6.88,G402120041200000,G402120041206335,8,2,15,2,16,200412,1104",
"B8.F8.53.62.C6.90,B8.F8.53.67.0D.C8,G402120032500000,G402120032535047,8,2,15,2,16,200325,1104",
"B8.F8.53.67.0D.D0,B8.F8.53.67.D3.C8,G402120050600000,G402120050606336,8,2,15,2,16,200506,1104",
"B8.F8.53.67.D3.D0,B8.F8.53.69.5F.C8,G402120050800000,G402120050812671,8,2,15,2,16,200508,1104",
"B8.F8.53.68.99.D0,B8.F8.53.68.B8.B1,G402120050900000,G402120050900989,8,2,15,2,16,200509,1104",
"B8.F8.53.69.5F.D0,B8.F8.53.6D.80.88,G402120051100000,G402120051133816,8,2,15,2,16,200511,1104",
"B8.F8.53.6D.80.90,B8.F8.53.70.35.88,G402120060600000,G402120060622176,8,2,15,2,16,200606,1104",
"B8.F8.53.70.35.90,B8.F8.53.72.0F.59,G402120052600000,G402120052634649,8,2,15,2,16,200526,1104",
"B8.F8.53.73.AA.58,B8.F8.53.73.B9.F9,G402120070200000,G402120070200501,8,2,15,2,16,200702,1104",
"B8.F8.53.74.70.58,B8.F8.53.75.36.50,G402120070300000,G402120070306335,8,2,15,2,16,200703,1104",
"B8.F8.53.75.36.58,B8.F8.53.77.52.70,G402120070500000,G402120070517284,8,2,15,2,16,200705,1104",
"B8.F8.53.77.52.78,B8.F8.53.7D.EB.B0,G402120062600000,G402120062654056,8,2,15,2,16,200626,1104",
"B8.F8.53.7D.EB.B8,B8.F8.53.7F.14.B0,G402120072300000,G402120072309503,8,2,15,2,16,200723,1104",
"B8.F8.53.7F.14.B8,B8.F8.53.85.C5.38,G402120071800000,G402120071854800,8,2,15,2,16,200718,1104",
"B8.F8.53.85.C5.40,B8.F8.53.8B.AB.A8,G402120081100000,G402120081148333,8,2,15,2,16,200811,1104",
"B8.F8.53.8B.AB.B0,B8.F8.53.95.FB.A8,G402120081200000,G402120081284479,8,2,15,2,16,200812,1104",
"B8.F8.53.95.FB.B0,B8.F8.53.9E.AF.28,G402120082000000,G402120082071279,8,2,15,2,16,200820,1104",
"B8.F8.53.9E.AF.32,B8.F8.53.A0.F4.30,E302120082400000,E302120082424789,6,2,15,2,16,200824,1103",
"B8.F8.53.A0.F4.34,B8.F8.53.A1.62.2C,G402120082800000,G402120082803519,8,2,15,2,16,200828,1104",
"B8.F8.53.A1.62.34,B8.F8.53.AA.85.1C,G402120083000000,G402120083074845,8,2,15,2,16,200830,1104",
"B8.F8.53.AA.85.1E,B8.F8.53.AD.F8.DA,E302120090800000,E302120090837706,6,2,15,2,16,200908,1103",
"B8.F8.53.AD.F8.DC,B8.F8.53.B6.85.24,G402120091500000,G402120091570025,8,2,15,2,16,200915,1104",
"B8.F8.53.B6.85.26,B8.F8.53.B7.01.8E,E302120091500000,E302120091505308,6,2,15,2,16,200915,1103",
"B8.F8.53.B7.01.8F,B8.F8.53.B7.98.C3,0,0,Unknown,2,0,2,0,0,0",
"B8.F8.53.B7.98.C4,B8.F8.53.B8.06.BC,G402120101500000,G402120101503519,8,2,15,2,16,201015,1104",
"B8.F8.53.B8.06.C4,B8.F8.53.BB.08.BC,G402120101600000,G402120101624639,8,2,15,2,16,201016,1104",
"B8.F8.53.BB.08.C4,B8.F8.53.BB.76.BC,G402120101800000,G402120101803519,8,2,15,2,16,201018,1104",
"B8.F8.53.BB.76.C4,B8.F8.53.C0.9E.BC,G402120101900000,G402120101942240,8,2,15,2,16,201019,1104",
"B8.F8.53.C0.9E.C4,B8.F8.53.C2.56.C4,G402120102400000,G402120102414080,8,2,15,2,16,201024,1104",
"B8.F8.53.C2.56.C4,B8.F8.53.C5.D1.1C,G402120102500000,G402120102528491,8,2,15,2,16,201025,1104",
"B8.F8.53.C5.D1.20,B8.F8.53.CD.91.D0,G402120100700000,G402120100763510,8,2,15,2,16,201007,1104",
"B8.F8.53.CD.91.D2,B8.F8.53.CE.C3.5A,E302120100700000,0,6,2,15,2,16,201007,1103",
"B8.F8.53.CE.C3.60,B8.F8.53.B7.98.BE,0,0,Unknown,2,0,2,0,0,0",
"B8.F8.53.CE.D4.C8,B8.F8.53.D3.58.68,G402120101300000,G402120101336980,8,2,15,2,16,201013,1104",
"B8.F8.53.D3.58.6A,B8.F8.53.D5.EC.64,E302120101500000,E302120101528159,6,2,15,2,16,201015,1103",
"B8.F8.53.D5.EC.68,B8.F8.53.D7.36.60,G402120111200000,G402120111210559,8,2,15,2,16,201112,1104",
"B8.F8.53.D7.36.68,B8.F8.53.DA.38.60,G402120111300000,G402120111324640,8,2,15,2,16,201113,1104",
"B8.F8.53.DA.38.68,B8.F8.53.DC.01.D0,G402120111700000,G402120111714637,8,2,15,2,16,201117,1104",
"B8.F8.53.DC.01.D2,B8.F8.53.DC.6F.CA,E302120111300000,E302120111304693,6,2,15,2,16,201113,1103",
"B8.F8.53.DC.6F.D0,B8.F8.53.E3.C6.D8,G402120111300000,G402120111360129,8,2,15,2,16,201113,1104",
"B8.F8.53.E3.C6.E0,B8.F8.53.EB.14.D8,G402120120200000,G402120120259839,8,2,15,2,16,201202,1104",
"B8.F8.53.EB.14.E0,B8.F8.53.F3.08.C8,G402120121000000,G402120121065150,8,2,15,2,16,201210,1104",
"B8.F8.53.F3.08.D0,B8.F8.53.F8.30.C8,G402120122300000,G402120122342240,8,2,15,2,16,201223,1104",
"B8.F8.53.F8.30.D0,B8.F8.53.FA.C4.C8,G402121011800000,G402121011821120,8,2,15,2,16,210118,1104",
"B8.F8.53.FA.C4.D0,B8.F8.53.FF.BF.F8,G402121010600000,G402121010640805,8,2,15,2,16,210106,1104",
"B8.F8.53.FF.BF.F9,B8.F8.53.FF.FF.FF,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.00.00.00,3C.BD.C5.01.25.57,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.01.25.58,3C.BD.C5.01.F5.78,G402121011510000,G402121011516660,8,2,15,2,16,210115,1104",
"3C.BD.C5.09.3B.F8,3C.BD.C5.0B.B4.78,G402121030100000,G402121030120240,8,2,15,2,16,210301,1104",
"3C.BD.C5.0B.B4.7A,3C.BD.C5.0B.BC.FD,E302121030300000,E302121030300364,6,2,15,2,16,210303,1103",
"3C.BD.C5.0C.30.38,3C.BD.C5.16.4A.C0,G402121021700000,G402121021782769,8,2,15,2,16,210217,1104",
"3C.BD.C5.16.4A.C8,3C.BD.C5.1E.E5.E8,G402121031200000,G402121031298659,8,2,15,2,16,210312,1104",
"3C.BD.C5.1E.E5.EA,3C.BD.C5.22.55.E6,E302121031200000,E302121031237546,6,2,15,2,16,210312,1103",
"3C.BD.C5.22.55.E8,3C.Bd.C5.2B.5B.E0,G402121033100000,G402121033173919,8,2,15,2,16,210331,1104",
"3C.BD.C5.2B.5B.E8,3C.BD.C5.31.1E.68,G402121041400000,G402121041447184,8,2,15,2,16,210414,1104",
"3C.BD.C5.31.1E.69,3C.BD.C5.35.36.88,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.35.36.90,3C.BD.C5.3C.6B.58,G402121051500000,G402121051559033,8,2,15,2,16,210515,1104",
"3C.BD.C5.3C.6B.59,3C.BD.C5.37.5C.88,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.37.5C.90,3C.BD.C5.3D.D3.38,G402121051800000,G402121051852950,8,2,15,2,16,210518,1104",
"3C.BD.C5.3D.D3.40,3C.BD.C5.42.27.40,G402121060700000,G402121060735456,8,2,15,2,16,210607,1104",
"3C.BD.C5.42.27.4C,3C.BD.C5.46.3E.C4,G402121062200000,G402121062233519,8,2,15,2,16,210622,1104",
"3C.BD.C5.46.3E.CC,3C.BD.C5.47.F6.C4,G402121071300000,G402121071314079,8,2,15,2,16,210713,1104",
"3C.BD.C5.47.F6.CC,3C.BD.C5.49.AE.C4,G402121072100000,G402121072114079,8,2,15,2,16,210721,1104",
"3C.BD.C5.49.AE.CC,3C.BD.C5.50.05.4C,G402121070100000,G402121070151920,8,2,15,2,16,210701,1104",
"3C.BD.C5.50.05.4C,3C.BD.C5.57.37.CC,G402121072000000,G402121072058960,8,2,15,2,9,210720,1104",
"3C.BD.C5.57.37.CE,3C.BD.C5.59.22.C0,E302121072000000,E302121072020947,6,2,15,2,9,210720,1103",
"3C.BD.C5.59.24.74,3C.BD.C5.60.3B.6C,G402121080200000,G402121080258079,8,2,15,2,9,210802,1104",
"3C.BD.C5.60.3B.74,3C.BD.C5.67.20.6C,G402121081100000,G402121081156479,8,2,15,2,9,210811,1104",
"3C.BD.C5.67.20.74,3C.BD.C5.69.B8.CC,G402121081160000,G402121081181259,8,2,15,2,9,210811,1104",
"3C.BD.C5.69.B8.D4,3C.BD.C5.71.07.FC,G402121082000000,G402121082059877,8,2,15,2,9,210820,1104",
"3C.BD.C5.71.08.00,3C.BD.C5.76.8D.C8,G402121082600000,G402121082645241,8,2,15,2,9,210826,1104",
"3C.BD.C5.76.8D.CD,3C.BD.C5.7E.9C.45,G402121090800000,G402121090865999,8,2,15,2,9,210908,1104",
"3C.BD.C5.7E.9C.4C,3C.BD.C5.83.03.CC,G402121091600000,G402121091636080,8,2,15,2,9,210916,1104",
"3C.BD.C5.83.03.CE,3C.BD.C5.85.4C.A4,E302121091600000,E302121091624953,6,2,15,2,9,210916,1103",
"3C.BD.C5.85.4C.AA,3C.BD.C5.8C.CB.CB,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.8C.CB.CC,3C.BD.C5.93.FF.94,G402121101500000,G402121101559001,8,2,15,2,9,211015,1104",
"3C.BD.C5.93.FF.9C,3C.BD.C5.9B.67.71,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.9B.67.72,3C.BD.C5.A1.71.B2,G402121110500000,G402121110549480,8,2,15,2,9,211105,1104",
"3C.BD.C5.A1.71.B4,3C.BD.C5.A3.FE.22,E302121110500000,E302121110527837,6,2,15,2,9,211105,1103",
"3C.BD.C5.A3.FE.28,3C.BD.C5.AD.16.63,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.AD.16.64,3C.BD.C5.AE.AE.34,E302122010100000,E30212201017400,6,2,15,2,9,220101,1103",
"3C.BD.C5.AE.AE.34,3C.BD.C5.B4.2D.5E,E302121122100000,E302121122160039,6,2,15,2,9,211221,1103",
"3C.BD.C5.B4.2D.62,3C.BD.C5.B5.09.9A,G402122021400000,G402122021407047,8,2,15,2,9,220214,1104",
"3C.BD.C5.B5.09.A2,3C.BD.C5.B5.E5.9A,G402122031000000,G402122031007039,8,2,15,2,9,220310,1104",
"3C.BD.C5.B5.E5.A2,3C.BD.C5.B8.0B.9A,G402122031100000,G402122031117599,8,2,15,2,9,220311,1104",
"3C.BD.C5.B8.0B.A2,3C.BD.C5.C1.ED.A2,G402122031500000,G402122031580960,8,2,15,2,9,220315,1104",
"3C.BD.C5.C1.ED.A4,3C.BD.C5.C2.40.1E,E302122031300000,E302122031303519,6,2,15,2,9,220313,1103",
"3C.BD.C5.C2.40.24,3C.BD.C5.C2.89.E6,E302122031400000,E302122031403147,6,2,15,2,9,220314,1103",
"3C.BD.C5.C2.89.EC,3C.BD.C5.CD.A1.19,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.CD.A1.20,3C.BD.C5.CF.39.86,E302122052200000,E302122052217426,6,2,15,2,9,220522,1103",
"3C.BD.C5.CF.39.8C,3C.BD.C5.D2.C8.06,E302122091400000,E302122091438847,6,2,15,2,9,220914,1103",
"3C.BD.C5.D2.C8.0C,3C.BD.C5.DA.E1.12,E302122091500000,E302122091588450,6,2,15,2,9,220915,1103",
"3C.BD.C5.DA.E1.18,3C.BD.C5.DD.EE.86,E302122110900000,E302122110933341,6,2,15,2,9,221109,1103",
"3C.BD.C5.DD.EE.8C,3C.BD.C5.E5.83.97,0,0,Unknown,2,0,2,0,0,0",
"3C.BD.C5.E5.83.98,3C.BD.C5.E8.34.8A,E302123011000000,E302123011029396,6,2,15,2,9,230110,1103",
"3C.BD.C5.E8.34.90,3C.BD.C5.F1.11.4A,E302123030400000,E302123030496800,6,2,15,2,9,230304,1103",
"3C.BD.C5.F1.11.50,3C.BD.C5.F1.E1.E2,E302123042100000,E302123042108899,6,2,15,2,9,230421,1103",
"3C.BD.C5.F1.E1.E8,3C.BD.C5.F4.B3.C2,E302123042600000,E302123042630800,6,2,15,2,9,230426,1103",
"3C.BD.C5.F4.B3.C8,3C.BD.C5.F8.7A.CA,E302123052000000,E302123052041260,6,2,15,2,9,230520,1103",
"3C.BD.C5.F8.7A.D0,3C.BD.C5.F8.A3.63,E302123080600000,E302123080601732,6,2,15,2,9,230806,1103",
"3C.BD.C5.F8.A3.64,3C.BD.C5.FF.FF.FF,0,0,Unknown,2,0,2,0,0,0",
"DC.F5.1B.5F.EA.4F,DC.F5.1B.64.2B.10,Unknown,Unknown,0,2,16,2,9,223550,1103",
"DC.F5.1B.64.2B.1A,DC.F5.1B.64.34.D5,E302123103100000,0,6,2,15,2,9,231031,1103",
"DC.F5.1B.64.54.5A,DC.F5.1B.64.57.B5,E302123110200000,0,6,2,15,2,9,231102,1103",
"DC.F5.1B.67.F4.7A,DC.F5.1B.68.6F.Ff,E302123113000000,0,6,2,15,2,9,231130,1103",
"DC.F5.1B.5D.64.60,DC.F5.1B.60.14.57,AA63332935300000,0,6,2,15,2,9,329353,1103",]
    
    ref_data = parse_ref_data(ref_data_lines)
    
    user_input = input("Enter one or more MAC addresses (comma-separated) or the path to a.csv file: ")
    
    if user_input.lower().endswith('.csv'):
        while not os.path.isfile(user_input) or not user_input.lower().endswith('.csv'):
            user_input = input("Please enter a valid.csv file path: ")
        
        save_location = input("Enter the save location for the output.csv file: ")
        process_csv_file(user_input, ref_data, save_location)
    else:
        mac_addresses = user_input.split(',')
        for mac in mac_addresses:
            mac = mac.strip()  # Remove any leading/trailing whitespace
            result = process_single_mac(mac, ref_data)
            result['MAC'] = mac  # Include the original MAC address
            # Print the result in a formatted table
            print_formatted_table(result)

        
if __name__ == "__main__":
    main()

Output:
Code:
                      Fios-F1nDr
+--------------------+----------------------------------------+
| MAC Input          | B8.F8.53.E2.EC.60                      |
+--------------------+----------------------------------------+
| MAC Block Start    | B8.F8.53.DC.6F.D0                      |
+--------------------+----------------------------------------+
| MAC Block End      | B8.F8.53.E3.C6.D8                      |
+--------------------+----------------------------------------+
| MAC 2 Hex Ratio    | 8                                      |
+--------------------+----------------------------------------+
| Calculated Serial  | G402120111353138                       |
+--------------------+----------------------------------------+
| Keyspace           | w = <word>  n = <number>               |
|                    | ? = single digit at end of random word |
+--------------------+----------------------------------------+
| WiFi Pass Format   | w?-w?-w?                               |
+--------------------+----------------------------------------+
| WiFi Pass Length   | 15                                     |
+--------------------+----------------------------------------+
| Admin Pass Format  | <A-Z><0-9>                             |
+--------------------+----------------------------------------+
| Admin Pass Length  | 16                                     |
+--------------------+----------------------------------------+
| Model Type         |                                        |
+--------------------+----------------------------------------+
| Date Code (YYMMDD) | 201113                                 |
+--------------------+----------------------------------------+
| Hardware           | 1104                                   |
+--------------------+----------------------------------------+


Using the script is pretty simple, just run it! The script will prompt you to "Enter one or more MAC addresses (comma-separated) or the path to a.csv file:”. As it says, it will accept a variety of inputs. You can type a single MAC address, a few MACs separated by a comma, or feed it a list of MACs from a comma separated .csv file. The script strips out the characters . - : so it will accept a variety of formats. If you give it a .csv file, it will prompt you for a save path, which will be something like "/path/fiender.csv”. The script will then use “ref_data_lines” as the data to calculate the information. Everything should be human readable and commented, but let me know if you have any questions. For future updates I will only post the new ref_data_lines because that should be all that needs changed. The script currently doesn’t output the model because I forgot to include that info in the reference data. I will add it next update, but for now if the serial starts with G it’s G3100, if its E its E3200

Once I had the data and the script, I collected 25 new entries for the data base and checked them against the script. How did we do? OOF, only 36% of the new entries were calculated correctly. At least the correct hits show that we’re on the right path...

Correct 9 36%
Incorrect 10 40%
Unknown Block 4 16%
Unknown Device 1 4%
Not Enough data 1 4%

This actually isn’t a bad thing though, the script is making some assumptions about each MAC block, and these entries provide us with new date codes. After working the new entries into the data, we test the script again. Much better, with 80% of the hits correct. The ones we’re missing are because we don’t have enough information to calculate the block space just yet. Sometimes just a single entry can remove a bunch of the unknown, which means that continuing to collect data will continue to improve the script. The good news is that this only really pertains to calculating the correct Serial Number, which doesn’t matter much at this point without a keygen algorithm. The script will still output everything we know, including the Password Keyspace as this info doesn’t actually change too much across devices. I will keep track of these stats as I continue to update the data base to see how close we’re getting to finding all of the blocks. Although we don’t have the algorithm, all we need to do is drop it into the script and we have a complete recovery tool!

Correct 20 80%
Incorrect 0 0%
Unknown Block 4 16%
Unknown Device 0 0%
Not Enough data 1 4%
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
Sorry I realized that I had the data for the Password keyspace a little messed up, here is an updated script with it fixed. I also included the Model data this time, and added a single line at the end of the proccess_csv_file function to print a bit of information.


Python:
import pandas as pd
import os
from tabulate import tabulate


def parse_ref_data(ref_data_lines):
    ref_data = []
    for line in ref_data_lines:
        parts = line.split(',')
       
        # Ensure parts has enough elements
        if len(parts) < 11:
            print(f"Skipping line due to insufficient data: {line}")
            continue
       
        ref_data.append({
            'MAC_start': parts[0],
            'MAC_end': parts[1],
            'Serial_start': parts[2],
            'Serial_end': parts[3],
            'M2HR': parts[4] if parts[4] else "Unknown",
            'W_format': int(parts[5]) if parts[5].isdigit() else None,
            'W_len': int(parts[6]) if parts[6].isdigit() else None,
            'A_format': int(parts[7]) if parts[7].isdigit() else None,
            'A_len': int(parts[8]) if parts[8].isdigit() else None,
            'Date': parts[9] if len(parts) > 9 else '',
            'HW': parts[10] if len(parts) > 10 else '',
            'Model': parts[11] if len(parts) > 11 else ''
        })
    return ref_data

def mac_to_hex(mac):
    # Strip separators and convert to lowercase
    mac = mac.replace(':', '').replace('.', '').lower()
    return int(mac[-6:], 16)

def process_single_mac(mac, ref_data):
    # Strip separators and convert to lowercase
    mac_cleaned = mac.replace(':', '').replace('.', '').replace('-', '').lower()
   
    # Check for valid prefix
    valid_prefixes = ["04a222", "b8f853", "3cbdc5", "dcf51b"]
    if mac_cleaned[:6] not in valid_prefixes:

        return {"Model": "Unknown Device", "Calculated Serial": "Unknown Device"}
    # Skip calculation for DCF51B prefix
    if mac_cleaned[:6] == "dcf51b":
        return {"Calculated Serial": "Not enough data", "Model": "E3200", "HW": 1104,
                "W_format": 2, "W_len": "15", "A_format": 2, "A_len": 9}
       
   
    # Find the relevant block
    for block in ref_data:
        mac_start_cleaned = block['MAC_start'].replace(':', '').replace('.', '').replace('-', '').lower()
        mac_end_cleaned = block['MAC_end'].replace(':', '').replace('.', '').replace('-', '').lower()
       
     
        # Compare cleaned MAC addresses
        if mac_start_cleaned <= mac_cleaned <= mac_end_cleaned:
            if block['Serial_start'] == "Unknown Block":
                return {"Calculated Serial": "Unknown Block", "Model": block['Model'], "Hardware": block['HW'],
                        "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len']}
           
         
            # Skip calculation if M2HR is unknown
            if block['M2HR'] == "Unknown":
                return {"Calculated Serial": "Unknown", "M2HR": "Unknown", "Model": block['Model'], "HW": block['HW'],
                        "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len']}
           
            # Calculate Serial
            serial_prefix = block['Serial_start'][0]
            serial_start_cleaned = block['Serial_start'][1:]
           
            mac_dec = mac_to_hex(mac)
            mac_start_dec = mac_to_hex(block['MAC_start'])
            serial_difference = (mac_dec - mac_start_dec) / int(block['M2HR'])
            calculated_serial = serial_prefix + str(int(serial_start_cleaned) + serial_difference)
            calculated_serial = calculated_serial[:16]
           
            return {"MAC_start": block['MAC_start'], "MAC_end": block['MAC_end'], "M2HR": block['M2HR'], "Calculated Serial": calculated_serial,
                    "W_format": block['W_format'], "W_len": block['W_len'], "A_format": block['A_format'], "A_len": block['A_len'],
                    "Model": block['Model'], "Date": block['Date'], "HW": block['HW']}
   
    return {"Model": "Unknown Device", "Calculated Serial": "Unknown Device"}

def process_csv_file(file_path, ref_data, save_location):
    # Read the CSV file containing MAC addresses
    df = pd.read_csv(file_path)

    # Define the column titles for the output CSV
    columns = ["MAC_input", "MAC_start", "MAC_end", "M2HR", "Calculated Serial", "W_format", "W_len", "A_format", "A_len", "Model", "Date Code (YYMMDD)", "HW"]

    # Prepare a list to hold results
    results = []

    # Process each MAC address
    for mac in df:
        result = process_single_mac(mac.strip(), ref_data)  # Ensure whitespace is stripped
        result['MAC'] = mac  # Include the original MAC address

        # Format W_len and A_len using format_description
        W_format = format_description(result.get("W_format", ""), is_wifi=True)
        A_format = format_description(result.get("A_format", ""), is_wifi=False)
       
        # Append the values in the order of the defined columns
        results.append([
            result.get("MAC", ""),
            result.get("MAC_start", ""),
            result.get("MAC_end", ""),
            result.get("M2HR", ""),
            result.get("Calculated Serial", ""),
            W_format,
            result.get("W_len", ""),
            A_format,
            result.get("A_len", ""),
            result.get("Model", ""),
            result.get("Date", ""),
            result.get("HW", "")
        ])

    # Convert results to DataFrame
    results_df = pd.DataFrame(results, columns=columns)
   
    # Save the DataFrame to a CSV file
    results_df.to_csv(save_location, index=False)
    print(str(len(results_df)) + " MACs proccessed, saved to " + save_location)


def format_description(format_type, is_wifi=True):
    if is_wifi:
        return "wnwnw" if format_type == 1 else "w?-w?-w? " if format_type == 2 else ""
    else:
        return "wnw" if format_type == 1 else "<A-Z><0-9>" if format_type == 2 else ""


def print_formatted_table(result):
   
    print("                      Fios-F1NdR")
    # Organize the data into a 4x3 table
    table_data = [
        ["MAC Input", result.get("MAC", "")],
        ["MAC Block Start", result.get("MAC_start", "")],
        ["MAC Block End", result.get("MAC_end", "")],
        ["MAC 2 Hex Ratio", result.get("M2HR", "")],
        ["Calculated Serial", result.get("Calculated Serial", "")],
        ["Keyspace", "w = <word>  n = <number>\n? = single digit at end of random word"],
        ["WiFi Pass Format", format_description(result.get("W_format", ""), is_wifi=True)],
        ["WiFi Pass Length", result.get("W_len", "")],
        ["Admin Pass Format", format_description(result.get("A_format", ""), is_wifi=False)],
        ["Admin Pass Length", result.get("A_len", "")],
        ["Model Type", result.get("Model", "")],
        ["Date Code (YYMMDD)", result.get("Date", "")],
        ["Hardware", result.get("HW", "")]
    ]

    formatted_table = tabulate(table_data, tablefmt="grid")
    print(formatted_table)
   


def main():
    ref_data_lines = [
"04.A2.22.00.00.00,04.A2.22.AE.B8.7E,0,0,Unknown,1,0,1,0,0,0,Unknown",
"04.A2.22.AE.B8.89,04.A2.22.BA.30.A2,G401119042900000,G401119042968331,11,1,16,1,16,190429,1102,G3100",
"04.A2.22.BA.30.A6,04.A2.22.C3.3A.BB,G401119061100000,G401119061153855,11,1,16,1,16,190611,1103,G3100",
"04.A2.22.C3.3A.C6,04.A2.22.CB.A8.3C,G401119061800000,G401119061850210,11,1,16,1,16,190618,1103,G3100",
"04.A2.22.CB.5B.F0,04.A2.22.CB.5B.F2,E301119062000000,0,Unknown,1,16,1,16,190620,1102,E3200",
"04.A2.22.CB.E9.6C,04.A2.22.DB.74.3E,G401119062600000,G40111906292598,11,1,16,1,16,190626,1103,G3100",
"04.A2.22.D3.FF.3A,04.A2.22.DC.D2.DE,G401119071600000,G401119071652588,11,1,16,1,16,190716,1103,G3100",
"04.A2.22.DC.D2.E0,04.A2.22.DC.D2.E2,E301119072900000,0,Unknown,1,16,1,16,190729,1102,E3200",
"04.A2.22.DF.45.32,04.A2.22.DF.45.34,E301119073010000,0,Unknown,1,16,1,16,190730,1102,E3200",
"04.A2.22.DF.DB.E0,04.A2.22.DF.DB.E2,E301119073000000,0,Unknown,1,16,1,16,190730,1102,E3200",
"04.A2.22.E1.1D.12,04.A2.22.EA.27.27,G401119081000000,G401119081053855,11,1,16,1,16,190810,1103,G3100",
"04.A2.22.E2.B5.72,04.A2.22.E5.5E.A1,G401119073100000,G401119073115853,11,1,16,1,16,190731,1103,G3100",
"04.A2.22.E5.5E.A2,04.A2.22.F3.31.E2,G401119081300000,G401119081382368,11,1,16,1,16,190813,1103,G3100",
"04.A2.22.F3.31.E2,04.A2.22.F8.0B.14,G401119082300000,G401119082328886,11,1,16,1,16,190823,1103,G3100",
"04.A2.22.F8.0B.1F,04.A2.22.F8.3E.DB,G401119081300000,G401119081396619,0.000115284648277019,1,16,1,16,190813,1103,G3100",
"04.A2.22.F8.3E.DC,04.A2.22.FA.2B.90,0,0,Unknown,1,0,1,0,0,0,Unknown",
"04.A2.22.FA.2B.9A,04.A2.22.FB.F4.BF,G401119092000000,G401119092010639,11,1,16,1,16,190920,1103,G3100",
"04.A2.22.FB.F4.CA,04.A2.22.FF.FF.FF,0,0,Unknown,1,0,1,0,0,0,Unknown",
"B8.F8.53.00.00.00,B8.F8.53.02.AD.9B,0,0,Unknown,1,0,,0,0,0,Unknown",
"B8.F8.53.02.8D.41,B8.F8.53.05.93.61,G401119092620000,G401119092638016,11,1,16,1,16,190926,1103,G3100",
"B8.F8.53.05.93.61,B8.F8.53.07.27.6A,G401119101100000,G401119101109403,11,1,16,1,16,191011,1103,G3100",
"B8.F8.53.07.27.75,B8.F8.53.08.BF.CA,G401119101400000,G401119101409503,11,1,16,1,16,191014,1103,G3100",
"B8.F8.53.08.BF.D5,B8.F8.53.09.CF.B2,G401119101600000,G401119101606327,11,1,16,1,16,191016,1103,G3100",
"B8.F8.53.09.D0.15,B8.F8.53.0A.B2.DF,G401119101700000,G401119101705278,11,1,16,1,16,191017,1103,G3100",
"B8.F8.53.0A.B2.D6,B8.F8.53.0A.E0.54,0,0,Unknown,1,0,1,0,0,0,Unknown",
"B8.F8.53.0A.E0.55,B8.F8.53.0B.49.17,G401119101800000,G401119101802438,11,1,16,1,16,191018,1103,G3100",
"B8.F8.53.0B.49.0E,B8.F8.53.1C.67.5C,0,0,Unknown,1,0,1,0,0,0,Unknown",
"B8.F8.53.1C.67.5D,B8.F8.53.20.20.3D,G401119111200000,G401119111222176,11,1,16,1,16,191112,1103,G3100",
"B8.F8.53.20.20.3D,B8.F8.53.24.7C.D3,G401119110800000,G401119110825986,11,1,16,1,16,191108,1104,G3100",
"B8.F8.53.25.5A.AF,B8.F8.53.25.5A.BB,E301119111100000,0,Unknown,1,16,1,16,191111,1102,E3200",
"B8.F8.53.25.C1.09,B8.F8.53.28.DC.42,G401119111500000,G401119111518507,11,1,16,1,16,191115,1103,G3100",
"B8.F8.53.28.DC.49,B8.F8.53.2C.FC.75,G401119120200000,G401119120224580,11,1,16,1,16,191202,1103,G3100",
"B8.F8.53.2C.FC.75,B8.F8.53.2F.1D.C6,G401119120500000,G401119120512691,11,1,16,1,16,191205,1103,G3100",
"B8.F8.53.2F.1D.CD,B8.F8.53.34.F7.22,G401119120700000,G401119120734848,11,1,16,1,16,191207,1103,G3100",
"B8.F8.53.34.F7.2D,B8.F8.53.35.B1.B4,G401119121700000,G401119121704341,11,1,16,1,16,191217,1103,G3100",
"B8.F8.53.35.B1.BD,B8.F8.53.3B.0B.27,G401119120900000,G401119120931870,11,1,16,1,16,191209,1103,G3100",
"B8.F8.53.3B.0B.31,B8.F8.53.3C.A3.86,G401120010700000,G401120010709503,11,1,16,1,16,200107,1103,G3100",
"B8.F8.53.3C.A3.8B,B8.F8.53.3E.3B.EB,G401120010900000,G401120010909504,11,1,16,1,16,200109,1103,G3100",
"B8.F8.53.3E.3B.F1,B8.F8.53.40.1F.62,G401120011000000,G401120011011251,11,1,16,1,16,200110,1103,G3100",
"B8.F8.53.40.1F.65,B8.F8.53.43.05.09,G401120011300000,G401120011317260,11,1,16,1,16,200113,1103,G3100",
"B8.F8.53.43.05.11,B8.F8.53.44.90.98,G401120011600000,G401120011609205,11,1,16,1,16,200116,1103,G3100",
"B8.F8.53.44.90.9F,B8.F8.53.4C.AE.CE,G401120010200000,G401120010248365,11,1,16,1,16,200102,1103,G3100",
"B8.F8.53.4C.AE.D6,B8.F8.53.51.F0.93,G402120021000000,0,Unknown,1,16,1,16,200210,1104,G3100",
"B8.F8.53.51.B9.01,B8.F8.53.51.F7.F2,G402120022400000,0,Unknown,1,16,1,16,200224,1104,G3100",
"B8.F8.53.52.19.01,B8.F8.53.52.9E.82,G402120022500000,0,Unknown,1,16,1,16,200225,1104,G3100",
"B8.F8.53.52.E2.01,B8.F8.53.52.F7.12,G402120022600000,0,Unknown,1,16,1,16,200226,1104,G3100",
"B8.F8.53.54.6E.01,B8.F8.53.54.B6.FA,G402120022900000,0,Unknown,1,16,1,16,200229,1104,G3100",
"B8.F8.53.55.34.01,B8.F8.53.55.58.52,G402120030300000,0,Unknown,1,16,1,16,200303,1104,G3100",
"B8.F8.53.57.8C.41,B8.F8.53.57.D8.C2,G402120031000000,0,Unknown,1,15,1,16,200310,1104,G3100",
"B8.F8.53.59.7B.41,B8.F8.53.59.87.62,G402120031300000,0,Unknown,1,16,1,16,200313,1104,G3100",
"B8.F8.53.5A.41.41,B8.F8.53.5A.8A.C0,G402120031400000,0,Unknown,1,16,1,16,200314,1004,G3100",
"B8.F8.53.5B.CD.41,B8.F8.53.5F.4B.89,G402120031800000,G402120031828617,8,1,15,1,16,200318,1104,G3100",
"B8.F8.53.5F.4B.90,B8.F8.53.60.11.88,G402120040800000,G402120040806335,8,1,15,1,16,200408,1104,G3100",
"B8.F8.53.60.11.90,B8.F8.53.62.00.88,G402120040900000,G402120040915839,8,1,15,1,16,200409,1104,G3100",
"B8.F8.53.62.00.90,B8.F8.53.62.C6.88,G402120041200000,G402120041206335,8,1,15,1,16,200412,1104,G3100",
"B8.F8.53.62.C6.90,B8.F8.53.67.0D.C8,G402120032500000,G402120032535047,8,1,15,1,16,200325,1104,G3100",
"B8.F8.53.67.0D.D0,B8.F8.53.67.D3.C8,G402120050600000,G402120050606336,8,1,15,1,16,200506,1104,G3100",
"B8.F8.53.67.D3.D0,B8.F8.53.69.5F.C8,G402120050800000,G402120050812671,8,1,15,1,16,200508,1104,G3100",
"B8.F8.53.68.99.D0,B8.F8.53.68.B8.B1,G402120050900000,G402120050900989,8,1,15,1,16,200509,1104,G3100",
"B8.F8.53.69.5F.D0,B8.F8.53.6D.80.88,G402120051100000,G402120051133816,8,1,15,1,16,200511,1104,G3100",
"B8.F8.53.6D.80.90,B8.F8.53.70.35.88,G402120060600000,G402120060622176,8,1,15,1,16,200606,1104,G3100",
"B8.F8.53.70.35.90,B8.F8.53.72.0F.59,G402120052600000,G402120052634649,8,1,15,1,16,200526,1104,G3100",
"B8.F8.53.73.AA.58,B8.F8.53.73.B9.F9,G402120070200000,G402120070200501,8,1,15,1,16,200702,1104,G3100",
"B8.F8.53.74.70.58,B8.F8.53.75.36.50,G402120070300000,G402120070306335,8,1,15,1,16,200703,1104,G3100",
"B8.F8.53.75.36.58,B8.F8.53.77.52.70,G402120070500000,G402120070517284,8,1,15,1,16,200705,1104,G3100",
"B8.F8.53.77.52.78,B8.F8.53.7D.EB.B0,G402120062600000,G402120062654056,8,1,15,1,16,200626,1104,G3100",
"B8.F8.53.7D.EB.B8,B8.F8.53.7F.14.B0,G402120072300000,G402120072309503,8,1,15,1,16,200723,1104,G3100",
"B8.F8.53.7F.14.B8,B8.F8.53.85.C5.38,G402120071800000,G402120071854800,8,1,15,1,16,200718,1104,G3100",
"B8.F8.53.85.C5.40,B8.F8.53.8B.AB.A8,G402120081100000,G402120081148333,8,1,15,1,16,200811,1104,G3100",
"B8.F8.53.8B.AB.B0,B8.F8.53.95.FB.A8,G402120081200000,G402120081284479,8,1,15,1,16,200812,1104,G3100",
"B8.F8.53.95.FB.B0,B8.F8.53.9E.AF.28,G402120082000000,G402120082071279,8,1,15,1,16,200820,1104,G3100",
"B8.F8.53.9E.AF.32,B8.F8.53.A0.F4.30,E302120082400000,E302120082424789,6,1,15,1,16,200824,1103,E3200",
"B8.F8.53.A0.F4.34,B8.F8.53.A1.62.2C,G402120082800000,G402120082803519,8,1,15,1,16,200828,1104,G3100",
"B8.F8.53.A1.62.34,B8.F8.53.AA.85.1C,G402120083000000,G402120083074845,8,1,15,1,16,200830,1104,G3100",
"B8.F8.53.AA.85.1E,B8.F8.53.AD.F8.DA,E302120090800000,E302120090837706,6,1,15,1,16,200908,1103,E3200",
"B8.F8.53.AD.F8.DC,B8.F8.53.B6.85.24,G402120091500000,G402120091570025,8,1,15,1,16,200915,1104,G3100",
"B8.F8.53.B6.85.26,B8.F8.53.B7.01.8E,E302120091500000,E302120091505308,6,1,15,1,16,200915,1103,E3200",
"B8.F8.53.B7.01.8F,B8.F8.53.B7.98.C3,0,0,Unknown,1,0,1,0,0,0,Unknown",
"B8.F8.53.B7.98.C4,B8.F8.53.B8.06.BC,G402120101500000,G402120101503519,8,1,15,1,16,201015,1104,G3100",
"B8.F8.53.B8.06.C4,B8.F8.53.BB.08.BC,G402120101600000,G402120101624639,8,1,15,1,16,201016,1104,G3100",
"B8.F8.53.BB.08.C4,B8.F8.53.BB.76.BC,G402120101800000,G402120101803519,8,1,15,1,16,201018,1104,G3100",
"B8.F8.53.BB.76.C4,B8.F8.53.C0.9E.BC,G402120101900000,G402120101942240,8,1,15,1,16,201019,1104,G3100",
"B8.F8.53.C0.9E.C4,B8.F8.53.C2.56.C4,G402120102400000,G402120102414080,8,1,15,1,16,201024,1104,G3100",
"B8.F8.53.C2.56.C4,B8.F8.53.C5.D1.1C,G402120102500000,G402120102528491,8,1,15,1,16,201025,1104,G3100",
"B8.F8.53.C5.D1.20,B8.F8.53.CD.91.D0,G402120100700000,G402120100763510,8,1,15,1,16,201007,1104,G3100",
"B8.F8.53.CD.91.D2,B8.F8.53.CE.C3.5A,E302120100700000,0,6,1,15,1,16,201007,1103,E3200",
"B8.F8.53.CE.C3.60,B8.F8.53.B7.98.BE,0,0,Unknown,1,0,1,0,0,0,Unknown",
"B8.F8.53.CE.D4.C8,B8.F8.53.D3.58.68,G402120101300000,G402120101336980,8,1,15,1,16,201013,1104,G3100",
"B8.F8.53.D3.58.6A,B8.F8.53.D5.EC.64,E302120101500000,E302120101528159,6,1,15,1,16,201015,1103,E3200",
"B8.F8.53.D5.EC.68,B8.F8.53.D7.36.60,G402120111200000,G402120111210559,8,1,15,1,16,201112,1104,G3100",
"B8.F8.53.D7.36.68,B8.F8.53.DA.38.60,G402120111300000,G402120111324640,8,1,15,1,16,201113,1104,G3100",
"B8.F8.53.DA.38.68,B8.F8.53.DC.01.D0,G402120111700000,G402120111714637,8,1,15,1,16,201117,1104,G3100",
"B8.F8.53.DC.01.D2,B8.F8.53.DC.6F.CA,E302120111300000,E302120111304693,6,1,15,1,16,201113,1103,E3200",
"B8.F8.53.DC.6F.D0,B8.F8.53.E3.C6.D8,G402120111300000,G402120111360129,8,1,15,1,16,201113,1104,G3100",
"B8.F8.53.E3.C6.E0,B8.F8.53.EB.14.D8,G402120120200000,G402120120259839,8,1,15,1,16,201202,1104,G3100",
"B8.F8.53.EB.14.E0,B8.F8.53.F3.08.C8,G402120121000000,G402120121065150,8,1,15,1,16,201210,1104,G3100",
"B8.F8.53.F3.08.D0,B8.F8.53.F8.30.C8,G402120122300000,G402120122342240,8,1,15,1,16,201223,1104,G3100",
"B8.F8.53.F8.30.D0,B8.F8.53.FA.C4.C8,G402121011800000,G402121011821120,8,1,15,1,16,210118,1104,G3100",
"B8.F8.53.FA.C4.D0,B8.F8.53.FF.BF.F8,G402121010600000,G402121010640805,8,1,15,1,16,210106,1104,G3100",
"B8.F8.53.FF.BF.F9,B8.F8.53.FF.FF.FF,0,0,Unknown,1,0,1,0,0,0,Unknown",
"3C.BD.C5.00.00.00,3C.BD.C5.01.25.57,0,0,Unknown,1,0,1,0,0,0,Unknown",
"3C.BD.C5.01.25.58,3C.BD.C5.01.F5.78,G402121011510000,G402121011516660,8,1,15,1,16,210115,1104,G3100",
"3C.BD.C5.09.3B.F8,3C.BD.C5.0B.B4.78,G402121030100000,G402121030120240,8,1,15,1,16,210301,1104,G3100",
"3C.BD.C5.0B.B4.7A,3C.BD.C5.0B.BC.FD,E302121030300000,E302121030300364,6,1,15,1,16,210303,1103,E3200",
"3C.BD.C5.0C.30.38,3C.BD.C5.16.4A.C0,G402121021700000,G402121021782769,8,1,15,1,16,210217,1104,G3100",
"3C.BD.C5.16.4A.C8,3C.BD.C5.22.55.E0,G402121031200000,G402121031298659,8,1,15,1,16,210312,1104,G3100",
"3C.BD.C5.1E.E5.EA,3C.BD.C5.22.55.E6,E302121031200000,E302121031237546,6,1,15,1,16,210312,1103,E3200",
"3C.BD.C5.22.55.E8,3C.Bd.C5.2B.5B.E0,G402121033100000,G402121033173919,8,1,15,1,16,210331,1104,G3100",
"3C.BD.C5.2B.5B.E8,3C.BD.C5.31.1E.68,G402121041400000,G402121041447184,8,1,15,1,16,210414,1104,G3100",
"3C.BD.C5.31.1E.69,3C.BD.C5.35.36.88,0,0,Unknown,1,0,1,0,0,0,Unknown",
"3C.BD.C5.35.36.90,3C.BD.C5.3C.6B.58,G402121051500000,G402121051559033,8,1,15,1,16,210515,1104,G3100",
"3C.BD.C5.3C.6B.59,3C.BD.C5.37.5C.88,0,0,Unknown,1,0,1,0,0,0,Unknown",
"3C.BD.C5.37.5C.90,3C.BD.C5.3D.D3.38,G402121051800000,G402121051852950,8,1,15,1,16,210518,1104,G3100",
"3C.BD.C5.3D.D3.40,3C.BD.C5.42.27.40,G402121060700000,G402121060735456,8,1,15,1,16,210607,1104,G3100",
"3C.BD.C5.42.27.4C,3C.BD.C5.46.3E.C4,G402121062200000,G402121062233519,8,1,15,1,16,210622,1104,G3100",
"3C.BD.C5.46.3E.CC,3C.BD.C5.47.F6.C4,G402121071300000,G402121071314079,8,1,15,1,16,210713,1104,G3100",
"3C.BD.C5.47.F6.CC,3C.BD.C5.49.AE.C4,G402121072100000,G402121072114079,8,1,15,1,16,210721,1104,G3100",
"3C.BD.C5.49.AE.CC,3C.BD.C5.50.05.4C,G402121070100000,G402121070151920,8,1,15,1,16,210701,1104,G3100",
"3C.BD.C5.50.05.4C,3C.BD.C5.57.37.CC,G402121072000000,G402121072058960,8,2,15,2,9,210720,1104,G3100",
"3C.BD.C5.57.37.CE,3C.BD.C5.59.22.C0,E302121072000000,E302121072020947,6,2,15,2,9,210720,1103,E3200",
"3C.BD.C5.59.24.74,3C.BD.C5.60.3B.6C,G402121080200000,G402121080258079,8,2,15,2,9,210802,1104,G3100",
"3C.BD.C5.60.3B.74,3C.BD.C5.67.20.6C,G402121081100000,G402121081156479,8,2,15,2,9,210811,1104,G3100",
"3C.BD.C5.67.20.74,3C.BD.C5.69.B8.CC,G402121081160000,G402121081181259,8,2,15,2,9,210811,1104,G3100",
"3C.BD.C5.69.B8.D4,3C.BD.C5.71.07.FC,G402121082000000,G402121082059877,8,2,15,2,9,210820,1104,G3100",
"3C.BD.C5.71.08.00,3C.BD.C5.76.8D.C8,G402121082600000,G402121082645241,8,2,15,2,9,210826,1104,G3100",
"3C.BD.C5.76.8D.CD,3C.BD.C5.7D.27.A5,G402121090800000,G402121090854075,8,2,15,2,9,210908,1104,G3100",
"3C.BD.C5.7D.27.A6,3C.BD.C5.83.03.C6,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.7E.9C.4C,3C.BD.C5.83.03.CC,G402121091600000,G402121091636080,8,2,15,2,9,210916,1104,G3100",
"3C.BD.C5.83.03.CE,3C.BD.C5.85.4C.A4,E302121091600000,E302121091624953,6,2,15,2,9,210916,1103,E3200",
"3C.BD.C5.85.4C.AA,3C.BD.C5.8C.CB.CB,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.8C.CB.CC,3C.BD.C5.93.FF.94,G402121101500000,G402121101559001,8,2,15,2,9,211015,1104,G3100",
"3C.BD.C5.93.FF.9C,3C.BD.C5.9B.67.71,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.9B.67.72,3C.BD.C5.A1.71.B2,G402121110500000,G402121110549480,8,2,15,2,9,211105,1104,G3100",
"3C.BD.C5.A1.71.B4,3C.BD.C5.A3.FE.22,E302121110500000,E302121110527837,6,2,15,2,9,211105,1103,E3200",
"3C.BD.C5.A3.FE.28,3C.BD.C5.AD.16.63,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.AD.16.64,3C.BD.C5.AE.AE.34,E302122010100000,E30212201017400,6,2,15,2,9,220101,1103,E3200",
"3C.BD.C5.AE.AE.34,3C.BD.C5.B4.2D.5E,E302121122100000,E302121122160039,6,2,15,2,9,211221,1103,E3200",
"3C.BD.C5.B4.2D.62,3C.BD.C5.B5.09.9A,G402122021400000,G402122021407047,8,2,15,2,9,220214,1104,G3100",
"3C.BD.C5.B5.09.A2,3C.BD.C5.B5.E5.9A,G402122031000000,G402122031007039,8,2,15,2,9,220310,1104,G3100",
"3C.BD.C5.B5.E5.A2,3C.BD.C5.B8.0B.9A,G402122031100000,G402122031117599,8,2,15,2,9,220311,1104,G3100",
"3C.BD.C5.B8.0B.A2,3C.BD.C5.C1.ED.A2,G402122031500000,G402122031580960,8,2,15,2,9,220315,1104,G3100",
"3C.BD.C5.C1.ED.A4,3C.BD.C5.C2.40.1E,E302122031300000,E302122031303519,6,2,15,2,9,220313,1103,E3200",
"3C.BD.C5.C2.40.24,3C.BD.C5.C2.89.E6,E302122031400000,E302122031403147,6,2,15,2,9,220314,1103,E3200",
"3C.BD.C5.C2.89.EC,3C.BD.C5.CD.A1.19,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.CD.A1.20,3C.BD.C5.CF.39.86,E302122052200000,E302122052217426,6,2,15,2,9,220522,1103,E3200",
"3C.BD.C5.CF.39.8C,3C.BD.C5.D2.C8.06,E302122091400000,E302122091438847,6,2,15,2,9,220914,1103,E3200",
"3C.BD.C5.D2.C8.0C,3C.BD.C5.DA.E1.12,E302122091500000,E302122091588450,6,2,15,2,9,220915,1103,E3200",
"3C.BD.C5.DA.E1.18,3C.BD.C5.DD.EE.86,E302122110900000,E302122110933341,6,2,15,2,9,221109,1103,E3200",
"3C.BD.C5.DD.EE.8C,3C.BD.C5.E5.83.97,0,0,Unknown,2,0,2,0,0,0,Unknown",
"3C.BD.C5.E5.83.98,3C.BD.C5.E8.34.8A,E302123011000000,E302123011029396,6,2,15,2,9,230110,1103,E3200",
"3C.BD.C5.E8.34.90,3C.BD.C5.F1.11.4A,E302123030400000,E302123030496800,6,2,15,2,9,230304,1103,E3200",
"3C.BD.C5.F1.11.50,3C.BD.C5.F1.E1.E2,E302123042100000,E302123042108899,6,2,15,2,9,230421,1103,E3200",
"3C.BD.C5.F1.E1.E8,3C.BD.C5.F4.B3.C2,E302123042600000,E302123042630800,6,2,15,2,9,230426,1103,E3200",
"3C.BD.C5.F4.B3.C8,3C.BD.C5.F8.7A.CA,E302123052000000,E302123052041260,6,2,15,2,9,230520,1103,E3200",
"3C.BD.C5.F8.7A.D0,3C.BD.C5.F8.A3.63,E302123080600000,E302123080601732,6,2,15,2,9,230806,1103,E3200",
"3C.BD.C5.F8.A3.64,3C.BD.C5.FF.FF.FF,0,0,Unknown,2,0,2,0,0,0,Unknown",
"DC.F5.1B.5F.EA.4F,DC.F5.1B.64.2B.10,Unknown,Unknown,0,2,16,2,9,223550,1103,Unknown",
"DC.F5.1B.64.2B.1A,DC.F5.1B.64.34.D5,E302123103100000,0,6,2,15,2,9,231031,1103,E3200",
"DC.F5.1B.64.54.5A,DC.F5.1B.64.57.B5,E302123110200000,0,6,2,15,2,9,231102,1103,E3200",
"DC.F5.1B.67.F4.7A,DC.F5.1B.68.6F.Ff,E302123113000000,0,6,2,15,2,9,231130,1103,E3200",
"DC.F5.1B.5D.64.60,DC.F5.1B.60.14.57,AA63332935300000,0,6,2,15,2,9,329353,1103,Unknown",
    ]
   
    ref_data = parse_ref_data(ref_data_lines)
   
    user_input = input("Enter one or more MAC addresses (comma-separated) or the path to a.csv file: ")
   
    if user_input.lower().endswith('.csv'):
        while not os.path.isfile(user_input) or not user_input.lower().endswith('.csv'):
            user_input = input("Please enter a valid.csv file path: ")
       
        save_location = input("Enter the save location for the output.csv file: ")
        process_csv_file(user_input, ref_data, save_location)
    else:
        mac_addresses = user_input.split(',')
        for mac in mac_addresses:
            mac = mac.strip()  # Remove any leading/trailing whitespace
            result = process_single_mac(mac, ref_data)
            result['MAC'] = mac  # Include the original MAC address
            # Print the result in a formatted table
            print_formatted_table(result)

       
if __name__ == "__main__":
    main()

Output:
Code:
                      Fios-F1NdR
+--------------------+----------------------------------------+
| MAC Input          | B8.F8.53.E2.EC.60                      |
+--------------------+----------------------------------------+
| MAC Block Start    | B8.F8.53.DC.6F.D0                      |
+--------------------+----------------------------------------+
| MAC Block End      | B8.F8.53.E3.C6.D8                      |
+--------------------+----------------------------------------+
| MAC 2 Hex Ratio    | 8                                      |
+--------------------+----------------------------------------+
| Calculated Serial  | G402120111353138                       |
+--------------------+----------------------------------------+
| Keyspace           | w = <word>  n = <number>               |
|                    | ? = single digit at end of random word |
+--------------------+----------------------------------------+
| WiFi Pass Format   | wnwnw                                  |
+--------------------+----------------------------------------+
| WiFi Pass Length   | 15                                     |
+--------------------+----------------------------------------+
| Admin Pass Format  | wnw                                    |
+--------------------+----------------------------------------+
| Admin Pass Length  | 16                                     |
+--------------------+----------------------------------------+
| Model Type         | G3100                                  |
+--------------------+----------------------------------------+
| Date Code (YYMMDD) | 201113                                 |
+--------------------+----------------------------------------+
| Hardware           | 1104                                   |
+--------------------+----------------------------------------+


Again, in the future I will post just the updated ref_data_lines and you can update the script yourself.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
I am happy to say I have added another 59 entries to the dataset, which with to the 25 that I collected in the previous post brings us to 84 new entries! I was hoping for 100, but that is close enough for me. There are now 313 complete entries.

Dataset: g3100_E3200_04_16_24.xlsx
Reference Images: Ref_images.zip

Let’s see what we’ve learned...

First, we test the new entries against the Fios-F1nDr database. We test 58 entries, since 1 was actually missing the serial. Again it doesn’t look good at first, but all it really means is that we’ve collected a bunch of new Date Codes! After working all of the new entries into the proper place and recalculating everything, we retest against the updated database.

Before:
Correct - 12 (~20.5%)
Incorrect - 23 (~40.5%)
unknown block - 12 (~20.5%)
Unknown device - 9 (~15.5%)
Not Enough Data - 2 (~3%)

After:
Correct - 44 (~75.9%)
Incorrect - 3 (~5%)
unknown block - 0 (0%)
Unknown Device 9 (~15.5%)
Not enough data 2 (~3%)

Much Better! The 3 incorrect are outliers in the dataset, that don’t calculate correctly at the moment. The Unknown devices are showing up because they are either in the DC.F5.1B block or new block 74.90.BC. We are starting to gather a bit more info about both of these, but we need more entries to better understand what’s going on there.

We now have 166 Unique Date codes. There is now a second sheet named “Date Codes” in the dataset that has each of the blocks separated. This eventually gets collapsed into that data_ref_lines that the Fios-F1nDr code uses, which is also included with the dataset. Sorting the entries by the date codes allows us to see devices that are in the same block possibly removing some of the overall randomness. There are quite a few blocks that we have 4-7 entries for.

Also added to the dataset is a new column named “QR”, which indicates if the QR data was read from the reference image. The QR data either reads or it doesn’t, so it is the most accurate and therefore preferred source of the data. This is the script that I am using to sort the scraped images into “found” and “not found”. Found images contain either a QR code, or some part of the back label we are looking for (WIFI, Serial, Password, etc). The script is adapted from code found here, and uses apple vision for the text and zbarimg for the QR code. All of the necessary packages are included in the header. The script might only work on an Apple devices, but I am not sure. The script will output “imagedata.csv” at the end, that contains all of the info that it finds. The script works great to sort the images, but unfortunately the extracted text is rarely complete or correct, but it’s sometimes fun to see what the computer picks up.

Code:
"""

Use Apple's Vision Framework via PyObjC to detect text in images
modified code from: https://gist.github.com/RhetTbull/1c34fc07c95733642cffcd1ac587fc4c

Necessary Dependencies:
python3 -m pip install pyobjc-core pyobjc-framework-Quartz pyobjc-framework-Vision wurlitzer
brew install zbar

This script process all of the images in folder_path using apple vision to extract text.
It checks to see if any of the keywords are found in the image, and extracts the relevent info
All of the info is saved to <file name> and relevent photos are moved to the "found" folder
in case they need to be referenced later.  Any photos without keywords are deleted.
"""

import pathlib
import sys
import Quartz
import Vision
import os
import shutil
import pandas as pd
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
import subprocess

import re

from pyzbar.pyzbar import decode, ZBarSymbol
from PIL import Image

sys.path.append(r'/opt/homebrew/Cellar/zbar')  #tell the script where to find zbarimg

KEYWORDS = ["Wi-Fi Name:", "Wi-Fi Password:", "Admin URL:", "Admin Password:",
"Model Number:", "Serial #.", "WAN MAC:", "HW ver.:", "Shipped FW ver.:"]

# Define the keyword mappings
qr_to_keyword_map = {
        "WIFI:S:": "Wi-Fi Name:",
        "T:WPA;P:": "Wi-Fi Password:",
        "ROUTER:M:": "Model Number:",
        "S:": "Serial #.",
        "W:": "WAN MAC:",
        "I:admin;P:": "Admin Password:",
}

QRKEYWORDS = ["WIFI:S:", "T:WPA;P:", "I:admin;P:", "ROUTER:M:", "S:"]
KEYWORDS = ["Wi-Fi Name:", "Wi-Fi Password:", "Admin URL:", "Admin Password:",
"Model Number:", "Serial #.", "WAN MAC:", "HW ver.:", "Shipped FW ver.:"]

keywords_lower = [keyword.lower() for keyword in KEYWORDS]  #makes keywords lowercase(case insensitive) for better matching

global qr_found

def image_to_text(img_path, lang="eng"):
    input_url = NSURL.fileURLWithPath_(img_path)

    with pipes() as (out, err):
    # capture stdout and stderr from system calls
    # otherwise, Quartz.CIImage.imageWithContentsOfURL_
    # prints to stderr something like:
    # 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
    # 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
        input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)

    vision_options = NSDictionary.dictionaryWithDictionary_({})
    vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
        input_image, vision_options
    )
    results = []
    handler = make_request_handler(results)
    vision_request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(handler)
    error = vision_handler.performRequests_error_([vision_request], None)

    return results

def scan_qr(image_path, img_name):
    qr_read = pd.DataFrame()
    global qr_found
    qr_found = False
  
    try:
        # Run zbarimg command on the image
        #qimage_path = '/Users/Brian/Downloads/Routers/Fios-3100/ebay_images/found/image_702.webp'
        img = Image.open(image_path)  # Replace with your file
        decoded_objects = decode(img)
      
        if decoded_objects:
      
            qr_read = pd.DataFrame(decoded_objects)
          #print(decoded_objects)
            #print(f"✅ QR Code Found:" + str(decoded_objects))
            #qr_codes = [line.split(": ", 1)[1] for line in result.stdout.strip().split("\n") if ": " in line]

            #qr_read = pd.DataFrame(qr_codes)    # Create a DataFrame
            qr_read["QR_read"] = "Yes"
            #df = pd.DataFrame(qr_codes, columns=["QR Code Data"])
            qr_found = True
        #else:
            #print(f"❌ No QR code detected in", img_name)
  
    except FileNotFoundError:
        print("Error: zbarimg is not installed or not found in PATH.")
  
    #return qr_read
    return decoded_objects


def make_request_handler(results):
    """ results: list to store results """
    if not isinstance(results, list):
        raise ValueError("results must be a list")

    def handler(request, error):
        if error:
            print(f"Error! {error}")
        else:
            observations = request.results()
            for text_observation in observations:
                recognized_text = text_observation.topCandidates_(1)[0]
                results.append([recognized_text.string(), recognized_text.confidence()])
    return handler


def extract_values(qrcodes, results, QRKEYWORDS, KEYWORDS):
    global match_found
    match_found = False
  
    extracted_data = {key: "" for key in KEYWORDS}  # Initialize a dictionary with empty values
  
    # Convert QRKEYWORDS to KEYWORDS mapping
    qr_to_keyword_map = {
        "WIFI:S:": "Wi-Fi Name:",
        "T:WPA;P:": "Wi-Fi Password:",
        "I:admin;P:": "Admin Password:",
        "ROUTER:M:": "Model Number:",
        ";S:": "Serial #."
    }
  
    # **Step 1: Search QR Code Data**
    for qr_entry in qrcodes:
        data_str = str(qr_entry)  # Decode bytes to string
        for qr_key, keyword in qr_to_keyword_map.items():
            match = re.search(fr'{qr_key}([^;]+)', data_str)
            if match:
                extracted_data[keyword] = match.group(1).split("'")[0]  # Remove trailing artifacts
                match_found = True
                extracted_data["QR_found"] = "Yes"


    # **Step 2: Search OCR Text (Only if not found in QR)**
    for text_entry in results:
        text = str(text_entry)
      
        for keyword in KEYWORDS:
            if extracted_data[keyword]:  # Skip if already filled by QR
                continue
            match = re.search(fr'{keyword}([^;]+)', text, re.IGNORECASE)
            if match:
                extracted_data[keyword] = match.group(1).split("'")[0]  # Extract OCR value
                match_found = True

    # Convert to DataFrame
    extract_df = pd.DataFrame([extracted_data])
    return extract_df

folder_path = input("Please enter the file path to folder with the images: ")
folder_path = (folder_path + "/")
folder_contents = os.listdir(folder_path)
os.makedirs(folder_path +"found/", exist_ok=True)
os.makedirs(folder_path +"not_found/", exist_ok=True)
global match_found
match_found = False

matched_text = pd.DataFrame()  #the dataframe where well store all of oure matched text
clean_list = pd.DataFrame()

for imgs in folder_contents:
    if not imgs.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".tiff")):  #.lower makes the comparison case-insensitive
        continue
    match_found = False

    print ("Processing: " + imgs)
    img_path = (folder_path + imgs)
    qrcodes = scan_qr(img_path, imgs)
    results = image_to_text(img_path)
  
    # Extract values from QR and OCR results
    if qrcodes:
        #qr_data = extract_values(qrcodes)
        print("QR Code Found: " + str(qrcodes))
    if results:
        #ocr_data = extract_values(results)
        print ("OCR Text Found: " + str(results))
  
    if qrcodes or results:
        img_data = extract_values(qrcodes, results, QRKEYWORDS, KEYWORDS)
        img_data['IMG'] = imgs #add the image name to the info

    
  
    if match_found == True:
        print(f"✅ Match found! Moving: {imgs}")
        matched_text = pd.concat([matched_text, img_data], ignore_index=True)  #add the extracted info to our dataframe
        shutil.move(folder_path + imgs, folder_path + "found/" + imgs)    #moves the images to a folder named "found"
    else:
        print(f"❌ No match. Moving: {imgs}")
        shutil.move(folder_path + imgs, folder_path + "not_found/" + imgs)    #moves the images to a folder named "found"

        #os.remove(img_path)  #delete files where no info is found


#Now to do some cleaning to our matched_text
#clean_list['Duplicates'] = 0
if not matched_text.empty:
    clean_list = matched_text #copy the matched text into a new dataframe
    clean_list = clean_list.fillna("") #remove any nan
    clean_list = clean_list.map(lambda x: x.replace(" ", "") if isinstance(x, str) else x)
    #clean_list = clean_list.map(lambda x: x.replace("', 0.5]", "") if isinstance(x, str) else x)


    img_column = clean_list.pop('IMG')  # Removes "IMG" column but keeps it in memory

    clean_list["duplicates"] = clean_list.duplicated(keep="first").astype(int)  #adds a column "duplicates" and denotes duplicate with 0 or 1
    clean_list["duplicates"] = clean_list.groupby(clean_list.columns.drop(["duplicates"]).tolist())["duplicates"].transform("sum") #count the number of duplicates
    clean_list = clean_list.drop_duplicates() #.reset_index(drop=True)  # Drop duplicate rows and keep the first occurrence

    clean_list.insert(0, "IMG", img_column)

    clean_list = clean_list[["IMG"] + [col for col in clean_list.columns if col != "IMG"]] # ensures "IMG" is first column, while preserving the rest of the order
    clean_list = clean_list.sort_values(by="IMG").reset_index(drop=True) #Arranges list in alphabetical order based on image name
  
    clean_list = clean_list.reset_index(drop=True)

    clean_list.to_csv(folder_path + "/imagedata.csv")

else:
    print("No matched images found!")

# the number in the results is the confidence score, It's important to note that a lower confidence score doesn't
#necessarily mean the recognized text is incorrect, but it does suggest that the system is less certain about
#its accuracy. In such cases, manual verification or additional processing might be warranted to ensure the information's correctness.


Unfortunately, the script is not very good at reading the QR code. Running the script against all of the reference images only yields 38/313 QR codes. Using my phone, with a QR reader app is how I actually collect the QR codes. Using this method I’ve read 133/313 codes, but it takes much longer. If I could somehow get the script to read the QR with the same effectiveness as my phone it would make the scraping process so much faster and simpler! The good news is now that we have the QR column, we actually can separate the reference images into “Read” and “Unread”, and perhaps use this to train the image processing to better recognize our QR codes. Furthermore, we are looking for a specific format to the QR code, so hopefully this can better inform the script too? Here is what an example read looks like.

Code:
WIFI:S:Verizon_ZFBX9P;T:WPA;P:bread-veto6-ode;;ROUTER:M:G3100;S:G402121090810583;W:3CBDC577D884;I:admin;P:KT7XFBH3D;;4

If you want to see if your phone is any better at reading images, I can read the first image but not the second.



Despite everything that I’ve been able to accomplish, I am very much new to all this. This is my first foray into hardware hacking, and I am working mostly off of tutorials found online. I was able to get binwalk to run, and I can get it loaded into Ghidra, but I honestly have no clue what the next steps should be. I believe it is encrypted, possibly with LUKS. I found an awesome post on the openwrt forums discussing the CR1000A which is a similar device. The post is an exciting read, but quickly got over my head. I also found a nice teardown of the E3200, which seems to be very similar to the G3100 internally. Looks like the embedded processor of choice is a Broadcom BCM68369KFEBG, which is a Dual Core 1.5 GHz ARM v8 processor featuring 4x GbE PHYs, 1x USB 3.0, DDR 800MHz support, 4ch VoIP.

"Looks like the embedded processor of choice is a Broadcom BCM68369KFEBG, which is a Dual Core 1.5 GHz ARM v8 processor featuring 4x GbE PHYs, 1x USB 3.0, DDR 800MHz support, 4ch VoIP."

@drsnooker I hope that my posts show that I am putting a lot of effort into this, and willing to share my ideas/results openly. I don’t really expect to find the keygen algorithm, but it’s enough of a carrot to keep me going. I will continue to drudge along by myself, but if you, or anyone you know may be interested in furthering this research please let me know. I have all of the listed firmware downloaded and I’m happy to pass it along.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
forgot to include an example of a E3200 QR read and the example images

Code:
WIFI:S:Verizon_4L79ZB;T:WPA;P:wavy-guise4-sup;;EXTENDER:M:E3200;S:E302123030400689;P:Z6CL3QXVP;B:3CBDC5E844B6;;1


I can read:
image_25060.jpg

I can’t read:
image_28001.jpeg
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
@FiosFiend Always enjoy reading your updates! Keep it going! You are definitely putting in a lot of effort. I'm just not sure what I can add to your research. I did some work on older Netgear and Verizon models, but never managed to find even a hint of the keygen. The parameter space is very large to just grope around in the dark. (Did the Verizon engineers use a hash, a linear congruent RNG, or some other way of randomization? Are they using the MAC, SN, seconds since 1970, or even something like the admin password as the seed for the keygen?)

Does a factory reset work without a network connection? If a network is needed, then perhaps the word-list gets downloaded. If not, than the password is just pulled from NVRAM or the word-list is encoded somewhere. Tracking down the factory reset mechanism would be where I'd focus my attention.
If it's not in there, then next bet would be to dig through older generations/models to hopefully find a common ancestor that evolved into the current form of the algorithm.

I still have a box full of older PACE routers hoping for something to help with the 5268AC algorithm. So far that white whale still eludes me...

The extraction of 6000+ firmwares from a wide range of makes and models just completed. Now I gotta dig through 500GB of files to find the few needles in that haystack.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
@drsnooker I greatly appreciate your replies! I came to these boards in hopes of finding like minded, and more adept people like you. If you know of any other forums, discord whatever that might be helpful please let me know.

I really don’t think the keygen will be on the device, as with this keyspace it seems like that would complicate things during production. Similarly I don’t really expect to find the wordlist in the firmware, though stranger things have happened. That said, being able to properly extract the firmware is still an attainable goal I would like to accomplish.

Binwalk does extract a vol-rootfs_ubifs, which has all of the correct folders and file names. From what I understand, I should be able to open and read any files such as .txt or .html, which is not currently the case. However, I believe the binwalk images in my original post show that there is not a lot of encryption evident? So I am not sure what is hindering the extraction. Checking the strings results in 100’s of human readable lines. I am guessing I need to do a more precise carving or there is some other encryption/compression that binwalk isn’t catching. @soxrok2212 was instrumental in unlocking the CR1000A, maybe they are interested in taking a look? I think there is a lot of useful information in the serial output, but again I don’t fully understand it.

I made even less progress with Ghidra. I load g3100_fw_2.0.0.6.bin and choose AARCH 64 v8A little endian, but it doesn’t catch any functions on initial analysis. I am not really sure what to look for or what I should be doing next.
Ghidra.png
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
I actually started using unblob instead of binwalk recently. The bin file sounds to me that it still needs another round of extraction. Lots of firmware are nested, so you may need to run binwalk with the -eM command. Some times, you need to run ubi_extract_files or ubi_extract_images on the ubifs file to get the file system.

I've never actually dealt with encrypted firmware (other than a few by d-link, for which there now is "delink" to do the decryption)
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
Oh and @soxrok2212 is the one that got me started on this journey only a few years ago. (Working on ATT routers on the hashcat forums). Not sure if he still spends time on these forums though.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
@drsnooker I actually tried with unblob, and received the same results. With both tools I can see a valid file system, I just can’t actually see what’s inside the files. Perhaps this is a mounting issue? I still haven’t tried to mount it in a Linux environment, but that would require setting up a complete VM at the moment.


2.0.0.6 Filesystem.png

For now, I wanted to share some more of my scripts that I’ve talked about. Here is how I am scraping the Ebay images and is based on code found here. Again all of the dependancies are in the header. This is is a 2-part script that could easily be combined into 1, but for now I keep them seperate. This script will prompt you for a search term, whether you want to search current, past, or both listing, and where to save the output. It will save the output to /save_path/search_term/current_listings.csv or past_listings.csv. These files just have links to the listings, which get fed into the second script actually collect the images.


Code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Necessary Dependencies:
python3 -m pip install beautifulsoup4 requests pandas urlencode

modified code from: https://hasdata.com/blog/scrape-ebay-with-python

This script prompts the user for a search term and then scrapes ebay for
matching listings and saves them as product_info.csv.  You will then run
photoscraper.py, which downloads the images from each link in product_info.csv.
"""
import requests
from bs4 import BeautifulSoup
import pandas as pd
from urllib.parse import urlencode
import os

#URL to search for past items on Ebay
past_items = "https://www.ebay.com/sch/i.html?_fsrp=1&rt=nc&_from=R40&_nkw=" + user_search_query + "&_sacat=0&LH_Sold=1&LH_Complete=1"

def extract_product_details(item):
    title = item.find("div", class_="s-item__title")
    price = item.find("span", class_="s-item__price")
    seller = item.find("span", class_="s-item__seller-info-text")
    shipping = item.find("span", class_="s-item__logisticsCost")
    url = item.find("a", class_="s-item__link")
    list_date = item.find("span", class_="s-item__listingDate")

    return {
        "Title": title.text.strip() if title else "Not available",
        "Price": price.text.strip() if price else "Not available",
        "Seller": seller.text.strip() if seller else "Not available",
        "Shipping Cost": (
            shipping.text.strip().replace("shipping", "").strip()
            if shipping
            else "Not available"
        ),
        "List Date": (
            list_date.text.strip() if list_date else "Date not found"
        ),
        "URL": url["href"].split("?")[0] if url else "Not available",
    }

def extract_page_data(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")

    products = [
        extract_product_details(item) for item in soup.find_all("li", class_="s-item")
    ]

    next_page_button = soup.select_one("a.pagination__next")
    next_page_url = next_page_button["href"] if next_page_button else None

    return products, next_page_url

def write_to_csv(products, filename):
   
   
    df = pd.DataFrame(products, columns=['Title','Price', 'Seller', 'Shipping', 'Date', 'URL'])  
    new_df = df[["Title", "URL"]].copy()
   
    mode = "w" if not pd.io.common.file_exists(filename) else "a"
    new_df.to_csv(filename, index=False, header=(mode == "w"), mode=mode)
    print(f"Data has been written to {filename}")

def make_request(query, sort, items_per_page=60):
    base_url = "https://www.ebay.com/sch/i.html?"
    query_params = {
        "_nkw": query,
        "_ipg": items_per_page,
        "_sop": SORTING_MAP[sort],
    }
    print (base_url + urlencode(query_params))
    return base_url + urlencode(query_params)

def scrape_ebay(search_query, sort, save_path):
    current_url = make_request(search_query, sort, 240)
    print ("Scraping current listings on " + past_items)
    total_products = []

    while current_url:
        products_on_page, next_page_url = extract_page_data(current_url)
        total_products.extend(products_on_page)
        current_url = next_page_url
    total_products = [
        product for product in total_products if "Shop on eBay" not in product["Title"]
    ]
    print(str(len(total_products)) + " current listings collected")
    os.makedirs(save_path +"/" + user_search_query + "/" , exist_ok=True)

   
    write_to_csv(total_products, save_path + "/" + user_search_query+ "/current_listings.csv")

def scrape_ebay_history(search_past, save_path):
    past_items = "https://www.ebay.com/sch/i.html?_fsrp=1&rt=nc&_from=R40&_nkw=" + search_past + "&_sacat=0&LH_Sold=1&LH_Complete=1"
    print ("Scraping past listings on " + past_items)
    total_products = []

    while past_items:
        products_on_page, next_page_url = extract_page_data(past_items)
        total_products.extend(products_on_page)
        past_items = next_page_url
    total_products = [
        product for product in total_products if "Shop on eBay" not in product["Title"]
    ]
 
    print(str(len(total_products)) + " past listings collected")
    os.makedirs(save_path +"/" + user_search_query + "/" , exist_ok=True)

    write_to_csv(total_products, save_path + "/" + user_search_query+ "/past_listings.csv")


SORTING_MAP = {
    "best_match": 12,
    "ending_soonest": 1,
    "newly_listed": 10,
}

def query_user():
    sort_type = 'best_match'

    user_search_query = input("Enter eBay search query: ")
    past_or_present = input("Would you like to search [C]urrent listings, [P]ast listings or [B]oth?")
    save_path = input("Enter the file path to save the list(s)?: ")
    if (past_or_present.lower() == "c"):
        scrape_ebay(user_search_query, save_path)
       
    elif (past_or_present.lower() == "p"):
        scrape_ebay_history(user_search_query, sort_type, save_path)
       
    elif (past_or_present.lower() == "b"):
        scrape_ebay(user_search_query, sort_type, save_path)
        scrape_ebay_history(user_search_query, save_path)

    else:
        print("Invalid entries, try again")
        query_user()
   
query_user()


This script is actually what collects the images from the links gathered in the first script and is adapted from code found here. The script will prompt you for the folder that contains the .csv files output by the previous script. It will then use those links to collect all of the associated images with each link. The script keeps also outputs and reads from an already_scrapted.csv which keeps track of listings that we successfully scrape. This way we’re not recollecting the same images over and over. We also take advantage of https://picclick.co.uk/, which archives past Ebay listing images. Finally, once all of the images are collected I use the script that I previously posted to sort all of the images into “found” and “not found”. This chain allows for quick and easy scraping, and can certainly be directed at other devices :wink:




Code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Feb 14 18:51:21 2025

modified code from: https://hasdata.com/blog/scrape-ebay-with-python
and https://medium.com/towards-data-science/a-tutorial-on-scraping-images-from-the-web-using-beautifulsoup-206a7633e948


This script visits each link collected fromB ebaylist.py(saved as current_listings.csv)
and downloads all of the photos in full sized format to the specified save_folder
"""


import requests
from bs4 import BeautifulSoup
#import json
import pandas as pd
import os


save_path = input("Please enter the folder path to find .csv from the previous script: ")  #file path to save the images
os.makedirs(save_folder, exist_ok=True)
current_listings = save_path + "/current_listings.csv"  #file path to current listing data scraped from ebaylist.py
past_listings = save_path + "/past_listings.csv"  #file path to past listing data scraped from ebaylist.py
already_scraped = save_path + "/already_scraped.csv"  #data file to keep track of listings that we already scraped, and skip them
imgcount = input("Please enter a numerical start point for the images, ie. 100: ") #number used to keep track of and name each image
imgcount = int(imgcount)
def fetch_picclick_item_info(item_id):
   

   
    try:
        #url = "#https://picclick.co.uk/-{item_id}.html"
        html_page = requests.get('https://picclick.co.uk/-' + item_id + '.html')
        html_page.raise_for_status()  # Raise an exception for bad requests
    except requests.exceptions.RequestException as e:
        print(f"Error: Unable to fetch data from pickclick ({e})")
        return None
   
    print ("Collecting: https://picclick.co.uk/-" + item_id + '.html')

    soup = BeautifulSoup(html_page.content, 'html.parser')
   
    item_data = {}
   
    try:

        title = soup.find('title')  #find the title
        title = title.string        #remove the html tag
       
        image_links = []

        image_grid_container = soup.findAll('img') #find all image tags
        if image_grid_container:
           # img_elements = image_grid_container.select(".ux-image-grid-item img")
            image_links = [img["src"]

    for img in image_grid_container if "src" in img.attrs]
        item_data["Title"] = title
        #item_data["Current Price"] = current_price
        #item_data["Original Price"] = original_price
        #item_data["Savings"] = savings
        #item_data["Shipping Price"] = shipping_info
        #item_data["Seller Feedback"] = seller_feedback
        item_data["Images"] = image_links
        return item_data
       
        #filtered_urls = [url for url in item_data["Images"] if url.startswith("http")]
        #filtered_urls = pd.DataFrame(filtered_urls)  #convert to a dataframe
        #filtered_urls.rename(columns={0:"Images"}, inplace=True)  #Set the column name back to URL

        #return filtered_urls
    except Exception as e:
        print(f"Error: Unable to parse picclick data ({e})")
        return None
   


def fetch_ebay_item_info(item_id):
    #url = f"https://www.ebay.com/itm/{item_id}"
    url = item_id
    print ("Collecting: " + url)

    try:
        response = requests.get(url)
        response.raise_for_status()  # Raise an exception for bad requests
    except requests.exceptions.RequestException as e:
        print(f"Error: Unable to fetch data from eBay ({e})")
        return None
   
    soup = BeautifulSoup(response.text, "html.parser")

    item_data = {}

    try:
       
        item_title_element = soup.select_one(".x-item-title__mainTitle .ux-textspans--BOLD")
        item_title = item_title_element.text if item_title_element else "Not available"

        image_grid_container = soup.find("div", class_="ux-image-grid-container")
        image_links = []

        if image_grid_container:
            img_elements = image_grid_container.select(".ux-image-grid-item img")
            image_links = [img["src"]

    for img in img_elements if "src" in img.attrs]
        item_data["Title"] = item_title
        #item_data["Current Price"] = current_price
        #item_data["Original Price"] = original_price
        #item_data["Savings"] = savings
        #item_data["Shipping Price"] = shipping_info
        #item_data["Seller Feedback"] = seller_feedback
        item_data["Images"] = image_links

        return item_data
    except Exception as e:
        print(f"Error: Unable to parse eBay data ({e})")
        return None

def save_to_json(item_data, filename="product_info.json"):  #this function isnt actually used anymore, but retained from original script
    if item_data:
        with open(filename, "w") as file:
            #json.dump(item_data, file, indent=4)
            print(f"Success: eBay item information saved to {filename}")


def download_images(url, folder):
    try:
        response = requests.get(url, stream=True)
        response.raise_for_status()
        if response.status_code == 200:
            ext = url.split('.')[-1].split('?')[0]  # Extract file extension from the URL

            filename = os.path.join(folder, f"image_{imgcount}.{ext}")
               
            # Save image
            with open(filename, "wb") as file:
                for chunk in response.iter_content(1024):
                    file.write(chunk)
               
            print(f"Saved: {filename}")
        else:
            print(f"Failed to download {url} - Status Code: {response.status_code}")
    except Exception as e:
        print(f"Error downloading {url}: {e}")


def query_user():
    past_or_present = input("Would you like to download images from [C]urrent listings, [P]ast listings or [B]oth?")
    #sort = input("Choose one ('best_match', 'ending_soonest', 'newly_listed'): ")
    if (past_or_present.lower() == "c"):
        sort = 'best_match'
        if os.path.exists(current_listings):
            scrape_images(current_listings)
        else:
            print(current_listings + " does not exist!")
   
    elif (past_or_present.lower() == "p"):
        if os.path.exists(past_listings):
            scrape_past_images(past_listings)
        else:
            print(past_listings + " does not exist!")
    elif (past_or_present.lower() == "b"):
        if os.path.exists(current_listings):
            scrape_images(current_listings)
        else:
            print(current_listings + " does not exist!")
     
        if os.path.exists(past_listings):
            scrape_past_images(past_listings)
        else:
            print(past_listings + " does not exist!")
   
    else:
        print("Invalid entries, try again")
        query_user()

def scrape_images(URLlist):
#This part downloads all of the images from links that we scraped inside each listing
   
    scrapedlist = []
    listURL = pd.read_csv(URLlist)
    listURL = listURL[['URL']]
    item_info = pd.DataFrame()

    if os.path.exists(already_scraped):     #Check for the previously scraped data file
        past_scrapes = pd.read_csv(already_scraped) #load list if it exists
        #past_scrapes.rename(columns={"0": "URL"}, inplace=True)
        listURL = listURL.merge(past_scrapes, how='left', indicator=True).query('_merge == "left_only"').drop('_merge', axis=1)  #checks if any of our links are in "past_scrapes" and remove them

    print("Found " + str(len(listURL)) + " current listings that need scraped.")

    for url in listURL["URL"]:
        #item_number = url.split("/")[-1]
        item_info = fetch_ebay_item_info(url)
        item_df = pd.DataFrame(item_info)
        imgURL = pd.DataFrame()



        if item_df.empty == False:
            #imgURL = pd.concat([imgURL, item_df['Images']], ignore_index=True)

            imgURL = pd.concat([imgURL, pd.DataFrame([item_info["Images"]])], ignore_index=True)
            imgURL = imgURL.stack().reset_index(drop=True).to_frame(name="URL")  #collapse the dataframe to one long list
            imgURL = [url.replace("s-l140", "s-l1600") for url in imgURL["URL"]] #collect full size images

            for links in imgURL:
                global imgcount
                imgcount = (imgcount + 1)#keeps cound of the photos downloaded to apppend to file name
                download_images(links, save_folder)  #download each image in imgurl
           
            scrapedlist.append(url)
       
           
        savescrapes = pd.DataFrame(scrapedlist)
        savescrapes.rename(columns={0:"URL"}, inplace=True)  #Set the column name back to URL

        if not os.path.exists(already_scraped):   #write the already_scraped file if it doesnt exist
            savescrapes.to_csv(already_scraped, index=False)
        else:      
            savescrapes.to_csv(already_scraped, mode='a', index=False)  #mode='a' Appends data instead of overwriting. index=False Excludes the index from being written. header=False Avoids writing the header again when appending.


#save_to_json(item_info)

def scrape_past_images(URLlist):
#This part downloads all of the images from links that we scraped inside each listing
    print("scraping past listings")
    scrapedlist = []
    imgURL = pd.DataFrame()
    listURL = pd.read_csv(URLlist)
   
    if os.path.exists(already_scraped):     #Check for the previously scraped data file
        past_scrapes = pd.read_csv(already_scraped) #load list if it exists
        #past_scrapes.rename(columns={"0": "URL"}, inplace=True)
        listURL = listURL.merge(past_scrapes, how='left', indicator=True).query('_merge == "left_only"').drop('_merge', axis=1)  #checks if any of our links are in "past_scrapes" and remove them

    print("Found " + str(len(listURL)) + " past listings that need scraped.")
   

    for url in listURL["URL"]:
        item_number = url.split("/")[-1]
        item_info = fetch_picclick_item_info(item_number)

        if item_info:
           
            filtered_url = pd.DataFrame(item_info)
            filtered_url = [url for url in item_info["Images"] if url.startswith("http")]   #remove any links that dont start with http
            #filtered_url = pd.DataFrame(filtered_url)  #convert to a dataframe
            #filtered_url.rename(columns={0:"Images"}, inplace=True)  #Set the column name back to Images
                        #imgURL = pd.concat(([imgURL, filtered_url]), ignore_index=True)
           
            for links in filtered_url:
                global imgcount
                imgcount = (imgcount + 1)#keeps cound of the photos downloaded to apppend to file name
                download_images(links, save_folder)  #download each image in imgurl
           
            #imgURL = pd.concat([imgURL, pd.DataFrame([filtered_url["Images"]])], ignore_index=True)

            scrapedlist.append(url)
       
       
    #imgURL = imgURL.stack().reset_index(drop=True).to_frame(name="URL")  #collapse the dataframe to one long list
    #imgURL = [url.replace("s-l140", "s-l1600") for url in imgURL["URL"]] #collect full size images

        savescrapes = pd.DataFrame(scrapedlist)
        savescrapes.rename(columns={0:"URL"}, inplace=True)  #Set the column name back to URL

        if not os.path.exists(already_scraped):   #write the already_scraped file if it doesnt exist
            savescrapes.to_csv(already_scraped, index=False)
        else:      
            savescrapes.to_csv(already_scraped, mode='a', index=False)  #mode='a' Appends data instead of overwriting. index=False Excludes the index from being written. header=False Avoids writing the header again when appending.


 
#save_to_json(item_info)

query_user()  #start the script

#https://picclick.co.uk/-N.html
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
Weird, it knows it is 1kb in size. Is it an access rights issue? I'm sure you tried sudo nano /filepath..../automount.sh or gedit or other text editor instead of nano
For me, I just browse to that folder and open everything. Never seen that before.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
@drsnooker yeah I have tried opening them with nano, they always return something like this which is why I wondered about encryption... however its always these characters and only these characters.

Nano.png
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
I realized I had an error with some of the wifi password keyspace in Fios-F1nDr. Since this is the only useful part of the tool currently, here is an updated ref_data_lines that will fix the issue.


Code:
    ref_data_lines = [
"Mac_Start,Mac_End,Serial_start,Serial_End,M2HR,W_Format,W_Len,A_Format,A_Len,Date,HW,Model",
"04.A2.22.00.00.00,04.A2.22.AE.B8.7E,0,0,0,1,16,1,16,0,0,",
"04.A2.22.AE.B8.89,04.A2.22.BA.30.A2,G401119042900000,G401119042968331,11,1,16,1,16,190429,1102,G3100",
"04.A2.22.BA.30.A6,04.A2.22.C3.3A.BB,G401119061100000,G401119061153855,11,1,16,1,16,190611,1103,G3100",
"04.A2.22.C3.3A.C6,04.A2.22.CB.A8.3C,G401119061800000,G401119061850210,11,1,16,1,16,190618,1103,G3100",
"04.A2.22.C7.E3.01,04.A2.22.CB.E9.69,E301119062000000,E301119062043964,6,1,16,1,16,190620,1102,E3200",
"04.A2.22.CB.E9.6C,04.A2.22.DB.74.3E,G401119062600000,G40111906292598,11,1,16,1,16,190626,1103,G3100",
"04.A2.22.D3.FF.3A,04.A2.22.DB.FD.50,G401119071600000,G401119071647618,11,1,16,1,16,190716,1103,G3100",
"04.A2.22.DB.FD.57,04.A2.22.DF.45.2D,E301119072900000,E301119072935833,6,1,16,1,16,190729,1102,E3200",
"04.A2.22.DE.4F.57,04.A2.22.DF.45.39,E301119073000000,E301119073010491,6,1,16,1,16,190730,1102,E3200",
"04.A2.22.DF.2E.17,04.A2.22.E1.1D.11,E301119073000000,E301119073021119,6,1,16,1,16,190730,1102,E3200",
"04.A2.22.E1.1D.12,04.A2.22.EA.27.27,G401119081000000,G401119081053855,11,1,16,1,16,190810,1103,G3100",
"04.A2.22.E2.B5.72,04.A2.22.E5.5E.A1,G401119073100000,G401119073115853,11,1,16,1,16,190731,1103,G3100",
"04.A2.22.E5.5E.A2,04.A2.22.F3.31.E2,G401119081300000,G401119081382368,11,1,16,1,16,190813,1103,G3100",
"04.A2.22.F3.31.E2,04.A2.22.F7.22.67,G401119082300000,G401119082323471,11,1,16,1,16,190823,1103,G3100",
"04.A2.22.F7.22.72,04.A2.22.F8.3E.DB,G401119081390000,G401119081396619,11,1,16,1,16,190813,1103,G3100",
"04.A2.22.F8.3E.DC,04.A2.22.FA.2B.90,0,0,Unknown,1,16,1,16,0,0,",
"04.A2.22.FA.2B.9A,04.A2.22.FB.FE.F9,G401119092000000,G401119092010877,11,1,16,1,16,190920,1103,G3100",
"04.A2.22.FB.FF.04,04.A2.22.FF.FF.FF,0,0,Unknown,1,16,1,16,0,0,",
"B8.F8.53.00.00.00,B8.F8.53.02.8D.36,0,0,Unknown,1,16,1,16,0,0,",
"B8.F8.53.02.8D.41,B8.F8.53.05.93.61,G401119092620000,G401119092638016,11,1,16,1,16,190926,1103,G3100",
"B8.F8.53.05.93.61,B8.F8.53.07.27.6A,G401119101100000,G401119101109403,11,1,16,1,16,191011,1103,G3100",
"B8.F8.53.07.27.75,B8.F8.53.08.BF.CA,G401119101400000,G401119101409503,11,1,16,1,16,191014,1103,G3100",
"B8.F8.53.08.BF.D5,B8.F8.53.09.CF.B2,G401119101600000,G401119101606327,11,1,16,1,16,191016,1103,G3100",
"B8.F8.53.09.D0.15,B8.F8.53.0A.E0.4A,G401119101700000,G401119101706335,11,1,16,1,16,191017,1103,G3100",
"B8.F8.53.0A.E0.55,B8.F8.53.0B.49.17,G401119101800000,G401119101802438,11,1,16,1,16,191018,1103,G3100",
"B8.F8.53.0B.49.0E,B8.F8.53.0D.F6.F2,0,0,Unknown,1,16,1,16,0,0,",
"B8.F8.53.0D.97.D2,B8.F8.53.11.2B.8C,E301119100900000,E301119100939071,6,1,16,1,16,191009,1102,E3200",
"B8.F8.53.11.2B.8D,B8.F8.53.1C.67.52,G401119103000000,G401119103066927,11,1,16,1,16,191030,1103,G3100",
"B8.F8.53.1C.67.5D,B8.F8.53.20.20.3D,G401119111200000,G401119111222176,11,1,16,1,16,191112,1103,G3100",
"B8.F8.53.20.20.3D,B8.F8.53.24.7C.D3,G401119110800000,G401119110825986,11,1,16,1,16,191108,1104,G3100",
"B8.F8.53.24.E9.62,B8.F8.53.25.C1.08,E301119111100000,E301119111109201,6,1,16,1,16,191111,1102,E3200",
"B8.F8.53.25.C1.09,B8.F8.53.28.DC.42,G401119111500000,G401119111518507,11,1,16,1,16,191115,1103,G3100",
"B8.F8.53.28.DC.49,B8.F8.53.2B.99.AA,G401119120200000,G401119120216323,11,1,16,1,16,191202,1103,G3100",
"B8.F8.53.2B.89.3A,B8.F8.53.2B.D3.74,E301119112500000,E301119112503167,6,1,16,1,16,191125,1102,E3200",
"B8.F8.53.2B.D3.7A,B8.F8.53.2C.FC.74,E301119120500000,E301119120512671,6,1,16,1,16,191205,1102,E3200",
"B8.F8.53.2C.FC.75,B8.F8.53.2F.1D.C6,G401119120500000,G401119120512691,11,1,16,1,16,191205,1103,G3100",
"B8.F8.53.2F.1D.CD,B8.F8.53.34.F7.22,G401119120700000,G401119120734848,11,1,16,1,16,191207,1103,G3100",
"B8.F8.53.34.F7.2D,B8.F8.53.35.B1.B4,G401119121700000,G401119121704341,11,1,16,1,16,191217,1103,G3100",
"B8.F8.53.35.B1.BD,B8.F8.53.3B.0B.27,G401119120900000,G401119120931870,11,1,16,1,16,191209,1103,G3100",
"B8.F8.53.3B.0B.31,B8.F8.53.3C.A3.86,G401120010700000,G401120010709503,11,1,16,1,16,200107,1103,G3100",
"B8.F8.53.3C.A3.8B,B8.F8.53.3E.3B.EB,G401120010900000,G401120010909504,11,1,16,1,16,200109,1103,G3100",
"B8.F8.53.3E.3B.F1,B8.F8.53.40.1F.62,G401120011000000,G401120011011251,11,1,16,1,16,200110,1103,G3100",
"B8.F8.53.40.1F.65,B8.F8.53.43.05.09,G401120011300000,G401120011317260,11,1,16,1,16,200113,1103,G3100",
"B8.F8.53.43.05.11,B8.F8.53.44.90.98,G401120011600000,G401120011609205,11,1,16,1,16,200116,1103,G3100",
"B8.F8.53.44.90.9F,B8.F8.53.4B.03.D3,G401120010200000,G401120010238428,11,1,16,1,16,200102,1103,G3100",
"B8.F8.53.4B.03.DC,B8.F8.53.4C.AE.D2,E301120011000000,E301120011018217,6,1,16,1,16,200110,1102,E3200",
"B8.F8.53.4C.AE.D6,B8.F8.53.51.B8.FE,G402120021000000,G402120021041285,8,1,16,1,16,200210,1104,G3100",
"B8.F8.53.51.B9.01,B8.F8.53.52.1B.F9,G402120022400000,G402120022403167,8,1,16,1,16,200224,1104,G3100",
"B8.F8.53.52.1C.01,B8.F8.53.52.E1.F9,G402120022500000,G402120022506335,8,1,16,1,16,200225,1104,G3100",
"B8.F8.53.52.E2.01,B8.F8.53.54.6D.F9,G402120022600000,G402120022612671,8,1,16,1,16,200226,1104,G3100",
"B8.F8.53.54.6E.01,B8.F8.53.55.33.F9,G402120022900000,G402120022906335,8,1,16,1,16,200229,1104,G3100",
"B8.F8.53.55.34.01,B8.F8.53.57.8C.39,G402120030300000,G402120030319207,8,1,16,1,16,200303,1104,G3100",
"B8.F8.53.57.8C.41,B8.F8.53.58.52.39,G402120031000000,G402120031006335,8,1,15,1,16,200310,1104,G3100",
"B8.F8.53.58.52.41,B8.F8.53.59.7B.39,G402120031100000,G402120031109503,8,1,16,1,16,200311,1103,G3100",
"B8.F8.53.59.7B.41,B8.F8.53.5A.41.39,G402120031300000,G402120031306335,8,1,16,1,16,200313,1104,G3100",
"B8.F8.53.5A.41.41,B8.F8.53.5B.CD.39,G402120031400000,G402120031412671,8,1,16,1,16,200314,1004,G3100",
"B8.F8.53.5B.CD.41,B8.F8.53.5F.4B.89,G402120031800000,G402120031828617,8,1,15,1,16,200318,1104,G3100",
"B8.F8.53.5F.4B.90,B8.F8.53.60.11.88,G402120040800000,G402120040806335,8,1,15,1,16,200408,1104,G3100",
"B8.F8.53.60.11.90,B8.F8.53.62.00.88,G402120040900000,G402120040915839,8,1,15,1,16,200409,1104,G3100",
"B8.F8.53.62.00.90,B8.F8.53.62.C6.88,G402120041200000,G402120041206335,8,1,15,1,16,200412,1104,G3100",
"B8.F8.53.62.C6.90,B8.F8.53.67.0D.C8,G402120032500000,G402120032535047,8,1,15,1,16,200325,1104,G3100",
"B8.F8.53.67.0D.D0,B8.F8.53.67.D3.C8,G402120050600000,G402120050606336,8,1,15,1,16,200506,1104,G3100",
"B8.F8.53.67.D3.D0,B8.F8.53.69.5F.C8,G402120050800000,G402120050812671,8,1,15,1,16,200508,1104,G3100",
"B8.F8.53.68.99.D0,B8.F8.53.68.B8.B1,G402120050900000,G402120050900989,8,1,15,1,16,200509,1104,G3100",
"B8.F8.53.69.5F.D0,B8.F8.53.6D.80.88,G402120051100000,G402120051133815,8,1,15,1,16,200511,1104,G3100",
"B8.F8.53.69.C2.D0,B8.F8.53.6A.88.C8,G402120051400000,G402120051406335,8,1,15,1,16,200514,1104,G3100",
"B8.F8.53.6A.88.D0,B8.F8.53.6B.EE.48,G402120051600000,G402120051611439,8,1,15,1,16,200516,1104,G3100",
"B8.F8.53.6B.EE.50,B8.F8.53.6D.80.88,G402120051800000,G402120051812871,8,1,15,1,16,200518,1104,G3100",
"B8.F8.53.6D.80.90,B8.F8.53.70.35.88,G402120060600000,G402120060622175,8,1,15,1,16,200606,1104,G3100",
"B8.F8.53.70.35.90,B8.F8.53.72.0F.59,G402120052600000,G402120052634649,8,1,15,1,16,200526,1104,G3100",
"B8.F8.53.73.AA.58,B8.F8.53.73.B9.F9,G402120070200000,G402120070200501,8,1,15,1,16,200702,1104,G3100",
"B8.F8.53.74.70.58,B8.F8.53.75.36.50,G402120070300000,G402120070306335,8,1,15,1,16,200703,1104,G3100",
"B8.F8.53.75.36.58,B8.F8.53.77.52.70,G402120070500000,G402120070517284,8,1,15,1,16,200705,1104,G3100",
"B8.F8.53.77.52.78,B8.F8.53.7D.EB.B0,G402120062600000,G402120062654055,8,1,15,1,16,200626,1104,G3100",
"B8.F8.53.7D.EB.B8,B8.F8.53.7F.14.B0,G402120072300000,G402120072309503,8,1,15,1,16,200723,1104,G3100",
"B8.F8.53.7F.14.B8,B8.F8.53.85.C5.38,G402120071800000,G402120071854800,8,1,15,1,16,200718,1104,G3100",
"B8.F8.53.85.C5.40,B8.F8.53.8B.AB.A8,G402120081100000,G402120081148333,8,1,15,1,16,200811,1104,G3100",
"B8.F8.53.8B.AB.B0,B8.F8.53.95.FB.A8,G402120081200000,G402120081284479,8,1,15,1,16,200812,1104,G3100",
"B8.F8.53.95.FB.B0,B8.F8.53.9E.F3.F8,G402120082000000,G402120082073481,8,1,15,1,16,200820,1104,G3100",
"B8.F8.53.9E.AF.32,B8.F8.53.A0.F4.30,E302120082400000,E302120082424789,6,1,15,1,16,200824,1103,E3200",
"B8.F8.53.A0.F4.34,B8.F8.53.A1.62.2C,G402120082800000,G402120082803519,8,1,15,1,16,200828,1104,G3100",
"B8.F8.53.A1.62.34,B8.F8.53.AA.85.1C,G402120083000000,G402120083074845,8,1,15,1,16,200830,1104,G3100",
"B8.F8.53.AA.85.1E,B8.F8.53.AD.F8.DA,E302120090800000,E302120090837706,6,1,15,1,16,200908,1103,E3200",
"B8.F8.53.AD.F8.DC,B8.F8.53.B6.85.24,G402120091500000,G402120091570025,8,1,15,1,16,200915,1104,G3100",
"B8.F8.53.B6.85.26,B8.F8.53.B7.01.8E,E302120091500000,E302120091505308,6,1,15,1,16,200915,1103,E3200",
"B8.F8.53.B7.01.8F,B8.F8.53.B7.98.C3,0,0,Unknown,1,15,1,16,0,0,",
"B8.F8.53.B7.98.C4,B8.F8.53.B8.06.BC,G402120101500000,G402120101503519,8,1,15,1,16,201015,1104,G3100",
"B8.F8.53.B8.06.C4,B8.F8.53.BB.08.BC,G402120101600000,G402120101624639,8,1,15,1,16,201016,1104,G3100",
"B8.F8.53.BB.08.C4,B8.F8.53.BB.76.BC,G402120101800000,G402120101803519,8,1,15,1,16,201018,1104,G3100",
"B8.F8.53.BB.76.C4,B8.F8.53.C0.9E.BC,G402120101900000,G402120101942240,8,1,15,1,16,201019,1104,G3100",
"B8.F8.53.C0.9E.C4,B8.F8.53.C2.56.C4,G402120102400000,G402120102414080,8,1,15,1,16,201024,1104,G3100",
"B8.F8.53.C2.56.C4,B8.F8.53.C2.C4.BC,G402120102500000,G402120102503519,8,1,15,1,16,201025,1104,G3100",
"B8.F8.53.C2.C4.C4,B8.F8.53.C4.7C.C4,G402120102600000,G402120102614080,8,1,15,1,16,201026,1104,G3100",
"B8.F8.53.C4.7C.C6,B8.F8.53.C5.D1.1C,E302120102500000,E302120102514521,6,1,15,1,16,201025,1103,E3200",
"B8.F8.53.C5.D1.20,B8.F8.53.CD.91.D0,G402120100700000,G402120100763510,8,1,15,1,16,201007,1104,G3100",
"B8.F8.53.CD.91.D2,B8.F8.53.CE.C3.5A,E302120100700000,E302120100713036,6,1,15,1,16,201007,1103,E3200",
"B8.F8.53.CE.C3.60,B8.F8.53.B7.98.BE,0,0,Unknown,1,15,1,16,0,0,",
"B8.F8.53.CE.D4.C8,B8.F8.53.D3.58.68,G402120101300000,G402120101336980,8,1,15,1,16,201013,1104,G3100",
"B8.F8.53.D3.58.6A,B8.F8.53.D5.EC.64,E302120101500000,E302120101528159,6,1,15,1,16,201015,1103,E3200",
"B8.F8.53.D5.EC.68,B8.F8.53.D7.36.60,G402120111200000,G402120111210559,8,1,15,1,16,201112,1104,G3100",
"B8.F8.53.D7.36.68,B8.F8.53.DA.38.60,G402120111300000,G402120111324640,8,1,15,1,16,201113,1104,G3100",
"B8.F8.53.DA.38.68,B8.F8.53.DC.01.D0,G402120111700000,G402120111714637,8,1,15,1,16,201117,1104,G3100",
"B8.F8.53.DC.01.D2,B8.F8.53.DC.AF.00,E302120111300000,E302120111307389,6,1,15,1,16,201113,1103,E3200",
"B8.F8.53.DC.6F.D0,B8.F8.53.E3.C6.D8,G402120111300000,G402120111360129,8,1,15,1,16,201113,1104,G3100",
"B8.F8.53.E3.C6.E0,B8.F8.53.EB.14.D8,G402120120200000,G402120120259839,8,1,15,1,16,201202,1104,G3100",
"B8.F8.53.EB.14.E0,B8.F8.53.F3.08.C8,G402120121000000,G402120121065150,8,1,15,1,16,201210,1104,G3100",
"B8.F8.53.F3.08.D0,B8.F8.53.F8.30.C8,G402120122300000,G402120122342239,8,1,15,1,16,201223,1104,G3100",
"B8.F8.53.F8.30.D0,B8.F8.53.FA.C4.C8,G402121011800000,G402121011821120,8,1,15,1,16,210118,1104,G3100",
"B8.F8.53.FA.C4.D0,B8.F8.53.FF.BF.F8,G402121010600000,G402121010640805,8,1,15,1,16,210106,1104,G3100",
"B8.F8.53.FF.BF.F9,B8.F8.53.FF.FF.FF,0,0,Unknown,1,15,1,16,0,0,",
"3C.BD.C5.00.00.00,3C.BD.C5.01.25.57,0,0,Unknown,1,15,1,9,0,0,",
"3C.BD.C5.01.25.58,3C.BD.C5.01.F5.78,G402121011510000,G402121011516660,8,1,15,1,16,210115,1104,G3100",
"3C.BD.C5.09.3B.F8,3C.BD.C5.0B.B4.78,G402121030100000,G402121030120240,8,1,15,1,16,210301,1104,G3100",
"3C.BD.C5.0B.B4.7A,3C.BD.C5.0B.BC.FD,E302121030300000,E302121030300364,6,1,15,1,16,210303,1103,E3200",
"3C.BD.C5.0C.30.38,3C.BD.C5.16.4A.C0,G402121021700000,G402121021782769,8,1,15,1,16,210217,1104,G3100",
"3C.BD.C5.16.4A.C8,3C.BD.C5.1E.E5.E8,G402121031200000,G402121031270500,8,1,15,1,16,210312,1104,G3100",
"3C.BD.C5.1E.E5.EA,3C.BD.C5.22.55.E6,E302121031200000,E302121031237546,6,1,15,1,16,210312,1103,E3200",
"3C.BD.C5.22.55.E8,3C.Bd.C5.2B.5B.E0,G402121033100000,G402121033173919,8,1,15,1,16,210331,1104,G3100",
"3C.BD.C5.2B.5B.E8,3C.BD.C5.32.A2.88,G402121041400000,G402121041459604,8,1,15,1,16,210414,1104,G3100",
"3C.BD.C5.32.A2.90,3C.BD.C5.35.36.88,G402121042700000,G402121042721119,8,1,15,1,16,210427,1104,G3100",
"3C.BD.C5.35.36.90,3C.BD.C5.37.5C.88,G402121051500000,G402121051517599,8,1,15,1,16,210515,1104,G3100",
"3C.BD.C5.37.5C.90,3C.BD.C5.3D.D3.38,G402121051800000,G402121051852950,8,1,15,1,16,210518,1104,G3100",
"3C.BD.C5.3D.D3.40,3C.BD.C5.42.27.40,G402121060700000,G402121060735456,8,1,15,1,16,210607,1104,G3100",
"3C.BD.C5.42.27.4C,3C.BD.C5.46.3E.C4,G402121062200000,G402121062233519,8,1,15,1,16,210622,1104,G3100",
"3C.BD.C5.46.3E.CC,3C.BD.C5.47.F6.C4,G402121071300000,G402121071314079,8,1,15,1,16,210713,1104,G3100",
"3C.BD.C5.47.F6.CC,3C.BD.C5.49.AE.C4,G402121072100000,G402121072114079,8,1,15,1,16,210721,1104,G3100",
"3C.BD.C5.49.AE.CC,3C.BD.C5.50.05.4C,G402121070100000,G402121070151920,8,1,15,1,16,210701,1104,G3100",
"3C.BD.C5.50.05.4C,3C.BD.C5.57.37.CC,G402121072000000,G402121072058960,8,2,15,2,9,210720,1104,G3100",
"3C.BD.C5.57.37.CE,3C.BD.C5.59.22.C0,E302121072000000,E302121072020947,6,2,15,2,9,210720,1103,E3200",
"3C.BD.C5.59.24.74,3C.BD.C5.5D.02.74,G402121080200000,G402121080231680,8,2,15,2,9,210802,1104,G3100",
"3C.BD.C5.5D.02.76,3C.BD.C5.60.3B.70,E302121080300000,E302121080335199,6,2,15,2,9,210803,1103,E3200",
"3C.BD.C5.60.3B.74,3C.BD.C5.67.20.6C,G402121081100000,G402121081156479,8,2,15,2,9,210811,1104,G3100",
"3C.BD.C5.67.20.74,3C.BD.C5.69.B8.CC,G402121081160000,G402121081181259,8,2,15,2,9,210811,1104,G3100",
"3C.BD.C5.69.B8.D4,3C.BD.C5.71.07.FC,G402121082000000,G402121082059877,8,2,15,2,9,210820,1104,G3100",
"3C.BD.C5.71.08.00,3C.BD.C5.76.8D.C8,G402121082600000,G402121082645241,8,2,15,2,9,210826,1104,G3100",
"3C.BD.C5.76.8D.CC,3C.BD.C5.7E.9C.44,G402121090800000,G402121090865999,8,2,15,2,9,210908,1104,G3100",
"3C.BD.C5.7E.9C.4C,3C.BD.C5.83.03.CC,G402121091600000,G402121091636080,8,2,15,2,9,210916,1104,G3100",
"3C.BD.C5.83.03.CE,3C.BD.C5.85.4C.A4,E302121091600000,E302121091624953,6,2,15,2,9,210916,1103,E3200",
"3C.BD.C5.85.4C.AA,3C.BD.C5.8C.CB.CB,0,0,Unknown,2,15,2,9,0,0,",
"3C.BD.C5.8C.CB.CC,3C.BD.C5.94.1B.5C,G402121101500000,G402121101559890,8,2,15,2,9,211015,1104,G3100",
"3C.BD.C5.94.1B.5E,3C.BD.C5.9B.67.6E,E302121101500000,E302121101579704,6,2,15,2,9,211015,1103,E3200",
"3C.BD.C5.9B.67.72,3C.BD.C5.A1.71.B2,G402121110500000,G402121110549480,8,2,15,2,9,211105,1104,G3100",
"3C.BD.C5.A1.71.B4,3C.BD.C5.A7.EE.5E,E302121110500000,E302121110570855,6,2,15,2,9,211105,1103,E3200",
"3C.BD.C5.A7.EE.64,3C.BD.C5.AD.16.5E,E302121112300000,E302121112356319,6,2,15,2,9,211123,1103,E3200",
"3C.BD.C5.AD.16.64,3C.BD.C5.AE.AE.34,E302122010100000,E30212201017400,6,2,15,2,9,220101,1103,E3200",
"3C.BD.C5.AE.AE.34,3C.BD.C5.B2.95.8E,E302121122100000,E302121122142639,6,2,15,2,9,211221,1103,E3200",
"3C.BD.C5.B2.95.94,3C.BD.C5.B4.2D.5E,E302122011100000,E302122011117399,6,2,15,2,9,220111,1103,E3200",
"3C.BD.C5.B4.2D.62,3C.BD.C5.B5.09.9A,G402122021400000,G402122021407047,8,2,15,2,9,220214,1104,G3100",
"3C.BD.C5.B5.09.A2,3C.BD.C5.B5.E5.9A,G402122031000000,G402122031007039,8,2,15,2,9,220310,1104,G3100",
"3C.BD.C5.B5.E5.A2,3C.BD.C5.B8.0B.9A,G402122031100000,G402122031117599,8,2,15,2,9,220311,1104,G3100",
"3C.BD.C5.B8.0B.A2,3C.BD.C5.C1.ED.A2,G402122031500000,G402122031580960,8,2,15,2,9,220315,1104,G3100",
"3C.BD.C5.C1.ED.A4,3C.BD.C5.C2.40.1E,E302122031300000,E302122031303519,6,2,15,2,9,220313,1103,E3200",
"3C.BD.C5.C2.40.24,3C.BD.C5.C2.89.E6,E302122031400000,E302122031403147,6,2,15,2,9,220314,1103,E3200",
"3C.BD.C5.C2.89.EC,3C.BD.C5.C4.13.D7,0,0,Unknown,2,15,2,9,101137,1103,",
"3C.BD.C5.C4.34.88,3C.BD.C5.C7.75.14,E302122031200000,E302122031235522,6,2,15,2,9,220312,1103,E3200",
"3C.BD.C5.C7.75.15,3C.BD.C5.CD.A1.1F,0,0,Unknown,2,15,2,9,0,0,",
"3C.BD.C5.CD.A1.20,3C.BD.C5.CF.39.86,E302122052200000,E302122052217426,6,2,15,2,9,220522,1103,E3200",
"3C.BD.C5.CF.39.8C,3C.BD.C5.D2.C8.06,E302122091400000,E302122091438847,6,2,15,2,9,220914,1103,E3200",
"3C.BD.C5.D2.C8.0C,3C.BD.C5.DA.E1.12,E302122091500000,E302122091588450,6,2,15,2,9,220915,1103,E3200",
"3C.BD.C5.DA.E1.18,3C.BD.C5.E2.EA.E2,E302122110900000,E302122110987799,6,2,15,2,9,221109,1103,E3200",
"3C.BD.C5.E2.EA.E8,3C.BD.C5.E5.83.92,E302123011800000,E302123011828359,6,2,15,2,9,230118,1103,E3200",
"3C.BD.C5.E5.83.98,3C.BD.C5.E5.E0.F2,E302123011000000,E302123011003983,6,2,15,2,9,230110,1103,E3200",
"3C.BD.C5.E8.34.90,3C.BD.C5.EE.54.0A,E302123030400000,E302123030466879,6,2,15,2,9,230304,1103,E3200",
"3C.BD.C5.EE.54.10,3C.BD.C5.F1.11.4A,E302123033000000,E302123033029919,6,2,15,2,9,230330,1103,E3200",
"3C.BD.C5.F1.11.50,3C.BD.C5.F1.E1.E2,E302123042100000,E302123042108899,6,2,15,2,9,230421,1103,E3200",
"3C.BD.C5.F1.E1.E8,3C.BD.C5.F3.55.22,E302123042600000,E302123042630800,6,2,15,2,9,230426,1103,E3200",
"3C.BD.C5.F3.55.28,3C.BD.C5.F4.B3.C2,E302123050700000,E302123050714959,6,2,15,2,9,230507,1103,E3200",
"3C.BD.C5.F4.B3.C8,3C.BD.C5.F7.35.AA,E302123052000000,E302123052027387,6,2,15,2,9,230520,1103,E3200",
"3C.BD.C5.F7.35.B0,3C.BD.C5.F8.7A.CA,E302123062000000,E302123062013871,6,2,15,2,9,230620,1103,E3200",
"3C.BD.C5.F8.7A.D0,3C.BD.C5.FA.EA.6A,E302123080600000,E302123080626607,6,2,15,2,9,230806,1103,E3200",
"3C.BD.C5.FA.EA.70,3C.BD.C5.FB.07.9E,E302123080100000,E302123080101245,6,2,15,2,9,230801,1103,E3200",
"3C.BD.C5.FB.07.9F,3C.BD.C5.FF.FF.FF,0,0,Unknown,2,15,2,9,0,0,",
"DC.F5.1B.5F.EA.4F,DC.F5.1B.5F.EA.51,Unknown,Unknown,6,2,16,2,9,223550,1103,E3200",
"DC.F5.1B.62.0C.B6,DC.F5.1B.64.2B.14,E302123101100000,E302123101123141,6,2,15,2,9,231011,1103,E3200",
"DC.F5.1B.64.2B.1A,DC.F5.1B.64.54.54,E302123103100000,E302123103101759,6,2,15,2,9,231031,1103,E3200",
"DC.F5.1B.64.54.5A,DC.F5.1B.66.E8.54,E302123110200000,E302123110228160,6,2,15,2,9,231102,1103,E3200",
"DC.F5.1B.66.E8.5A,DC.F5.1B.67.F4.74,E302123112200000,E302123112211439,6,2,15,2,9,231122,1103,E3200",
"DC.F5.1B.67.F4.7A,DC.F5.1B.68.70.04,E302123113000000,E302123113005271,6,2,15,2,9,231130,1103,E3200",
"DC.F5.1B.60.14.55,DC.F5.1B.60.14.57,Unknown,Unknown,6,2,15,2,9,329353,1103,E3200",
]
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
I just found a major improvement to the image sorting/reading code!

I asked AI to help me with training a model to better detect and read the QR codes. That didn’t go very well, and I ended up abandoning that fairly quickly. So then I went back to old fashioned googling. I found a lot of people with the same issue, a phone can read the QR code but not zbarImg. Some people offered suggestions for image preprocessing, but those tended to make the QR less readable for me. Then I finally came across the answer on stack overflow. Based on this I tried swapping out zbarImg with QReader.

I then tested both scripts. Based on a sample of previously scraped images, zbarImg was able to read 6 QR codes, while QReader read 34! A few of these were actually new entries that I was previously unable to read or verify visually! This is a huge game changer as now the script is quite good at extracting the QR data. Here is the updated script, all we really did was swap QReader in for scan_qr function, but it also cleaned up the code a bit and I removed a bunch of old/bad code from testing thatI left in last time.

Code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""

Use Apple's Vision Framework via PyObjC to detect text in images
modified code from: https://gist.github.com/RhetTbull/1c34fc07c95733642cffcd1ac587fc4c

Necessary Dependencies:
python3 -m pip install pyobjc-core pyobjc-framework-Quartz pyobjc-framework-Vision wurlitzer
brew install zbar

This script process all of the images in folder_path using apple vision to extract text.
It checks to see if any of the keywords are found in the image, and extracts the relevent info
All of the info is saved to <file name> and relevent photos are moved to the "found" folder
in case they need to be referenced later.  Any photos without keywords are deleted.
"""

import pathlib
from qreader import QReader
import cv2
import sys
import Quartz
import Vision
import os
import shutil
import pandas as pd
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
import subprocess

import re



KEYWORDS = ["Wi-Fi Name:", "Wi-Fi Password:", "Admin URL:", "Admin Password:",
            "Model Number:", "Serial #.", "WAN MAC:", "HW ver.:", "Shipped FW ver.:"]


# Define the keyword mappings
qr_to_keyword_map = {
        "WIFI:S:": "Wi-Fi Name:",
        "T:WPA;P:": "Wi-Fi Password:",
        "ROUTER:M:": "Model Number:",
        "S:": "Serial #.",
        "W:": "WAN MAC:",
        "I:admin;P:": "Admin Password:",
}

QRKEYWORDS = ["WIFI:S:", "T:WPA;P:", "I:admin;P:", "ROUTER:M:", "S:"]
KEYWORDS = ["Wi-Fi Name:", "Wi-Fi Password:", "Admin URL:", "Admin Password:",
            "Model Number:", "Serial #.", "WAN MAC:", "HW ver.:", "Shipped FW ver.:"]

keywords_lower = [keyword.lower() for keyword in KEYWORDS]  #makes keywords lowercase(case insensitive) for better matching

global qr_found

            
def image_to_text(img_path, lang="eng"):
    input_url = NSURL.fileURLWithPath_(img_path)

    with pipes() as (out, err):
    # capture stdout and stderr from system calls
    # otherwise, Quartz.CIImage.imageWithContentsOfURL_
    # prints to stderr something like:
    # 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
    # 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
        input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)

    vision_options = NSDictionary.dictionaryWithDictionary_({})
    vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
        input_image, vision_options
    )
    results = []
    handler = make_request_handler(results)
    vision_request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(handler)
    error = vision_handler.performRequests_error_([vision_request], None)

    return results

def scan_qr(image_path, img_name):
    
    try:
        qread_img = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB)
        # Detect and Decode the QR
        decoded_objects = detector.detect_and_decode(image=qread_img)
    
        if decoded_objects:
    
            qr_read = pd.DataFrame(decoded_objects)
            qr_read["QR_read"] = "Yes"
            qr_found = True

    except FileNotFoundError:
        print("Error: QReader is not installed or not found in PATH.")
    
    return decoded_objects

    #print(detector.detect_and_decode(image=qreadimg))
    

def make_request_handler(results):
    """ results: list to store results """
    if not isinstance(results, list):
        raise ValueError("results must be a list")

    def handler(request, error):
        if error:
            print(f"Error! {error}")
        else:
            observations = request.results()
            for text_observation in observations:
                recognized_text = text_observation.topCandidates_(1)[0]
                results.append([recognized_text.string(), recognized_text.confidence()])
    return handler


def extract_values(qrcodes, results, QRKEYWORDS, KEYWORDS):
    global match_found
    match_found = False
    
    extracted_data = {key: "" for key in KEYWORDS}  # Initialize a dictionary with empty values
    
    # Convert QRKEYWORDS to KEYWORDS mapping
    qr_to_keyword_map = {
        "WIFI:S:": "Wi-Fi Name:",
        "T:WPA;P:": "Wi-Fi Password:",
        "I:admin;P:": "Admin Password:",
        "ROUTER:M:": "Model Number:",
        ";S:": "Serial #."
    }
    
    # **Step 1: Search QR Code Data**
    for qr_entry in qrcodes:
        data_str = str(qr_entry)  # Decode bytes to string
        for qr_key, keyword in qr_to_keyword_map.items():
            match = re.search(fr'{qr_key}([^;]+)', data_str)
            if match:
                extracted_data[keyword] = match.group(1).split("'")[0]  # Remove trailing artifacts
                match_found = True
                extracted_data["QR_found"] = "Yes"


    # **Step 2: Search OCR Text (Only if not found in QR)**
    for text_entry in results:
        text = str(text_entry)
        
        for keyword in KEYWORDS:
            if extracted_data[keyword]:  # Skip if already filled by QR
                continue
            match = re.search(fr'{keyword}([^;]+)', text, re.IGNORECASE)
            if match:
                extracted_data[keyword] = match.group(1).split("'")[0]   # Extract OCR value
                match_found = True

    # Convert to DataFrame
    extract_df = pd.DataFrame([extracted_data])
    return extract_df

#def main():
folder_path = input("Please input folder path of images: ") #where to find the images downloaded with photoscrape.py
folder_path = folder_path + "/"
folder_contents = os.listdir(folder_path)
os.makedirs(folder_path +"found/", exist_ok=True)
os.makedirs(folder_path +"not_found/", exist_ok=True)
global match_found, detector
match_found = False
detector = QReader()

matched_text = pd.DataFrame()   #the dataframe where well store all of oure matched text
clean_list = pd.DataFrame()


for imgs in folder_contents:
    
    
    if not imgs.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".tiff")):  #.lower makes the comparison case-insensitive
        continue
    match_found = False

    print ("Processing: " + imgs)
    img_path = (folder_path + imgs)
    
    qrcodes = scan_qr(img_path, imgs)
    results = image_to_text(img_path)
    
    # Extract values from QR and OCR results
    if qrcodes:
        #qr_data = extract_values(qrcodes)
        print("QR Code Found: " + str(qrcodes))
    if results:
        #ocr_data = extract_values(results)
        print ("OCR Text Found: " + str(results))
    
    if qrcodes or results:
        img_data = extract_values(qrcodes, results, QRKEYWORDS, KEYWORDS)
        img_data['IMG'] = imgs #add the image name to the info
    
    if match_found == True:
        print(f"✅ Match found! Moving: {imgs}")
        matched_text = pd.concat([matched_text, img_data], ignore_index=True)  #add the extracted info to our dataframe
        shutil.move(folder_path + imgs, folder_path + "found/" + imgs)    #moves the images to a folder named "found"
    else:
        print(f"❌ No match. Moving: {imgs}")
        shutil.move(folder_path + imgs, folder_path + "not_found/" + imgs)    #moves the images to a folder named "found"

        #os.remove(img_path)  #delete files where no info is found


#Now to do some cleaning to our matched_text
if not matched_text.empty:
    clean_list = matched_text #copy the matched text into a new dataframe
    clean_list = clean_list.fillna("") #remove any nan
    clean_list = clean_list.map(lambda x: x.replace(" ", "") if isinstance(x, str) else x)


    img_column = clean_list.pop('IMG')  # Removes "IMG" column but keeps it in memory

    clean_list["duplicates"] = clean_list.duplicated(keep="first").astype(int)  #adds a column "duplicates" and denotes duplicate with 0 or 1
    clean_list["duplicates"] = clean_list.groupby(clean_list.columns.drop(["duplicates"]).tolist())["duplicates"].transform("sum") #count the number of duplicates
    clean_list = clean_list.drop_duplicates() #.reset_index(drop=True)  # Drop duplicate rows and keep the first occurrence

    clean_list.insert(0, "IMG", img_column)

    clean_list = clean_list[["IMG"] + [col for col in clean_list.columns if col != "IMG"]] # ensures "IMG" is first column, while preserving the rest of the order
    clean_list = clean_list.sort_values(by="IMG").reset_index(drop=True) #Arranges list in alphabetical order based on image name
    
    clean_list = clean_list.reset_index(drop=True)

    clean_list.to_csv(folder_path + "/image_data.csv")

else:
    print("No matched images found!")

# the number in the results is the confidence score, It's important to note that a lower confidence score doesn't
#necessarily mean the recognized text is incorrect, but it does suggest that the system is less certain about
#its accuracy. In such cases, manual verification or additional processing might be warranted to ensure the information's correctness.
 

FiosFiend

Active member
Feedback: 0 / 0 / 0
Joined
Apr 6, 2025
Messages
79
Reaction score
63
Credits
1,563
@drsnooker do you have any experience extracting images with ubi_reader? It’s what binwalk is using under the hood to extract this firmware. Using the tool directly I am able to see some output that binwalk hides.

Initially extracting the firmware gives us an ubi.img. Using the command ubireader_extract_images ubi.img extracts 4 folders vol-METADATA.ubifs, vol-METADATACOPY.ubifs, vol-filestruct_full.bin.ubifs, vol-rootfs_ubifs.ubifs. Using the command ubireader_extract_files vol-rootfs_ubifs.ubifs gives us the filesystem, but with the following errors for every file. I think this is why I don’t see valid data in the extracted scripts, everything seems to look good otherwise.


Code:
decompress Warn: ZLib Error: Error -3 while decompressing data: invalid distance too far back
_process_reg_file Warn: inode num:3458 path:/Users/Routers/Fios-3100/Firmware/ubi.img/ubifs-root/sbin/arc_printk :can't concat NoneType to bytearray
decompress Warn: ZLib Error: Error -3 while decompressing data: invalid stored block lengths
_process_reg_file Warn: inode num:3575 path:/Users/Routers/Fios-3100/Firmware/ubi.img/ubifs-root/sbin/arc_fwupgrade :can't concat NoneType to bytearray


extracted_files.png
 

drsnooker

Active member
Contributor
VIP Member
Feedback: 0 / 0 / 0
Joined
Aug 1, 2020
Messages
448
Reaction score
727
Credits
4,064
Never dug that deep into ubireader. The guys at onekey-sec (unblob) are really responsive, so perhaps open an issue on the github. You might want to attach an example of the firmware so they can help figure out what's going on....
 

soxrok2212

Active member
Contributor
Feedback: 0 / 0 / 0
Joined
Dec 30, 2019
Messages
2,638
Reaction score
74
Credits
493
I don't really come on here because I while back there were some creeps on here that had developed a strange obsession with me... probably are still lurking.

Anyways, looking at the NAND dump I made of my G3100, the key is stored (multiple times) on a flash area labeled nVrAmDaT. Hate to break it to you but the algorithm probably doesn't exist for the main SSID.

Screenshot 2025-04-22 at 1.02.57 PM.png

HOWEVER, the reason I bought these and made a few dumps was to look for the algorithm of the hidden network that these broadcast. I think they were either for mesh or connected to STBs. This is from my G1100.

Screenshot 2025-04-22 at 1.58.47 PM.png
 
Top