[sCTF 2016] [CODE 100 – Deblink] Write Up

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 :
output15

 

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 ?

output172

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

Leave a Reply

Your email address will not be published. Required fields are marked *