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