Description
You have a device that is nearly completely bricked, but it seems to progress to a stage in boot where you can run your own code, and it conveniently has 8 LEDs. You write a small binary to read the bootlog and emit it, compressed, byte-by-byte on the LEDs. With a phone camera and a shaky hand, you record the LEDs.
Decode the video.
Resolution
This was a very fun challenge, but harder than we could think.
First, lets get all the pictures of the video.
pretty standard extraction with ffmpeg:
ffmpeg -i blink.mp4 raw%d.png
we get more than 3000 pics like this one :

We will have to do two things :
extract the 8 bits values from the picture (a yellow led is a 1 bit)extract those bytes on the right timing to create the final file
Part 1 – Getting the right bits:
the video is shaking, slowly moving from right to left, up to down … so we can’t just take one pixel for each bit
we can do this by taking the values of a whole part of the picture, and summing all the pixels that are ‘yellow’.
if this goes up to a specific treshold, we’ll consider this to be a yellow/1 bit
y_min=20
y_max=250
for y in range(y_min,y_max):
for x in range(image.size[0]):
value= list(pixdata[x,y])
if value[0]>254 and value[1]>230: #good pixel
compteur_j[4*((y-y_min)/((y_max-y_min)/2))+x/(image.size[0]/4)]+=1
you think that is good enough ? well …
what byte is this ?
that’s a tricky question, cause there is actually no way to know for sure if you just take that pictures.
Thoses red led come from a led either lighting up or lighing off.
so we have to analyse this picture by picture, and take the last picture into account when we analyse the current one.
we have two possibilities :
- if the led was yellow, a red one is a led turning off
- if the led was off, a red led mean the led is turning on
y_min=20
y_max=250
for y in range(y_min,y_max):
for x in range(image.size[0]):
value= list(pixdata[x,y])
if value[0]>254 and value[1]>230: #good pixel
compteur_y[4*((y-y_min)/((y_max-y_min)/2))+x/(image.size[0]/4)]+=1
elif value[0]>254 : #red pixel
compteur_r[4*((y-y_min)/((y_max-y_min)/2))+x/(image.size[0]/4)]+=1
for index,(bit_y,bit_r,old_y,old_r) in enumerate(zip(compteur_y,compteur_r,previous_y,previous_r)):
if bit_y > treshold_y[index]:
final='1'+final
final_compt.append(1)
else :
if got_first==False:
if old_y==0 and bit_r>treshold_r[index]:
final='1'+final
final_compt.append(1)
else :
final = '0'+final
final_compt.append(0)
else :
if old_y==1 and bit_r>treshold_r[index]:
final='1'+final
final_compt.append(1)
else :
final = '0'+final
final_compt.append(0)
but it also mean something else, it means that we are not in a final and “stable” state. and that is the crucial point for step2.
Step 2 : knowing when to extract the byte
that was the real difficulty of this problem, the timing was not really stable.
at first glance we could think that we have to extract one picture every two, but actually we sometimes have 3 pictures for the same Byte, and sometimes only one !
when also have to deal we the fact that sometimes, we can have 2 following bytes that will have the same value.
the idea was to use those red leds, as we know that they mean a led coming up or down, so a new byte showing up or a byte wearing off.
if we have a yellow led that turn red, we know the byte has been shown and we take it into account.
of course if a yellow led turned dark, it means the same :p
if a dark led turn red, we can already guess what the final byte should become
that’s mostly it.
you can have the full python script here.
#!/usr/bin/env python
#coding:utf-8
import binascii,PIL,zlib,struct
from PIL import Image,ImageTk
import shutil,os
def getdata(filepath,previous_j,previous_r,got_first):
datas=''
if filepath!='':
image=Image.open(filepath)
pixdata=image.load()
compteur_j=[0,0,0,0,0,0,0,0]
compteur_r=[0,0,0,0,0,0,0,0]
treshold_j=[200,200,200,200,50,200,200,50]
treshold_r=[20,20,20,20,20,20,20,20]
y_min=20
y_max=250
for y in range(y_min,y_max):
for x in range(image.size[0]):
value= list(pixdata[x,y])
if value[0]>254 and value[1]>230: #good pixel
compteur_j[4*((y-y_min)/((y_max-y_min)/2))+x/(image.size[0]/4)]+=1
elif value[0]>254 : #red pixel
compteur_r[4*((y-y_min)/((y_max-y_min)/2))+x/(image.size[0]/4)]+=1
final=''
final_compt=[]
for index,(bit_j,bit_r,old_j,old_r) in enumerate(zip(compteur_j,compteur_r,previous_j,previous_r)):
if bit_j > treshold_j[index]:
final='1'+final
final_compt.append(1)
else :
if got_first==False:
if old_j==0 and bit_r>treshold_r[index]:
final='1'+final
final_compt.append(1)
else :
final = '0'+final
final_compt.append(0)
else :
if old_j==1 and bit_r>treshold_r[index]:
final='1'+final
final_compt.append(1)
else :
final = '0'+final
final_compt.append(0)
final=chr(int(final,2))
return final,final_compt,compteur_r,True,datas
def resolv():
result=''
max=3537
compteur_j=[0,0,0,0,0,0,0,0]
compteur_r=[0,0,0,0,0,0,0,0]
last_char=''
last_count=0
last_skipped=0
logs=''
got_first=False
final=''
for i in range(14,max+1):
file='output%i%s.png'%(i,x)
if os.path.isfile(file):
old_compteur_j=compteur_j
old_compteur_r=compteur_r
old_logs=logs
res,compteur_j,compteur_r,is_red,logs=getdata(file,compteur_j,compteur_r,got_first)
if got_first==False:
#1° or 3° picture of a byte to extract
if res!=last_char:
#sure ? let's check again with got_first=True ...
res2,compteur_j2,compteur_r2,is_red2,logs2=getdata(file,old_compteur_j,old_compteur_r,True)
if res2==last_char:
file='output%i%s.png'%(i+1,x)
if os.path.isfile(file):
res3,compteur_j3,compteur_r3,is_red3,logs3=getdata(file,compteur_j,compteur_r,True)
if res3==res2:
flag3=True
got_first=True
print "4 in a row %i"%i
#got 4th
else:
#3° for one byte, skipping it
continue
else : #this is indeed the first pic for this byte
last_char=res
got_first=True
else : #if res==last_char:
file='output%i%s.png'%(i+1,x)
if os.path.isfile(file):
res2,compteur_j2,compteur_r2,is_red2,logs2=getdata(file,compteur_j,compteur_r,True)
if res2==res:
flag3=True#got 4th
got_first=True
print "4 in a row %i"%i
else:
#3° for one byte, skipping it
continue
elif got_first==True:
#getting 2° or 4th°
if res==last_char:#2° or 4°
got_first=False
print i," - Ok ",bin(ord(res))," ",res
final+=res
flag3=False
continue
else :#NOP, it s a new char,
#we register the previous one if the next one is the same as the actual one
#dans ce cas, l'actuel est un premier
old_logs=logs
res,compteur_j,compteur_r,is_red,logs=getdata(file,old_compteur_j,old_compteur_r,False)
file='output%i%s.png'%(i+1,x)
if os.path.isfile(file):
res3,compteur_j3,compteur_r3,is_red3,logs3=getdata(file,compteur_j,compteur_r,True)
if res3==res:#Single pic for the byte
final+=last_char
else :
print old_logs
print logs
print "No match at %s : %s - %s\n"%(file,bin(ord(res)),bin(ord(last_char)))
last_char=res
got_first=True
g=open('a.xz','wb')
g.write(final)
g.close()
print final
we end up with a raw file starting with the header of a XZ compressed file.
uncompressing it gives us the bootlogs :
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Initializing cgroup subsys cpuset
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
[ 0.000000] Linux version 3.14.14-cubox (root@kitchen) (gcc version 4.8.2 (Ubuntu/Linaro 4.8.2-16ubuntu4) ) #1 SMP Wed Sep 16 22:37:47 CEST 2015
[ 0.000000] CPU: ARMv7 Processor [412fc09a] revision 10 (ARMv7), cr=10c53c7d
[ 0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache
[ 0.000000] Machine model: SolidRun HummingBoard Dual/Quad
[ 0.000000] cma: CMA: reserved 256 MiB at 40000000
[ 0.000000] Memory policy: Data cache writealloc
[ 0.000000] On node 0 totalpages: 262144
[ 0.000000] free_area_init_node: node 0, pgdat 80a68280, node_mem_map af779000
[ 0.000000] DMA zone: 2048 pages used for memmap
[ 0.000000] DMA zone: 0 pages reserved
[ 0.000000] DMA zone: 262144 pages, LIFO batch:31
[ 0.000000] PERCPU: Embedded 8 pages/cpu @af758000 s9664 r8192 d14912 u32768
[ 0.000000] pcpu-alloc: s9664 r8192 d14912 u32768 alloc=8*4096
[ 0.000000] pcpu-alloc: [0] 0 [0] 1
[ 0.000000] Built 1 zonelists in Zone order, mobility grouping on. Total pages: 260096
[ 0.000000] Kernel command line: root=/dev/mmcblk0p1 rootfstype=ext4 rootwait console=tty1 video=mxcfb0:dev=hdmi,1920x1080M@60,if=RGB24,bpp=32 rd.dm=0 rd.luks=0 rd.lvm=0 raid=noautodetect pci=nomsi ahci_imx.hotplug=1 consoleblank=0 vt.global_cursor_default=0 quiet
[ 0.000000] PID hash table entries: 4096 (order: 2, 16384 bytes)
[ 0.000000] Dentry cache hash table entries: 131072 (order: 7, 524288 bytes)
[ 0.000000] Inode-cache hash table entries: 65536 (order: 6, 262144 bytes)
[ 0.000000] allocated 2097152 bytes of page_cgroup
[ 0.000000] please try ‘cgroup_disable=memory’ option if you don’t want memory cgroups
[ 0.000000] your flag is iw4u5t098guetiujy0j
[ 0.000000] Memory: 763540K/1048576K available (7306K kernel code, 352K rwdata, 2632K rodata, 337K init, 518K bss, 285036K reserved, 0K highmem)
[ 0.000000] Virtual kernel memory layout:
[ 0.000000] vector : 0xffff0000 – 0xffff1000 ( 4 kB)
[ 0.000000] fixmap : 0xfff00000 – 0xfffe0000 ( 896 kB)
[ 0.000000] vmalloc : 0xc0800000 – 0xff000000 (1000 MB)
[ 0.000000] lowmem : 0x80000000 – 0xc0000000 (1024 MB)
[ 0.000000] pkmap : 0x7fe00000 – 0x80000000 ( 2 MB)
[ 0.000000] modules : 0x7f000000 – 0x7fe00000 ( 14 MB)
[ 0.000000] .text : 0x80008000 – 0x809bcd9c (9940 kB)
[ 0.000000] .init : 0x809bd000 – 0x80a115c0 ( 338 kB)
[ 0.000000] .data : 0x80a12000 – 0x80a6a0e0 ( 353 kB)
[ 0.000000] .bss : 0x80a6a0e8 – 0x80aebaa0 ( 519 kB)
[ 0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=2, Nodes=1
[ 0.000000] Hierarchical RCU implementation.
[ 0.000000] RCU restricting CPUs from NR_CPUS=4 to nr_cpu_ids=2.
[ 0.000000] RCU: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=2
[ 0.000000] NR_IRQS:16 nr_irqs:16 16
[ 0.000000] L2C-310 erratum 769419 enabled
[ 0.000000] L2C-310 enabling early BRESP for Cortex-A9
[ 0.000000] L2C-310 full line of zeros enabled for Cortex-A9
[ 0.000000] L2C-310 ID prefetch enabled, offset 4 lines
[ 0.000000] L2C-310 dynamic clock gating enabled, standby mode enabled
[ 0.000000] L2C-310 cache controller enabled, 16 ways, 1024 kB
[ 0.000000] L2C-310: CACHE_ID 0x410000c7, AUX_CTRL 0x76070001
[ 0.000010] sched_clock: 32 bits at 3000kHz, resolution 333ns, wraps every 1431655765682ns
[ 0.000573] Console: colour dummy device 80×30
[ 0.000705] console [tty1] enabled
your flag is iw4u5t098guetiujy0j
[sCTF 2016] [CODE 100 – Deblink] Write Up