INT 16h AH=02h returns one byte, the shift flags the keyboard handler keeps at 0040:0017h. The low four bits say which modifier keys are held down right now; the high four say which toggles are on. It is the only BIOS way to see Shift, Ctrl or Alt on their own, since pressing them puts nothing in the buffer.

BitMeaning when 1
0right Shift held
1left Shift held
2Ctrl held (either)
3Alt held (either)
4Scroll Lock on
5Num Lock on
6Caps Lock on
7Insert on (toggled by the Ins key)

The function only copies that byte, so the byte itself and the function always agree. The example tests the modifier bits, then flips the Insert bit in the BIOS data area by hand, reads the flags again, puts the bit back, and prints which bits changed. Insert has no LED, so the experiment is invisible to the keyboard.

prints: no Shift, Ctrl or Alt held / changed bits: 80 (two lines; the first says a key is down if you hold Shift, Ctrl or Alt while it runs)
MOV AH, 02h ; BIOS: get the shift flags
INT 16h ; AL = the flag byte
MOV BL, AL ; keep it: printing changes AL
MOV DX, OFFSET FREE1
TEST BL, 0Fh ; bits 0-3: Shift, Ctrl, Alt held?
JZ SHOW1
MOV DX, OFFSET HELD1
SHOW1:
MOV AH, 09h
INT 21h
MOV AX, 40h
MOV ES, AX ; ES = the BIOS data area
XOR BYTE PTR ES:[17h], 80h ; flip the Insert flag by hand
MOV AH, 02h
INT 16h ; AL = the flags as they are now
XOR BYTE PTR ES:[17h], 80h ; and flip it back
XOR AL, BL ; 1 bits: what changed
MOV BL, AL
MOV DX, OFFSET DIFF1
MOV AH, 09h ; DOS: print the $-string at DS:DX
INT 21h
MOV AL, BL
CALL PHEX2 ; 80: only bit 7
FREE1 DB "no Shift, Ctrl or Alt held", 13, 10, "$"
HELD1 DB "a Shift, Ctrl or Alt key is down", 13, 10, "$"
DIFF1 DB "changed bits: $"
PHEX2 PROC NEAR ; print AL as two hex digits; keeps every register
PUSH AX
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high nibble
CALL HEXN1
MOV AL, DH
AND AL, 0Fh ; the low nibble
CALL HEXN1
POP DX
POP CX
POP AX
RET
HEXN1:
ADD AL, "0"
CMP AL, "9"
JBE HEXN2
ADD AL, 7 ; past "9": "A".."F"
HEXN2:
MOV DL, AL
MOV AH, 02h
INT 21h
RET
PHEX2 ENDP

More detail

The byte does not say which Ctrl or Alt is held. Enhanced keyboards answer that through INT 16h AH=12h: AL is the same byte, AH adds left Ctrl (bit 0), left Alt (bit 1), right Ctrl (bit 2), right Alt (bit 3) and SysRq (bit 7), taken from 0040:0018h and 0040:0096h. Remember the toggles are states, not keys: bit 6 says Caps Lock is on, not that the key is being pressed.

Pitfalls

Writing to 0040:0017h changes what the BIOS believes. Setting the Caps Lock bit really switches Caps Lock on: the next letters come out in capitals, and many BIOSes (DOSBox-X's among them) notice the change on the next timer tick and update the keyboard LEDs. Programs used that to force Num Lock on at startup. If you change the byte, change it back.