I had to head into work this weekend to prepare for some upcoming travel, so I originally had no plans to play any CTFs. But, when Skolor told me there was a Ruby challenge in the Tokyo Westerns CTF 2016, I knew I had to check it out. Despite my insistence that Ruby is better than Python, I always seem to struggle with Ruby challenges. Sadly, this one was no different...
Restricted Ruby
This challenge (files here) had 3 parts:
Private ppc1.chal.ctf.westerns.tokyo 1111 (Flag 1)
Local ppc1.chal.ctf.westerns.tokyo 1112 (Flag 2)
Comment ppc1.chal.ctf.westerns.tokyo1113 (Flag 3)
Common to all of them was a file called restrict.rb
:
require "fiddle/import"
module Libc
extend Fiddle::Importer
dlload "libc.so.6"
extern "int alarm(int)"
end
module Seccomp
extend Fiddle::Importer
dlload "libseccomp.so.2"
extern "void* seccomp_init(int)"
extern "int seccomp_rule_add(void*, int, int, int)"
extern "int seccomp_load(void*)"
SCMP_ACT_KILL = 0x00000000
SCMP_ACT_TRAP = 0x00030000
SCMP_ACT_ERRNO_0 = 0x00050000 # ignore syscall
SCMP_ACT_ALLOW = 0x7fff0000
end
class Restrict
def self.set_timeout
#Libc.alarm(10)
end
def self.seccomp
ctx = Seccomp.seccomp_init(Seccomp::SCMP_ACT_ERRNO_0)
ret = 0
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 1, 0) # allow write
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 60, 0) # allow exit
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 3, 0) # allow close
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 12, 0) # allow brk
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 10, 0) # allow mprotect
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 9, 0) # allow mmap
ret |= Seccomp::seccomp_rule_add(ctx, Seccomp::SCMP_ACT_ALLOW, 11, 0) # allow munmap
ret |= Seccomp::seccomp_load(ctx)
fail "Failed to setup syscall." unless ret == 0
end
end
This file uses Fiddle
, an FFI wrapper that is
part of Ruby's standard libraries, to call the alarm
function from libc
and
to restrict access to syscalls with seccomp
. We are given 10 seconds to
input what we want to be evaluated for each challenge (not actually relevant for solving, just a way for them to
automatically kill connections left open). We are also only given the syscalls write
, exit
, close
, brk
,
mprotect
, mmap
, and munmap
.
Each challenge was implemented in its own, separate file that imported this one as a module and started with run.sh
:
#!/bin/bash
cd $(dirname "$0")
/usr/bin/ruby2.0 $1.rb 2> /dev/null | head --bytes=512 # Ubuntu 14.04(64bit) ruby2.0 package
This places an additional restriction on the challenge: We can only get 512 bytes back from their interpreter at a time. Again, not actually relevant for solving the challenge...but, limits the amount of information you can glean from their environment in a single connection.
Challenge One: Private
The first challenge was private.rb
running on port 1111:
require_relative 'restrict'
Restrict.set_timeout
class Private
private
public_methods.each do |method|
eval "def #{method.to_s};end"
end
def flag
return "TWCTF{CENSORED}"
end
end
p = Private.new
Private = nil
input = STDIN.gets
fail unless input
input.size > 24 && input = input[0, 24]
Restrict.seccomp
STDOUT.puts eval(input)
NOTE: For each of these challenges, the server's copy has the actual flag, and not "TWCTF{CENSORED}"
I started this challenge by heading down the entirely wrong path: Messing with interpreter internals. In hindsight, I should have solved this in about 30 seconds. Spoiler alert: I didn't.
The first thing I jumped to was thinking about how I could recover an instance of the Private
class
and display its source code. Normally, we could do something like
p.class.instance_method(:flag).source_location
to get the source file, then open and read it.
Even if seccomp
allowed that, this wouldn't work - class
itself is marked private:
# output from my local copy of the script when run with "p.class" as the input
private.rb:24:in `eval': private method `class' called for :Private (NoMethodError)
from private.rb:24:in `eval'
from private.rb:24:in `<main>'
Next, I thought about how I might call a private method from an object. The Object
class, which
pretty much everything in Ruby inherits from, has a method that can be used for this called send
.
You simply pass a Symbol
of the function name into send
and voila! Unfortunately, that trick
(and the same thing with __send__
) won't work either for the same reason:
# output from my local copy of the script when run with "p.send(:flag)" as the input
private.rb:24:in `eval': private method `send' called for :Private (NoMethodError)
from private.rb:24:in `eval'
from private.rb:24:in `<main>'
I then decided to look into other things available in the interpreter. Most of the Python jail challenges
in the past have required you to cleverly use aspects of the environment to solve them, after all.
If you run global_variables
, you get a lot of output:
$;
$-F
$@
$!
$SAFE
$~
$&
$`
$'
$+
$=
$KCODE
$-K
$,
$/
$-0
$\
$_
$stdin
$stdout
$stderr
$>
$<
$.
$FILENAME
$-i
$*
$?
$$
$:
$-I
$LOAD_PATH
$"
$LOADED_FEATURES
$VERBOSE
$-v
$-w
$-W
$DEBUG
$-d
$0
$PROGRAM_NAME
$-p
$-l
$-a
$1
$2
$3
$4
$5
$6
$7
$8
$9
Read through
this page if you
want to know what each of these are. One in particular, $"
, looked useful because I could get the path
to a loaded module's source file with it. Here's the output of $".reverse
(remember, we only get
512 bytes of output):
/home/ngltewpad1/restrict.rb
/usr/lib/ruby/2.0.0/fiddle/import.rb
/usr/lib/ruby/2.0.0/fiddle/cparser.rb
/usr/lib/ruby/2.0.0/fiddle/struct.rb
/usr/lib/ruby/2.0.0/fiddle/pack.rb
/usr/lib/ruby/2.0.0/fiddle/value.rb
/usr/lib/ruby/2.0.0/fiddle.rb
/usr/lib/ruby/2.0.0/fiddle/closure.rb
/usr/lib/ruby/2.0.0/fiddle/function.rb
/usr/lib/x86_64-linux-gnu/ruby/2.0.0/fiddle.so
/usr/lib/ruby/2.0.0/rubygems.rb
/usr/lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb
/usr/lib/ruby/2.0.0/monitor.rb
/usr/lib/ruby/2.0.0/thread.
Interestingly, we can get a File
object returned here with File.open($"[-1], 'r')
:
#<File:0x0000000152f6b0>
This shouldn't be possible due to the seccomp
rules... I'm guessing this means File
doesn't
actually open
the file when you create the object? Reading the file with
File.open($"[-1]).read
didn't give me any output. The shorter File.read($"[-1])
didn't work either.
Since the output of local_variables
and instance_variables
wasn't helpful, I temporarily ran out of ideas
and went back to work for awhile.
On the way home from work later, I thought about trying to modify the p
instance itself.
Unfortunately, Soen beat me to it before I could get home. Here was what I
had come up with as an answer to the problem:
class<<p;def x;flag;end;end;p.x
This modifies p
's "singleton class" (a special parent class that only belongs to the object itself)
to have a function x
that calls flag
and returns its result. Unfortunately, it's 8 characters
too long... Fortunately, Ruby now has an even easier (and, more importantly, shorter) way of doing this.
Here's Soen's solution:
def p.x;flag;end;p.x
To say I over-thought things earlier is...a bit of an over-statement. I probably could
have guessed the flag itself (TWCTF{PrivatePreview}
) faster than I was solving the challenge.
Anyway, why does this work? In Ruby, private
simply means that the "receiver" must be self
. So,
we simply create a new function on the instance p
and tell it to return the private value we want
to access. Because we are in the same object, this access is allowed. You can always modify instances
like this in Ruby and it's a pretty great feature - especially when faced with a challenge like this!
Challenge 2: Local
The second challenge was local.rb
running on port 1112:
require_relative 'restrict'
Restrict.set_timeout
def get_flag(x)
flag = "TWCTF{CENSORED}"
x
end
input = STDIN.gets
fail unless input
input.size > 60 && input = input[0, 60]
Restrict.seccomp
STDOUT.puts get_flag(eval(input))
We can't use a function here because x
takes no arguments. flag
will be out-of-scope unless it's
passed as an argument, so whatever solution we come up with has to be in the current scope. I started
by trying a lambda
(specifically, lambda { flag }
):
#<Proc:0x000000014576c0@(eval):1 (lambda)>
...dammit. The lambda
isn't executed...it's simply returned. Okay, so...what if we get this Proc
called? Let's try get_flag(lambda { flag }).call
:
# output from my local copy of the script
local.rb:15:in `eval': undefined local variable or method `flag' for main:Object (NameError)
from (eval):1:in `<main>'
from local.rb:15:in `eval'
from local.rb:15:in `<main>'
Great. Now, flag
is out-of-scope again because the call
happens before it's passed to the function.
In case you were wondering, you can't fix this by doing something like get_flag(get_flag(lambda { flag }).call)
.
That still won't evaluate the Proc
inside the proper context for the same reason. The inner call will evaluate
out-of-scope before it would be passed to the outer call.
So, we need something that will be executed in the scope of the function. A block itself would probably
work, but the argument to get_flag
is x
- not &x
. Blocks aren't objects in Ruby. They're just
an argument type for a method. Thus, we can only use a Proc
here. But, a Proc
clearly won't work...
Since this seemed to be the wrong path to head down, I decided to work on getting a handle to the function itself. I figured, once I had that, I could find a way to bash that little function into a string no matter how long it took.
How do we get a handle to our function? Simple: We get it from Kernel
with Kernel.method(:get_flag)
:
#<Method: Module(Object)#get_flag>
See, get_flag
was declared at top-level scope. You might think that means it's not attached to any
object as a result, but you'd be wrong. The top-level scope in Ruby is actually just the main
object.
You can read more about this at
the bottom of this page.
Anyway, it turns out you can also do self.method(:get_flag)
to get (effectively) the same result:
#<Method: Object#get_flag>
This is important later because it's shorter.
Anyway, now that we've got a handle to our function, how can we get the value of flag
out of it?
I'll be honest: I have no idea. I tried calling virtually every method I could on it. There are lots:
# output from my local copy of the script when run with "self.method(:flag).methods" as the input
[:!, :!=, :!~, :<=>, :==, :===, :=~, :[], :__id__, :__send__, :arity, :call, :class, :clone, :curry,
:define_singleton_method, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?,
:get_flag, :hash, :inspect, :instance_eval, :instance_exec, :instance_of?,
:instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables,
:is_a?, :itself, :kind_of?, :method, :methods, :name, :nil?, :object_id, :original_name, :owner,
:parameters, :pretty_inspect, :pretty_print, :pretty_print_cycle, :pretty_print_inspect,
:pretty_print_instance_variables, :private_methods, :protected_methods, :public_method,
:public_methods, :public_send, :receiver, :remove_instance_variable, :respond_to?, :send,
:singleton_class, :singleton_method, :singleton_methods, :source_location, :super_method, :taint,
:tainted?, :tap, :to_enum, :to_proc, :to_s, :trust, :unbind, :untaint, :untrust, :untrusted?]
The source_location
(local.rb
), the instance_variables
([]
), the class
(Method
)...they all
seem like they're going to be useful or interesting, but they're not for a number of reasons.
You might be able to use tap
somehow? I never figured it out if you can.
Grasping for straws, I went back to the only thing I know I know how to do in Ruby: Disassemble
bytecode. I never included it in the Sullivan Square write-up, but it turns out
the default Ruby 2.0 interpreter (KRI) has a built-in disassembler. Hilariously, this gave me the solution!
All I had to do was run RubyVM::InstructionSequence.disasm(self.method(:get_flag))
:
== disasm: <RubyVM::InstructionSequence:[email protected]>==============
local table (size: 3, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 3] x<Arg> [ 2] flag
0000 trace 8 ( 4)
0002 trace 1 ( 5)
0004 putstring "TWCTF{EnjoyC0untryLife}"
0006 setlocal_OP__WC__0 2
0008 trace 1 ( 6)
0010 getlocal_OP__WC__0 3
As you can see above, the flag is TWCTF{EnjoyC0untryLife}
. I'm fairly confident that
wasn't the intended solution...but, I couldn't figure out the right one. I'm just glad they gave me
60 characters to work with!
Challenge 3: Comment
The third and final challenge was comment.rb
on port 1113:
require_relative 'restrict'
Restrict.set_timeout
input = STDIN.gets
fail unless input
input.size > 60 && input = input[0, 60]
require_relative 'comment_flag'
Restrict.seccomp
STDOUT.puts eval(input)
This does a require_relative
of another file, comment_flag.rb
, on line 8. Here's the contents
of that file:
# FLAG is TWCTF{CENSORED}
Let's see what happens when I pair one of my ideas from the last challenge (disassembling bytecode) with one
of the ideas from the first challenge (abusing interpreter globals)! Here's what we get when I
run RubyVM::InstructionSequence.compile_file($"[-1]).to_a
:
2
0
1
{:arg_size=>0, :local_size=>1, :stack_max=>1}
<main>
/home/ngltewpad3/comment_flag.rb
/home/ngltewpad3/comment_flag.rb
1
top
0
putnil
leave
As I suspected, we can't use disassembly to directly solve this one since we're looking for a comment.
It's crazy that this worked at all, though! File.read($"[-1])
doesn't work, so this means I must have
found a way around seccomp
? Should probably take another look at this later...
Anyway, I played with this for a little while longer and then decided there was no way I could get the flag from here.
Even if there were options to pass to RubyVM::InstructionSequence
to include comments (after reading through
the documentation, I don't think there are), we're out of available
characters in our input string.
At this point, it was late and I decided to head to bed. I'd taken a break to get another questline done in World of WarCraft: Legion earlier. Wife's orders.
On Sunday, I took a crack at this again. I next looked at Marshal
for a bit, hoping that it could maybe get me the
comment. Unfortunately, as I suspected, the comments are gone well before you're able to Marshal
data. I also didn't
have a handle to the comment (since it's not an object), so Marshal
wouldn't have done me much good. I then looked
at Module
, but Ruby modules aren't the same as Python modules. Calling Ruby's effective equivalent of Python's
import
won't get you a module or a new namespace or anything. You have to explicitly declare that your code is part
of a module in Ruby. Even if they had, I don't think that would allow us to see the comment.
Unfortunately, the rest of my day was consumed by family activities, so I missed stumbling upon the answer until after the CTF was over. When I wrote up everything above (I figured I could at least get a blog post out of failing miserably), I left it at the last sentence in that last paragraph. Re-reading it that night, I thought: What if the comment was an object?
In Ruby, there's a handy module called ObjectSpace
that I found
when I was searching around
for extra ideas for the first challenge above. I never tried it, though, because there
was no way I could possibly get a solution in under 24 characters (ObjectSpace.each_object
is already 23). Here, like
the last challenge, we have 60.
Locally, I turned on script
and tried
ObjectSpace.each_object.each { |o| puts o }
in comment.rb
. each_object
is an enumerator
that lets you access every object still "living" in the interpreter. This line attempted to print every single object
to my terminal that hadn't yet been garbage collected. I got a bunch of stuff, then an ArgumentError
:
comment.rb:11:in `eval': NULL pointer given (ArgumentError)
from (eval):1:in `puts'
from (eval):1:in `puts'
from (eval):1:in `block in <main>'
from (eval):1:in `each_object'
from (eval):1:in `<main>'
from comment.rb:11:in `eval'
from comment.rb:11:in `<main>'
Since I had no idea what caused this, I tried a few different things like { |o| puts o if o.to_s }
and { |o| puts o if o.class == String }
for the block. The last one worked, but only gave me String
objects back.
At least it had finished... I exit
ed my script
and did grep TWCTF typescript
. Lo and behold, it gave me back:
Binary file typescript matches
. Success! ...kinda. grep -a TWCTF typescript
showed me that I had, in fact,
found the string I was looking for: # FLAG is TWCTF{CENSORED}
. Who knew program comments were kept around as
String
objects in the interpreter?!
The last thing I had to figure out was how to get the string filtered out server-side. Remember: We only get 512
bytes of output from the server, so printing every string won't work. I decided a regex was the way to go and came
up with ObjectSpace.each_object{|o|puts o if /TWCTF/ =~ o}
. This, of course, didn't work - it's not operating only on
String
objects. Fortunately, the documentation told me that I could give each_object
a class or module and it
would only return instances of those.
Okay, final answer: ObjectSpace.each_object(String){|o|puts o if /TWCTF/ =~ o}
. The server was still up, so I tried
it out:
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TWCTF
TW
Dammit! Why are there all these "TWCTF"s?! Locally, there were a few, but I thought it would be okay...
Fine. Final, final answer: ObjectSpace.each_object(String){|o|puts o if /TWCTF\{/ =~ o}
. This gave me:
# FLAG is TWCTF{Transformation_t0_Artificial_Satelite}
# FLAG is TWCTF{Transformation_t0_Artificial_Satelite}
6270
Success! Better late than never, right..?
Lessons Learned
Similar to Sullivan Square last year, try the easiest possible solutions first. I should have had the first challenge finished very quickly, but I got side-tracked by trying to be clever. The straight-forward solution was the right one.
Also, if something won't work now, but still seems useful, keep track of it. I should have had the third challenge finished during the CTF because I'd already stumbled across the solution. I just forgot about it until after it was over...
Lastly, don't be afraid to try things! I never would have solved the second challenge (and wouldn't have bothered to write this up) if I hadn't given disassembling bytecode a shot. I'm hoping someone posts the "correct" solution, though, because I'm pretty sure that wasn't it!
If you're new to CTFs or programming or whatever, don't be afraid to screw up! As long as you analyze what you did and make changes to avoid the problem in the future, messing up is simply part of learning. Ask yourself: If you give up, how are you going to have content for your blog?