Ryan Doenges

Chromatophoria

A few weeks ago I participated in the inagural PicoCTF, a high school hacking competition run by Carnegie Mellon’s Plaid Parliament of Pwning. I ended up placing a disappointing 30th–I spent way too much time that week playing trumpet in pit orchestra for the school musical.

At the close of the competition the organizers asked for interested competitors to do writeups of some problems, so here’s one from me.

Chromatophoria was my favorite forensics problem. It presents you with a PNG of a magnifying glass and asks you to determine the key with a generous hint:

Their transmission is entirely visual; you suspect that they may be communicating through the color values.

So, I knew I was looking for color alterations in the (mostly white) image. I opened it in GIMP and used the bucket tool to shade identical white pixels black. Zooming in, I looked for any remaining white pixels, and sure enough:

So, the first row of the image was full of slightly altered color values. Since I recently spent a lot of time working with PNG images in ruby with ChunkyPNG to build glitchit (blog post forthcoming), I decided to inspect the image with ChunkyPNG in IRB.

irb(main):001:0> require 'chunky_png'
=> true
irb(main):002:0> img = ChunkyPNG::Image.from_file('./steg.png')
=> <huge string representation>

I only need the first row, so I pulled it out into an array:

irb(main):003:0> arr = []
=> []
irb(main):004:0> (0...img.width).each { |i| arr << img[i, 0] }
=> 0...800
irb(main):005:0> arr
=> [4278189823, 4278189823, 4278124287, 4294967039, 4278189823, 4294901759,
4294967295, 4278124543, 4278124543, 4278124287, 4278124287, 4294901503,
<snip>
4278124287, 4278124287, 4278124287, 4278124287, 4278124287, 4278124287,
4278124287, 4278124287, 4278124287, 4278124287, 4278124287, 4278124287,
4278124287, 4278124287]
irb(main):006:0> arr.map { |x| x.to_s(16) }
=> ["fefffeff", "fefffeff", "fefefeff", "fffffeff", "fefffeff", "fffeffff",
<snip>
"fefefeff", "fefefeff", "fefefeff", "fefefeff", "fefefeff", "fefefeff"]

At this point I realized that all the bytes in the row were either 0xFF or 0xFE, so I split the whole array into bytes and did the first thing that came to mind–changed 0xFF to a 1 bit and 0xFE to a 0 bit and joined them into an ASCII string. That didn’t actually work–the transparency channel in the image was only 0xFF bytes and messed up the text. After dropping the transparency byte, I got the flag out. It was an entertaining exercise in ruby string manipulation and reminded me of building up a long shell pipeline in zsh.

Here’s what I ended up with, though I’m pretty sure my solution during the competition was slightly different. I’ll be taking better notes next time around.

irb(main):007:0> [arr.map { |x| x.to_s(16).scan(/../).slice(0,3) }.flatten.map { |x| x == "fe" ? 0 : 1 }.join('')].pack("B*")
=> "Hey I think we can write safely in this file without anyone seeing it. Anyway, the secret key is: XXXXXXXXXXXXXXX\0\0<more null bytes>"

So, I took a list of color integers, turned them into hex strings, split those into bytes, took the first three of those bytes in an array, flattened the arrays, changed the bytes to a 1 or 0 if they were 0xFF or 0xFE, joined those 1s and 0s into a string, wrapped the whole thing in an array, and used #pack to turn that string of binary digits into a string.

Pipelines! They’re so fun. I approached most of the problems in this same way–trying to slowly build up a pipeline before spending time on a single-purpose script. Sometimes the pipleine was shell, sometimes it was ruby. Either way, I felt thoroughly UNIXy!

emailgithubtwitterpublic keyrss