Skip to content

Instantly share code, notes, and snippets.

@BohdanTkachenko
Last active August 23, 2025 00:43
Show Gist options
  • Save BohdanTkachenko/3f852c352cb2e02cdcbb47419e2fcc74 to your computer and use it in GitHub Desktop.
Save BohdanTkachenko/3f852c352cb2e02cdcbb47419e2fcc74 to your computer and use it in GitHub Desktop.
Lenovo-shipped XMM7560 (8086:7560) FCC unlock

Lenovo-shipped XMM7560 (8086:7560) FCC unlock

This script allows ModemManager to properly perform Lenovo-shipped XMM7560 (8086:7560) FCC unlock.

It is based on merge request by Stoica Floris, but adds systemd logging and includes a suggested XDNS command which makes the modem actually usable. Without this command unlock works, but the connection is unstable.

Installation

Just place this script into /etc/ModemManager/fcc-unlock.d and make sure it is executable. You can do this with the following command:

sudo mkdir -p /etc/ModemManager/fcc-unlock.d
sudo curl \
  -o /etc/ModemManager/fcc-unlock.d/8086:7560 \
  -L https://gist.githubusercontent.com/BohdanTkachenko/3f852c352cb2e02cdcbb47419e2fcc74/raw/d646f82579af5f2ec64e4edb872a9510251106c2/fcc_unlock_8086:7560.sh
sudo chmod +x /etc/ModemManager/fcc-unlock.d/8086:7560
#!/bin/bash
# SPDX-License-Identifier: CC0-1.0
# 2024 Stoica Floris <[email protected]>
# 2024 Modified by Bohdan Tkachenko <[email protected]>
#
# Lenovo-shipped XMM7560 (8086:7560) FCC unlock
# require program name and at least 2 arguments
[ $# -lt 2 ] && exit 1
# first argument is DBus path, not needed here
shift
# second and next arguments are control port names
for PORT in "$@"; do
# match port type in Linux 5.14 and newer
grep -q AT "/sys/class/wwan/$PORT/type" 2>/dev/null && {
AT_PORT=$PORT
break
}
# match port name in Linux 5.13
echo "$PORT" | grep -q AT && {
AT_PORT=$PORT
break
}
done
# fail if no AT port exposed
[ -n "$AT_PORT" ] || exit 2
DEVICE=/dev/${AT_PORT}
at_command() {
exec 99<>"$DEVICE"
echo -e "$1\r" >&99
read answer <&99
read answer <&99
echo "$answer"
exec 99>&-
}
log() {
logger -t ModemManager -p info "<info> $1"
}
error() {
logger -t ModemManager -p error "<error> $1"
}
reverseWithLittleEndian() {
num="$1"
printf "%x" $(("0x${num:6:2}${num:4:2}${num:2:2}${num:0:2}"))
}
VENDOR_ID_HASH="bb23be7f"
for i in {1..9}; do
log "Requesting challenge from WWAN modem (attempt #${i})"
RAW_CHALLENGE=$(at_command "at+gtfcclockgen")
CHALLENGE=$(echo "$RAW_CHALLENGE" | grep -o '0x[0-9a-fA-F]\+' | awk '{print $1}')
if [ -n "$CHALLENGE" ]; then
log "Got challenge from modem: $CHALLENGE"
HEX_CHALLENGE=$(printf "%08x" "$CHALLENGE")
REVERSE_HEX_CHALLENGE=$(reverseWithLittleEndian "${HEX_CHALLENGE}")
COMBINED_CHALLENGE="${REVERSE_HEX_CHALLENGE}${VENDOR_ID_HASH}"
RESPONSE_HASH=$(printf "%s" "$COMBINED_CHALLENGE" | xxd -r -p | sha256sum | cut -d ' ' -f 1)
TRUNCATED_RESPONSE=$(printf "%.8s" "${RESPONSE_HASH}")
REVERSED_RESPONSE=$(reverseWithLittleEndian "$TRUNCATED_RESPONSE")
RESPONSE=$(printf "%d" "0x${REVERSED_RESPONSE}")
log "Sending hash modem: $RESPONSE"
UNLOCK_RESPONSE=$(at_command "at+gtfcclockver=$RESPONSE")
log "at+gtfcclockver response = $UNLOCK_RESPONSE"
UNLOCK_RESPONSE=$(at_command "at+gtfcclockmodeunlock")
log "at+gtfcclockmodeunlock response = $UNLOCK_RESPONSE"
UNLOCK_RESPONSE=$(at_command "at+cfun=1")
log "at+cfun response = $UNLOCK_RESPONSE"
UNLOCK_RESPONSE=$(at_command "at+gtfcclockstate")
log "at+gtfcclockstate response = $UNLOCK_RESPONSE"
UNLOCK_RESPONSE=$(echo "$UNLOCK_RESPONSE" | tr -d '\r')
if [ "$UNLOCK_RESPONSE" = "1" ] || [ "$UNLOCK_RESPONSE" = "OK" ]; then
XDNS_RESPONSE=$(at_command "at+xdns=0,1")
log "at+xdns response: ${XDNS_RESPONSE}"
fi
if [ "$UNLOCK_RESPONSE" = "1" ]; then
log "FCC was unlocked previously"
exit 0
fi
if [ "$UNLOCK_RESPONSE" = "OK" ]; then
log "FCC unlock success"
exit 0
else
error "Unlock failed. Got response: $UNLOCK_RESPONSE"
fi
else
error "Failed to obtain FCC challenge. Got: ${RAW_CHALLENGE}"
fi
sleep 0.5
done
exit 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment