zed.0xff.me
hack.lu 2011 CTF -- Unknown Planet -- writeup
[ Lobotomy ]
0. original image file
1. analyzing image
all JPEGs have special EOF marker FF D9
and no data must be after this marker.
1 2 3 4 5 |
[zed@zmac 200.unknown.planet+]#irb -E binary ruby-1.9.2-p290 :004 > data=File.read '0_8c4f14e28155a2c3cf4b2538c1e0958b.jpg'; data.size => 194420 ruby-1.9.2-p290 :005 > data.split("\xff\xd9").map(&:size) => [192405, 2013] |
so, we can see that there’s 2013 spare bytes after EOF marker.
1 2 3 4 5 6 |
File.open('foo','w'){ |f| f<< data.split("\xff\xd9").last } => #<File:foo (closed)> ruby-1.9.2-p290 :007 > ^D [zed@zmac 200.unknown.planet+]#file foo foo: Zip archive data, at least v2.0 to extract |
AHA! It’s a zip! :)
2. unzipping
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
[zed@zmac 200.unknown.planet+]#unzip foo Archive: foo inflating: 5IIUED7GheR inflating: 6JXtwsTTh9k inflating: 87F1s5POUJc inflating: BPiIOASG_Z6 inflating: nLPA8X0UJqf inflating: rySOWi4fZkA inflating: uvlSlG3Tgow inflating: Uw105aD3qYh inflating: Yui5oq58hlx [zed@zmac 200.unknown.planet+]#ls -la -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 5IIUED7GheR -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 6JXtwsTTh9k -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 87F1s5POUJc -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 BPiIOASG_Z6 -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 Uw105aD3qYh -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 Yui5oq58hlx -rw-r--r--@ 1 zed staff 1324 Apr 25 16:45 nLPA8X0UJqf -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 rySOWi4fZkA -rw-r--r--@ 1 zed staff 20000 Apr 25 16:45 uvlSlG3Tgow [zed@zmac 200.unknown.planet+]#file * 5IIUED7GheR: data 6JXtwsTTh9k: data 87F1s5POUJc: 8086 relocatable (Microsoft) BPiIOASG_Z6: data Uw105aD3qYh: data Yui5oq58hlx: data nLPA8X0UJqf: 8086 relocatable (Microsoft) rySOWi4fZkA: data uvlSlG3Tgow: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 8 bit, mono 8000 Hz |
Looks like audio file, that was split in chunks of 20000 bytes each.
uvlSlG3Tgow
is a first chunk b/c it has a RIFF WAVE header.
nLPA8X0UJqf
is a last tail chunk b/c it’s size less than 20000.
3. gluing waves
importing files in Audacity (or any other sound editor) discovers that source file is supposed to be a Morse – coded message. But we must find a correct order of chunks.
So, morse code consists of dots
and dashes
. Each kind must have fixed length.
We suppose that source file was generated programmatically, not recorder from line or mic. So, it’s timings must be perfect.
Following tool helps to manually find a correct chunks order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#!/usr/bin/env ruby STDOUT.sync = true if ARGV.size == 0 raise "gimme at least one chunk filename" end b0 = "\x80"*8 b1 = "\x27\x01\x27\x80\xd9\xff\xd9\x80" data = ARGV.map{ |x| File.read(x) }.join.force_encoding('binary') if data[0,4] == 'RIFF' data = data[44..-1] end N=120 b0 = b0*N b1 = b1*N r = '' 0.step(data.size-1,b0.size) do |i| case (d=data[i,b0.size]) when b0 print "." r << '0' when b1 print "#" r << '1' else raise "SYNC ERROR" if d.size == b0.size raise "NOT ENOUGH DATA #{d.size}/#{b0.size}" raise "unknown #{d.size} (normal: #{b0.size}) bytes of data #{d.split('').map{|x| "%02x " % x.ord}.join}" end end |
calling with a single chunk – script says that it needs more data (more chunks):
1 2 |
[zed@zmac 1]#./2_manually_guess_chunk_order.rb uvlSlG3Tgow ##..######..######.../2_manually_guess_chunk_order.rb:32:in `block in <main>': NOT ENOUGH DATA 756/960 |
calling with wrong 2nd chunk => SYNC ERROR
:
1 2 |
[zed@zmac 1]#./2_manually_guess_chunk_order.rb uvlSlG3Tgow 6JXtwsTTh9k ##..######..######..###./2_manually_guess_chunk_order.rb:31:in `block in <main>': SYNC ERROR |
two chunks in correct order, script says it needs more chunks:
1 2 |
[zed@zmac 1]#./2_manually_guess_chunk_order.rb uvlSlG3Tgow 5IIUED7GheR ##..######..######..##......##..##..##..#./2_manually_guess_chunk_order.rb:32:in `block in <main>': NOT ENOUGH DATA 596/960 |
all chunks in correct order:
1 2 |
[zed@zmac 1]#./2_manually_guess_chunk_order.rb uvlSlG3Tgow 5IIUED7GheR rySOWi4fZkA 87F1s5POUJc 6JXtwsTTh9k Uw105aD3qYh BPiIOASG_Z6 Yui5oq58hlx nLPA8X0UJqf ##..######..######..##......##..##..##..##......##......##..##......######..##..######......######..##..######..######......######..######..######......##..##..##...... |
4. decoding Morse
we’ll need a ruby morse gem. install it with “gem install morse
”
1 2 3 4 5 6 |
[zed@zmac 200.unknown.planet+]#irb ruby-1.9.2-p290 :001 > r='##..######..######..##......##..##..##..##......##......##..##......######..##..######......######..##..######..######......######..######..######......##..##..##......' ruby-1.9.2-p290 :002 > require 'morse' => true ruby-1.9.2-p290 :005 > puts Morse.decode(r.gsub('......'," ").gsub('######','-').gsub('.','').gsub('##','.')) PHEIKYOS |
Voila! “Pheikyos” is the answer. Case-sensitive.
PS: all source & data files are available at my ctf github repo.