Skip to content
All writing
4 min readElectronicsSoftware DevelopmentITAD

The USB stick that stole a ThinkPad's identity

A hardware audit tool reported a Lenovo laptop as a SanDisk flash drive. The cause was one convenient shell idiom, and it had silently overwritten the machine's identity three functions earlier.

The first real field run of my hardware audit tool produced a device record for a Lenovo ThinkPad T440. Almost everything on it was right: an i5-4200U, two cores and four threads, 8GB of DDR3 at 1600MHz, a 240GB drive, battery health at 68 percent, UEFI boot. Correct down to the detail.

Except the machine's name. The tool had recorded it as a LENOVO SanDisk 3.2Gen1, with a 128-character hexadecimal serial number.

That is not a laptop. That is the USB stick the tool had booted from.

What the tool does

The tool boots a device from a SystemRescue USB, reads its hardware directly from firmware and the kernel, and posts a complete profile to an inventory API. It exists because devices arriving for IT asset disposition are usually already wiped — there is no operating system to run an inventory agent on, so the specifications have to be read from outside.

Machine identity comes from DMI, early in the run:

MANUFACTURER=$(dmidecode -s system-manufacturer)
MODEL=$(dmidecode -s system-product-name)
SERIAL=$(dmidecode -s system-serial-number)

Straightforward. And at that point in the run, correct — I confirmed it later by adding a debug print.

The convenient idiom

Storage comes later. lsblk has a pairs output mode that looks purpose-built for shell consumption:

$ lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN
NAME="sda" MODEL="SanDisk 3.2Gen1" SERIAL="0401d2ea..." SIZE="28.9G" TRAN="usb"

Each line is already valid shell assignment syntax. The obvious thing to do — the thing I did — is to let the shell do the parsing:

lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN | while read -r line; do
  eval "$line"
  # $NAME, $MODEL, $SERIAL are now set. Very tidy.
done

eval on that line defines shell variables named NAME, MODEL, SERIAL, SIZE and TRAN.

Which are, of course, the same variables I had used for the machine's identity.

MODEL and SERIAL were quietly reassigned from the laptop's DMI values to whichever drive the loop touched last. Nothing errored. Nothing warned. The variables were still perfectly valid strings — they just now described a different piece of hardware.

Why it looked plausible

This is what made it survive to the field instead of dying in testing. The output was not garbage. It was a well-formed device record for a real device that was genuinely plugged into the machine. MANUFACTURER was untouched, so the record read "LENOVO SanDisk 3.2Gen1" — a Lenovo-branded something. At a glance that is a slightly odd product name, not a category error.

A crash would have been better. A crash is a question. This was an answer, and it was wrong.

The fix

Three changes, each doing separate work.

Stop letting external output define variables. Parse into a namespace the rest of the script does not use:

while IFS= read -r line; do
  D_NAME=$(printf '%s' "$line" | sed -n 's/.*NAME="\([^"]*\)".*/\1/p')
  D_MODEL=$(printf '%s' "$line" | sed -n 's/.*MODEL="\([^"]*\)".*/\1/p')
  # ...
done <<< "$(lsblk -P -o NAME,MODEL,SERIAL,SIZE,TRAN)"

More verbose. It cannot reach outside its own prefix.

Audit internal drives only. The boot stick was never supposed to be in the inventory at all. Excluding removable media by transport, by the removable flag, and by sysfs means the USB the tool runs from is structurally incapable of appearing in a device record:

[ "$D_TRAN" = "usb" ] && continue
[ "$D_RM" = "1" ] && continue
[ "$(cat /sys/block/$D_NAME/removable 2>/dev/null)" = "1" ] && continue

This mattered far beyond the naming bug. The same tool can perform a secure erase. A drive-selection routine that could see the boot medium is a drive-selection routine that could one day erase it.

Read the model from the right DMI field. A separate wrinkle the first run exposed: Lenovo puts the marketing name in system-version, not system-product-name, which holds a type code like 20B6005TUK. Dell and HP use system-product-name as you would expect. So the friendly name is vendor-conditional.

What I took from it

eval is usually flagged for its injection risk, and that framing is why I did not think twice here — lsblk output is not attacker-controlled in any meaningful sense on a machine I have physically in front of me. The security argument did not apply, so I skipped past it.

But the injection risk was never the only problem. eval hands an external program the ability to write into my namespace. What broke was not security. It was scope.

The wider lesson is about how the failure presented. Every serious bug I have found in this system has been silent: offline writes that vanished on sync, a dangling foreign key that wedged an upload queue, and now an identity quietly overwritten by a subroutine. None of them raised anything. They all produced confident, well-formed, wrong output.

Which is why the tool now prints its full captured profile before uploading, and why one junk asset named "LENOVO SanDisk 3.2Gen1" is still in my production database. I have not deleted it. It is a useful reminder that the plausible answer is the dangerous one.

bashlinuxdebugginghardware-auditlessons