342 lines
11 KiB
Go
342 lines
11 KiB
Go
/*
|
|
lexipwn (CLI version)
|
|
|
|
Can do the following on Google Pixel 6 and up to Pixel 9 (requires root access):
|
|
|
|
- manipulate devinfo partition (IMEIs, MACs, DSN, PSN, SKU etc)
|
|
- fix IMEI SHA (the bootmode PS tag needs to be set to factory)
|
|
- generate random but valid IMEIs based on the TAC/prefix
|
|
- run arbitrary AT commands on the modem
|
|
|
|
Usage: lexipwn [subcommand] [params]
|
|
|
|
Available subcommands:
|
|
|
|
* listps - list all tags in the PS tag area (TSV format)
|
|
* setpsval [name1] [value1] [name2] [value2] ... - set tag values in the PS tag area
|
|
* setpstype [name] [DIUS|DIFR] - set a tag type in the PS tag area
|
|
* fiximeisha - actualize the IMEI SHA string in the persist partition
|
|
(requires to be booted into the factory mode)
|
|
* genimei [prefix] - generate a random valid IMEI starting with [prefix]
|
|
(doesn't write this IMEI anywhere, so can be used without root)
|
|
* atcmd [command] - run a single-line AT command and output its result
|
|
|
|
Build with:
|
|
|
|
GOOS=linux GOARCH=arm64 go build -a -trimpath -gcflags=all="-l -B -e" -ldflags "-s -w"
|
|
(the result will be static; for dynamic PIE aarch64 builds, use GOOS=android instead)
|
|
|
|
Created by Luxferre in 2024-2025, released into public domain
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"strconv"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
/* a structure to describe a single tag in the PS or PSENV section */
|
|
type DevInfoTag struct {
|
|
Type [4]byte /* DIUS or DIFR */
|
|
FullLength uint32 /* name length + value length */
|
|
NameLength uint32
|
|
Name []byte
|
|
Value []byte
|
|
}
|
|
|
|
/* a structure to describe the entire devinfo partition image */
|
|
/* note that the unused areas might actually be used by the device */
|
|
type DevInfoImage struct {
|
|
Magic [4]byte /* must be DEVI */
|
|
VerMajor uint16
|
|
VerMinor uint16
|
|
UnusedArea1 [32]byte
|
|
BoardProject uint16
|
|
BoardStageCode uint8
|
|
BoardRevHi uint8
|
|
BoardRevLo uint8
|
|
BoardVariant uint8
|
|
SlotARetryCount uint8
|
|
SlotAFlags uint8
|
|
SlotBRetryCount uint8
|
|
SlotBFlags uint8
|
|
UnusedArea2 [74]byte
|
|
PStags []DevInfoTag
|
|
PSENVtags []DevInfoTag
|
|
}
|
|
|
|
/* info area byte offsets */
|
|
const offs_SlotARetryCount = 48
|
|
const offs_SlotBRetryCount = 52
|
|
const offs_PStags = 128
|
|
const offs_PSENVtags = 4096
|
|
|
|
/* read the file data into the DevInfoImage structure */
|
|
func readDevInfo(data []byte) DevInfoImage {
|
|
r := bytes.NewReader(data)
|
|
var info DevInfoImage
|
|
/* read the header area */
|
|
binary.Read(r, binary.BigEndian, &info.Magic)
|
|
binary.Read(r, binary.LittleEndian, &info.VerMajor)
|
|
binary.Read(r, binary.LittleEndian, &info.VerMinor)
|
|
binary.Read(r, binary.BigEndian, &info.UnusedArea1)
|
|
binary.Read(r, binary.LittleEndian, &info.BoardProject)
|
|
binary.Read(r, binary.LittleEndian, &info.BoardStageCode)
|
|
binary.Read(r, binary.LittleEndian, &info.BoardRevHi)
|
|
binary.Read(r, binary.LittleEndian, &info.BoardRevLo)
|
|
binary.Read(r, binary.LittleEndian, &info.BoardVariant)
|
|
r.Seek(offs_SlotARetryCount, io.SeekStart)
|
|
binary.Read(r, binary.LittleEndian, &info.SlotARetryCount)
|
|
binary.Read(r, binary.LittleEndian, &info.SlotAFlags)
|
|
r.Seek(offs_SlotBRetryCount, io.SeekStart)
|
|
binary.Read(r, binary.LittleEndian, &info.SlotBRetryCount)
|
|
binary.Read(r, binary.LittleEndian, &info.SlotBFlags)
|
|
binary.Read(r, binary.BigEndian, &info.UnusedArea2)
|
|
/* read the PS and PSENV tag areas */
|
|
info.PStags = make([]DevInfoTag, 0)
|
|
info.PSENVtags = make([]DevInfoTag, 0)
|
|
for {
|
|
var tag DevInfoTag
|
|
err := binary.Read(r, binary.BigEndian, &tag.Type)
|
|
if err != nil {
|
|
break
|
|
}
|
|
binary.Read(r, binary.LittleEndian, &tag.FullLength)
|
|
binary.Read(r, binary.LittleEndian, &tag.NameLength)
|
|
if tag.FullLength > 0 {
|
|
if tag.NameLength > 0 {
|
|
tag.Name = make([]byte, tag.NameLength)
|
|
binary.Read(r, binary.BigEndian, &tag.Name)
|
|
}
|
|
tag.Value = make([]byte, tag.FullLength - tag.NameLength)
|
|
binary.Read(r, binary.BigEndian, &tag.Value)
|
|
}
|
|
/* handle edge cases and populate the slices */
|
|
if tag.FullLength < 0xf00 && tag.NameLength > 0 {
|
|
curpos, _ := r.Seek(0, io.SeekCurrent)
|
|
if curpos < offs_PSENVtags {
|
|
info.PStags = append(info.PStags, tag)
|
|
} else {
|
|
info.PSENVtags = append(info.PSENVtags, tag)
|
|
}
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
/* write the DevInfoImage structure into 8192 bytes of data */
|
|
func writeDevInfo(info DevInfoImage) []byte {
|
|
buf := new(bytes.Buffer)
|
|
buf.Grow(8192)
|
|
/* write the header area */
|
|
binary.Write(buf, binary.BigEndian, info.Magic)
|
|
binary.Write(buf, binary.LittleEndian, info.VerMajor)
|
|
binary.Write(buf, binary.LittleEndian, info.VerMinor)
|
|
binary.Write(buf, binary.BigEndian, info.UnusedArea1)
|
|
binary.Write(buf, binary.LittleEndian, info.BoardProject)
|
|
binary.Write(buf, binary.LittleEndian, info.BoardStageCode)
|
|
binary.Write(buf, binary.LittleEndian, info.BoardRevHi)
|
|
binary.Write(buf, binary.LittleEndian, info.BoardRevLo)
|
|
binary.Write(buf, binary.LittleEndian, info.BoardVariant)
|
|
binary.Write(buf, binary.LittleEndian, uint16(0)) /* write two zeros */
|
|
binary.Write(buf, binary.LittleEndian, info.SlotARetryCount)
|
|
binary.Write(buf, binary.LittleEndian, info.SlotAFlags)
|
|
binary.Write(buf, binary.LittleEndian, uint16(0)) /* write two zeros again */
|
|
binary.Write(buf, binary.LittleEndian, info.SlotBRetryCount)
|
|
binary.Write(buf, binary.LittleEndian, info.SlotBFlags)
|
|
binary.Write(buf, binary.BigEndian, info.UnusedArea2)
|
|
/* write the PS tag area */
|
|
var curoffs uint32 = offs_PStags
|
|
for _, tag := range info.PStags {
|
|
binary.Write(buf, binary.BigEndian, tag.Type)
|
|
binary.Write(buf, binary.LittleEndian, tag.FullLength)
|
|
binary.Write(buf, binary.LittleEndian, tag.NameLength)
|
|
if tag.FullLength > 0 {
|
|
if tag.NameLength > 0 {
|
|
binary.Write(buf, binary.BigEndian, tag.Name)
|
|
}
|
|
binary.Write(buf, binary.BigEndian, tag.Value)
|
|
}
|
|
curoffs += 12 + tag.FullLength
|
|
}
|
|
/* write the last pseudo-tag of the section */
|
|
binary.Write(buf, binary.BigEndian, []byte("DIFR"))
|
|
binary.Write(buf, binary.LittleEndian, uint32(offs_PSENVtags - curoffs - 12))
|
|
/* fill in the zeros before the PSENV section */
|
|
skip := int(offs_PSENVtags - curoffs - 8)
|
|
for i:=0; i<skip; i++ {
|
|
binary.Write(buf, binary.LittleEndian, uint8(0))
|
|
}
|
|
/* write the PSENV tag area */
|
|
curoffs = offs_PSENVtags
|
|
for _, tag := range info.PSENVtags {
|
|
binary.Write(buf, binary.BigEndian, tag.Type)
|
|
binary.Write(buf, binary.LittleEndian, tag.FullLength)
|
|
binary.Write(buf, binary.LittleEndian, tag.NameLength)
|
|
if tag.FullLength > 0 {
|
|
if tag.NameLength > 0 {
|
|
binary.Write(buf, binary.BigEndian, tag.Name)
|
|
}
|
|
binary.Write(buf, binary.BigEndian, tag.Value)
|
|
}
|
|
curoffs += 12 + tag.FullLength
|
|
}
|
|
/* write the last pseudo-tag of the section */
|
|
binary.Write(buf, binary.BigEndian, []byte("DIFR"))
|
|
skip = int(8184 - curoffs)
|
|
binary.Write(buf, binary.LittleEndian, uint32(skip - 4))
|
|
for i:=0; i<skip; i++ {
|
|
binary.Write(buf, binary.LittleEndian, uint8(0))
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
/* set a string tag value (null-terminated) */
|
|
func setStringTag(tag *DevInfoTag, val string) {
|
|
tag.Value = append([]byte(val), byte(0))
|
|
tag.FullLength = tag.NameLength + uint32(len(tag.Value))
|
|
}
|
|
|
|
/* find the tag index in a slice (by name), return -1 if not found */
|
|
func findTag(tags []DevInfoTag, name string) int {
|
|
bname := append([]byte(name), byte(0)) /* null-terminated tag name */
|
|
for idx, tag := range tags {
|
|
if bytes.Equal(tag.Name, bname) {
|
|
return idx /* found the tag index, return it */
|
|
}
|
|
}
|
|
return -1 /* no tag found with this name */
|
|
}
|
|
|
|
/* calculate Luhn checksum over 14 IMEI digits */
|
|
func calcLuhn(num string) int {
|
|
sum := 0
|
|
dd := 0
|
|
for i, d := range num {
|
|
if d >= 48 && d < 58 {
|
|
dd = int(d) - 48
|
|
if i%2 == 1 {
|
|
dd += dd
|
|
if dd > 9 {
|
|
dd -= 9
|
|
}
|
|
}
|
|
sum += dd
|
|
}
|
|
}
|
|
sum %= 10
|
|
if sum != 0 {
|
|
sum = 10 - sum
|
|
}
|
|
return sum
|
|
}
|
|
|
|
/* run a single AT command and attempt to read its result */
|
|
func runAtCmd(cmd string) string {
|
|
ifacefile := "/dev/umts_router"
|
|
rcmd := append([]byte(cmd), byte(13), byte(10)) /* add CRLF */
|
|
err := os.WriteFile(ifacefile, rcmd, 0666) /* write the request */
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
data, err := os.ReadFile(ifacefile) /* read the response */
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
/* entry point */
|
|
func main() {
|
|
params := os.Args[1:]
|
|
subcmd := params[0]
|
|
fname := "/dev/block/bootdevice/by-name/devinfo"
|
|
var info DevInfoImage
|
|
/* preread the data if the subcommand is devinfo-related */
|
|
if subcmd == "listps" || subcmd == "setpsval" || subcmd == "setpstype" {
|
|
data, err := os.ReadFile(fname)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
info = readDevInfo(data)
|
|
}
|
|
/* execute the subcommand */
|
|
switch subcmd {
|
|
case "listps":
|
|
for _, tag := range info.PStags {
|
|
fmt.Printf("%s\t%s\t%s\n", tag.Type, tag.Name, tag.Value)
|
|
}
|
|
case "setpsval":
|
|
valpairs := params[1:]
|
|
plen := len(valpairs)
|
|
if plen % 2 == 1 {
|
|
fmt.Println("invalid parameter count!")
|
|
return
|
|
}
|
|
for i := 0; i < plen; i+=2 {
|
|
tagidx := findTag(info.PStags, valpairs[i])
|
|
if tagidx > -1 {
|
|
setStringTag(&info.PStags[tagidx], valpairs[i+1])
|
|
}
|
|
}
|
|
os.WriteFile(fname, writeDevInfo(info), 0644)
|
|
fmt.Println("devinfo image saved")
|
|
case "setpstype":
|
|
if len(params) != 3 {
|
|
println("invalid parameter count!")
|
|
return
|
|
}
|
|
tagtype := params[2]
|
|
tagidx := findTag(info.PStags, params[1])
|
|
if tagidx > -1 {
|
|
if tagtype != "DIFR" { /* only two types are allowed, force DIUS unless it's DIFR */
|
|
tagtype = "DIUS"
|
|
}
|
|
copy(info.PStags[tagidx].Type[:], []byte(tagtype))
|
|
os.WriteFile(fname, writeDevInfo(info), 0644)
|
|
fmt.Println("devinfo image saved")
|
|
}
|
|
case "genimei":
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
var imeisb strings.Builder
|
|
imeisb.WriteString(params[1]) /* start with the prefix */
|
|
for i:=0; i < 14; i++ {
|
|
imeisb.WriteString(strconv.Itoa(rand.Intn(10)))
|
|
}
|
|
imei := imeisb.String()[:14] /* only take the first 14 digits of the result */
|
|
fmt.Printf("%s", imei + strconv.Itoa(calcLuhn(imei)))
|
|
case "atcmd":
|
|
if len(params) != 2 {
|
|
println("invalid parameter count!")
|
|
return
|
|
}
|
|
fmt.Printf("%s\n", runAtCmd(params[1]))
|
|
case "fiximeisha":
|
|
rawsharesult := runAtCmd("AT+GOOGGETIMEISHA")
|
|
sharesult := strings.Split(strings.ReplaceAll(rawsharesult, "\r\n", "\n"), "\n")
|
|
if len(sharesult) > 2 {
|
|
if sharesult[2] == "ERROR" {
|
|
fmt.Println("Error getting IMEI SHA, are you booted into the factory mode?")
|
|
} else {
|
|
shaparts := strings.Split(sharesult[2], ":")
|
|
if len(shaparts) > 1 {
|
|
targetfile := "/mnt/vendor/persist/modem/cpsha"
|
|
os.WriteFile(targetfile, []byte(shaparts[1]), 0644)
|
|
fmt.Println("CPSHA file written")
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
fmt.Println("Valid subcommands: listps, setpsval, setpstype, fiximeisha, genimei, atcmd")
|
|
}
|
|
}
|
|
|