Introduction
The attachment can be found in my repository: link
Many people ask me to give this writeup because I announced that all the 11 teams used unintended solution on IRC. Now comes the intended one :D
This task asks you to exploit the ruby interpreter, given arbitrary ruby code execution but protected by seccomp rules.
The service code (server.rb):
#!/usr/bin/ruby require __dir__ + '/sandbox/sandbox' Sandbox.run $stdout.sync = true proc { my_exit = Kernel.method(:exit!) my_puts = $stdout.method(:puts) ObjectSpace.each_object(Module) { |m| m.freeze } set_trace_func proc { |event, file, line, id, binding, klass| bad_id = /`|exec|foreach|fork|load|method_added|open|read(?!line$)|require|set_trace_func|spawn|syscall|system/ bad_class = /(?<!True|False|Nil)Class|Module|Dir|File|ObjectSpace|Process|Thread/ if event =~ /class/ || (event =~ /call/ && (id =~ bad_id || klass.to_s =~ bad_class)) my_puts.call "\e[1;31m== Hacker Detected (#{$&}) ==\e[0m" my_exit.call end } }.call loop do # line = Readline.readline('real> ', true) # this uses sysopen.. print 'real> ' line = gets puts '=> ' + eval(line, TOPLEVEL_BINDING).inspect end
And the ruby binary is Full RELRO / NX / PIE.
Sandbox.run installed the seccomp rules, which forbid syscalls with number greater than 199, and a blacklist of some syscalls. The most important is the open syscall is forbidden!
But the seccomp rules doesn't check the architecture running. Now the target is clear: we need exploit ruby to arbitrary shellcode execution, then use the 32-bit open syscall to read the flag.
Unintended Solution
As I said before, all the 11 teams used unintended solution that bypassing the set_trace_func method. After bypassing it, you can use the powerful method syscall, which can read / write memory easily:syscall accepts integral arguments as pointers, so using read / write syscall directly leads to write / read memory, respectively.
Update (2017/11/12): If you are interested in how to bypass the tracing, you can see this writeup by team 0x0C113F04.
Intended Solution
Time for the intended solution.I use the method IO#ioctl, and here's its document:
Yes, this method has the same behavior as method syscall, treats integers as pointers. I saw this document and started to find when will ioctl do read / write in memory - and I found it.ioctl(integer_cmd, arg) → integerProvides a mechanism for issuing low-level commands to control or query I/O devices. Arguments and results are platform dependent. If arg is a number, its value is passed directly. If it is a string, it is interpreted as a binary sequence of bytes. On Unix platforms, seeioctl(2)
for details. Not implemented on all platforms.
Here is the list of valid first argument of ioctl. I use the TIOCGWINSZ/TIOCSWINSZ pair to play with memory. These two arguments are used for getting / setting current window size. My leak method is defined as follows, which can leak 8 bytes given target address.
TIOCGWINSZ = 0x5413 TIOCSWINSZ = 0x5414 leak = lambda do |addr| s = 'A' * 8 begin STDIN.ioctl(TIOCSWINSZ, addr) rescue return 0 end STDIN.ioctl(TIOCGWINSZ, s) s[0, 8].unpack("Q*")[0] end
And the write method is easier:
write = lambda do |addr, val| STDIN.ioctl(TIOCSWINSZ, [val].pack("Q*")) STDIN.ioctl(TIOCGWINSZ, addr) end
Exploitation
With arbitrary memory read / write, I'm sure you can pwn the ruby binary. Following is my exploitation flow.- "".__id__ * 2 would be a heap address (it's a feature!)
- Scan heap to leak any library's address
- Leak stack address via glibc._libc_argv
- Write ROP payload to do mprotect and write shellcode that mmap / read on the mprotect-ed page
- Write 32-bit open shellcode to the mmapped page
- Gotcha!
See my exploit script in my repo for details ;)
Fun Facts
unintended?
Actually bypassing and using Kernel.syscall became intended solution right before this challenge being released. If you participated in this CTF you might notice that the easier challenge (Baby Ruby Escaping) was released 6 hours before this (Real) one. We want to know if anyone can bypass the tracing - and of course the answer is yes.
So it became our challenge, should we harden the tracing? Or just let teams exploit it with the easy way?
I chose the second option for three reasons:
- My solution is really hard to find, my teammate can't solve this challenge even I told him it's a documented feature and related to IO.
- There're too many hard pwnables, let this be a medium-level one :p
- (Main reason) No matter how we harden the tracing, it still might be bypassed.
So this challenge wasn't modified even we already knew teams can use Kernel.syscall to solve it.
Bypassing the tracing
The tracing sandbox had been modified 10+ versions (We had a tiny atk&def on it :p). And the final version you saw is the unbreakable one - to us.
While during the competition we got at least three kinds of bypassing strategy. Good job, you guys are really amazing!
Post Script
Thanks for reading, wish you guys had a fun time with Ruby 😛
沒有留言:
張貼留言