Add build tool for King of Spades

This commit is contained in:
wchen-r7 2017-07-28 00:42:30 -05:00
parent 27127d2a37
commit 2aef26c959
4 changed files with 250 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,249 @@
#!/usr/bin/env ruby
# This tool is built to create the King of Spades flag, which is placed in a motd file on IRC.
#
# Because IRC has a max size limit of roughly 200kb for MOTD, we need to do some
# modifications to the images, such as resizing, changing the quality, and optimizing to
# keep them under that limit.
# To use image_optimizer, first you need to do this:
# $ brew install optipng jpegoptim gifsicle pngquant
require 'image_optimizer'
require 'fileutils'
require 'zip'
require 'base64'
require 'digest'
require 'mini_magick'
require 'chunky_png'
include ChunkyPNG::Color
FLAGNAME = 'flag.png' # The flag name, which should be used in the CTF
B64NAME = 'flag_base64.txt' # The file name of the Base64 string
CARDNAME = 'king_of_spades.png' # The name of the card is the secret ZIP.
WORKFOLDER = 'temp' # A temp folder for processing the images
RESOURCES = ['fake.png', 'king_of_spades.png'] # The source images we want to use
# There is some trial-and-errors involved to determine these two numbers.
# Changing the quality tends to have a bigger size change.
JPGQUALITY = '10'
REIZEPERT = 0.18
# Resizes the image by reducing a specific percentage.
# The percentage to reduce is set by the RESIZEPERT constant.
def resize_image(img_path)
img = MiniMagick::Image.open(img_path)
print_status("Original #{img_path} dimension = #{img.height}x#{img.width}")
new_width = img.width - (img.width * REIZEPERT).to_i
new_height = img.height - (img.height * REIZEPERT).to_i
img = img.resize("#{new_width}x#{new_height}")
print_status("Resized #{img_path} dimension = #{img.height}x#{img.width}")
img.write(img_path)
end
# Converts a PNG to JPG.
# The purpose of this is really to lower the quality of the image, which will dramatically
# reduce the size of the image.
def convert_png_to_jpg(img_path)
print_status("Converting #{img_path} to JPG to reduce image quality to #{JPGQUALITY}")
basename = File.basename(img_path, '.png')
img = MiniMagick::Image.open(img_path)
img.format('JPEG')
img.quality(JPGQUALITY)
dst = "#{WORKFOLDER}/#{basename}.jpg"
img.write(dst)
dst
end
# Converts a JPG back to PNG.
def convert_jpg_to_png(img_path)
print_status("Converting #{img_path} back to PNG")
basename = File.basename(img_path, '.jpg')
img = MiniMagick::Image.open(img_path)
img.format('PNG')
dst = "#{WORKFOLDER}/#{basename}.png"
img.write(dst)
dst
end
# Collects the transparent pixel indexes from PNG.
# The purpose of this is because after converting an image to JPG, we lose transparency.
# So before converting to JPG, we need to keep track of where the transparent pixels are,
# and then when we can finally convert these JPGs back to PNGs again, we will need to
# restore these pixels.
def map_transparent_pixels(img_path)
print_status("Mapping transparent pixels for #{img_path}")
img = ChunkyPNG::Image.from_file(img_path)
found_indexes = []
index = 0
img.pixels.each do |p|
found_indexes << index if ChunkyPNG::Color.fully_transparent?(p)
index += 1
end
found_indexes
end
# Optimizes a PNG file.
# There is some voodoo invovled in this optimizer to make a PNG smaller without
# losing quality that our eyes can see.
# @see https://github.com/jtescher/image_optimizer
def optimize(image_path)
print_status("Optimizing #{image_path}, this may take a while...")
ImageOptimizer.new(image_path, quiet: true).optimize
end
# Restores the transparent pixels for a PNG file based on a map.
def restore_transparency(img_path, transparency_map)
print_status("Restoring transparency for #{img_path}")
img = ChunkyPNG::Image.from_file(img_path)
transparency_map.each do |i|
img.pixels[i] = ChunkyPNG::Color::TRANSPARENT
end
img.save(img_path)
end
# Zip a file
def zip_file(fname)
png = File.read(fname)
zip = Zip::OutputStream.write_buffer(::StringIO.new('')) do |o|
o.put_next_entry(CARDNAME)
o.write(png)
end
dst = "#{fname}.zip"
File.write(dst, zip.string)
dst
end
# Creates a Base64 file for a PNG
def make_base64_file(fname)
b64 = Base64.strict_encode64(File.read(fname))
File.write(B64NAME, b64)
B64NAME
end
# Creates a fake PNG that actually functions like a ZIP file. Once is ZIP file is decompressed,
# there is actually another PNG in it (which in our case, is the flag).
def finalize_king_of_spades(fake_card_path, king_of_spades_path)
print_status("Creating flag as #{FLAGNAME}")
zip = zip_file(king_of_spades_path)
print_status("King of Spades compressed as: #{zip}")
`cat #{fake_card_path} #{zip} > #{FLAGNAME}`
FLAGNAME
end
# Initializes our image workspace
def make_work_folder_if_empty
unless Dir.exists?(WORKFOLDER)
FileUtils.mkdir_p(WORKFOLDER)
print_status("Workspace \"#{WORKFOLDER}\" created")
end
end
# Removes the workspace at the end of the script
def cleanup
if Dir.exists?(WORKFOLDER)
FileUtils.remove_dir(WORKFOLDER)
print_status("Workspace \"#{WORKFOLDER}\" deleted.")
end
end
# Returns the file size
def get_file_size(img_path)
File.size(img_path)
end
def print_status(msg='')
puts "* #{msg}"
end
def init_workspace
make_work_folder_if_empty
FileUtils.copy(RESOURCES.first, WORKFOLDER)
FileUtils.copy(RESOURCES.last, WORKFOLDER)
@fake_card_path = "#{WORKFOLDER}/#{RESOURCES.first}"
@king_of_spades_path = "#{WORKFOLDER}/#{RESOURCES.last}"
end
# Returns the MD5 string of a file
def get_md5(fname)
Digest::MD5.hexdigest(File.read(fname))
end
def fake_card_path
@fake_card_path
end
def king_of_spades_path
@king_of_spades_path
end
def process_cards
resize_image(fake_card_path)
resize_image(king_of_spades_path)
fake_png_transparency_map = map_transparent_pixels(fake_card_path)
king_of_spades_transparency_map = map_transparent_pixels(king_of_spades_path)
[ fake_card_path, king_of_spades_path ].each do |card_path|
jpg_path = convert_png_to_jpg(card_path)
convert_jpg_to_png(jpg_path)
case card_path
when fake_card_path
restore_transparency(card_path, fake_png_transparency_map)
when king_of_spades_path
restore_transparency(card_path, king_of_spades_transparency_map)
end
optimize(card_path)
end
md5 = get_md5(king_of_spades_path)
print_status("Final King of Spades MD5 hash is: #{md5} (use this on the score server)")
flag_path = finalize_king_of_spades(fake_card_path, king_of_spades_path)
print_status("Flag created: #{flag_path} (Size: #{get_file_size(flag_path)} bytes)")
b64_file = make_base64_file(flag_path)
print_status("Base64 file for the flag is: #{b64_file} (Size: #{get_file_size(b64_file)} bytes)")
end
def main
init_workspace
process_cards
ensure
cleanup
end
if __FILE__ == $PROGRAM_NAME
main
end
=begin
Script output:
* Workspace "temp" created
* Original temp/fake.png dimension = 700x500
* Resized temp/fake.png dimension = 574x410
* Original temp/king_of_spades.png dimension = 700x500
* Resized temp/king_of_spades.png dimension = 574x410
* Mapping transparent pixels for temp/fake.png
* Mapping transparent pixels for temp/king_of_spades.png
* Converting temp/fake.png to JPG to reduce image quality to 10
* Converting temp/fake.jpg back to PNG
* Restoring transparency for temp/fake.png
* Optimizing temp/fake.png, this may take a while...
pngquant: unrecognized option `--speed 1'
* Converting temp/king_of_spades.png to JPG to reduce image quality to 10
* Converting temp/king_of_spades.jpg back to PNG
* Restoring transparency for temp/king_of_spades.png
* Optimizing temp/king_of_spades.png, this may take a while...
pngquant: unrecognized option `--speed 1'
* Final King of Spades MD5 hash is: 8fc453ee48180b958f98e0d2d856d1c8 (use this on the score server)
* Creating flag as flag.png
* King of Spades compressed as: temp/king_of_spades.png.zip
* Flag created: flag.png (Size: 150533 bytes)
* Base64 file for the flag is: flag_base64.txt (Size: 200712 bytes)
* Workspace "temp" deleted.
=end

View File

@ -1,6 +0,0 @@
#!/bin/bash
zip king_of_spades.zip king_of_spades.png
cat fake.png king_of_spades.zip > card.png
rm king_of_spades.zip
echo "Done"