-

haxelion's den

nothing to see here

Latests

Articles

Writeups

Projects

About

CSAW 2015

We didn’t have enough time and enough people to do CSAW 2015 seriously, but we did have fun on some of the challenges. Below are the two most memorable ones for me.

Reversing 500 - Wyvern

The binary

As soon as you start reversing this binary, the first thing you notice are those weird basic blocks everywhere:

0x4020f7         mov        eax, dword [ds:x3]
0x4020fe         mov        ecx, dword [ds:y4]
0x402105         mov        edx, eax
0x402107         sub        edx, 0x1
0x40210d         imul       eax, edx
0x402110         and        eax, 0x1
0x402115         cmp        eax, 0x0
0x40211a         sete       sil
0x40211e         cmp        ecx, 0xa
0x402124         setl       dil
0x402128         or         sil, dil
0x40212b         test       sil, 0x1
0x40212f         jne        0x40213a
0x402135         jmp        0x404264

What are those? Opaque predicates. Indeed, the branch taken by jne depends on the variables x3 and y4 but those are generated at runtime and always have the same value. This means the branch is already determined but you have to actually run the program to determine it. Here these opaque predicates are used in two ways:

  • splitting basic block and adding false branches in between them
  • obfuscating the control flow of real branches

Here it’s not too bad: the opaque predicates are always the same inside a function and the basic block are not too much shuffled. You can still deduce the order of the basic blocks and guess what a function is doing. This seems to be the basic flow of the program:

  • read user input (the dragon secret) at 0x401ef5 in main()
  • call the start_quest() (0x404350) function with the dragon secret as a C++ string
  • inside start_quest() a global std::vector (called hero) is initialized, then sanitize_input() (0x401cc0) is called with a copy of the dragon secret string
  • inside sanitize_input() a whole lot of bullshit happens where the hero vector is manipulated using the dragon string and transform_input() (0x4014b0) is called with some other std::vector.
  • somehow the whole thing needs to return 0x1337

As you can see I still have a very rough idea of what the program is doing although I solved the challenge. But you only really need to spot two things.

The first one is the length check on the dragon secret string inside start_quest(). As you can see it’s compared with a static variable from the data segment but binary shifted right by two (probably some constants obfuscation) which evaluate to 28. Again it’s mixed with an opaque predicate but it’s pretty obvious: the input string should be 28 characters long.

(note: I annotated some stack variables, that’s not in the actual binary)

0x404698         mov        rdi, qword [ss:rbp+dragon_secret]; our input
0x40469c         call       j__ZNKSs6lengthEv; the length method of std::string
0x4046a1         sub        rax, 0x1; remove the \n from the character count
0x4046a7         mov        r9d, dword [ds:legend]; the static variable
0x4046af         sar        r9d, 0x2; probably just for obfuscation
0x4046b3         movsxd     rcx, r9d
0x4046b6         cmp        rax, rcx; the comparison
0x4046b9         setne      r10b; set r10b if not equal
0x4046bd         mov        r9d, dword [ds:x25]; start of the opaque predicate
0x4046c5         mov        r11d, dword [ds:y26]
0x4046cd         mov        ebx, r9d
0x4046d0         sub        ebx, 0x1
0x4046d6         imul       r9d, ebx
0x4046da         and        r9d, 0x1
0x4046e1         cmp        r9d, 0x0
0x4046e8         sete       r14b
0x4046ec         cmp        r11d, 0xa
0x4046f3         setl       r15b
0x4046f7         or         r14b, r15b
0x4046fa         test       r14b, 0x1
0x4046fe         mov        byte [ss:rbp+var_41], r10b
0x404702         jne        0x40470d
0x404708         jmp        0x404c13
0x40470d         mov        al, byte [ss:rbp+var_41]
0x404710         test       al, 0x1; here the result is finally used
0x404712         jne        0x40471d
0x404718         jmp        0x4047b8

The loop increment inside sanitize_input(). This variable is used to index the input string (the dragon secret) at 0x402188. You can easily guess that there should be a loop going through all the characters, it’s supposed to check your input after all.

0x40368e         mov        rax, qword [ss:rbp+input_idx]; pointer to the variable
0x403692         mov        ecx, dword [ds:rax]; derefence it
0x403694         add        ecx, 0x1; increment it
0x40369a         mov        dword [ds:rax], ecx; update the variable
0x40369c         mov        ecx, dword [ds:x17]; again opaque predicate
0x4036a3         mov        edx, dword [ds:y18]
0x4036aa         mov        esi, ecx
0x4036ac         sub        esi, 0x1
0x4036b2         imul       ecx, esi
0x4036b5         and        ecx, 0x1
0x4036bb         cmp        ecx, 0x0
0x4036c1         sete       dil
0x4036c5         cmp        edx, 0xa
0x4036cb         setl       r8b
0x4036cf         or         dil, r8b
0x4036d2         test       dil, 0x1
0x4036d6         jne        0x4036e1
0x4036dc         jmp        0x4040e2

If you set a breakpoint at the start of sanitize_input() and at the loop increment you can validate two things: it will only execute sanitize_input() if the dragon secret is 28 characters long, it never steps on the loop increment. Why? Because it detects your dragon secret is invalid at the first character and breaks early. And that’s a flawed design because it means I can count the loop iterations to know how many characters I got right (note: you can also use Intel pin or another DBI framework to count the number of instructions executed).

The attack is the following: run the program on dragon secrets of 28 caracters but starting with a different first character each time, if the program breaks on the loop increment it means the character is correct and you can attack the next one. At the end you will have the complete dragon secret.

Of course this takes many tries and you have to automate. Here’s how to do it with GDB and its python API. This script implements a “counting breakpoint” using the GDB API. Passing it to the --command flag of GDB will return at the end the loop iteration count of sanitize_input().

gdb_wyvern_count.py

 
# inherit from gdb Breakpoint class
class CountingBreakpoint(gdb.Breakpoint):
    # variable to hold the hit counter
    count = 0
    # override the stop method which is called when this breakpoint is 
    # triggered
    def stop(self):
        self.count += 1

    def getCount(self):
        return self.count

# breakpoint to check if we did call sanitize_input()
bk1 = CountingBreakpoint("* 0x401cc0")
# breakpoint to count the loop iterations
bk2 = CountingBreakpoint("* 0x40368e")
# called when the program exit, display the breakpoint counters
gdb.events.exited.connect(lambda event: print("Breakpoint 1 count: {}\nBreakpoint 2 count: {}".format(bk1.getCount(), bk2.getCount())))
# called when the program stops on a breakpoint, force it to continue
gdb.events.stop.connect(lambda event: gdb.execute("continue"))
# start the program immediately when this script is loaded
gdb.execute("run")

Now this other script uses the first one to bruteforce the dragon secret.

wyvern_attack.py

 
from subprocess import check_output

# the shell command to run one attempt
cmd = 'echo "{}" | gdb --command gdb_wyvern_count.py wyvern_c85f1be480808a9da350faaa6104a19b'
passwd = [32 for i in range(0,28)]

# bruteforce character per character
for i in range(0, 28):
    # visible ascii range
    for j in range(32, 127):
        # mutate the password
        passwd[i] = j
        passwd_str = ''.join(map(lambda x: chr(x), passwd))
        # some shell escape
        passwd_str = passwd_str.replace('\\', '\\\\').replace('`', '\\`').replace('"', '\\"')
        # execute wyvern
        result = check_output(cmd.format(passwd_str), shell=True)
        # just to make sure we hit sanitize_input()
        if 'Breakpoint 1 count: 1' not in result:
            print('Execution failed ...')
        # that's what we hope for :)
        if 'Breakpoint 2 count: {}'.format(i+1) in result:
            print(''.join(map(lambda x: chr(x), passwd)))
            break

It will take some time to run but in the end you will get the flag ;)

% python2 wyvern_attack.py 2>/dev/null
d                           
dr                          
dr4                         
dr4g                        
dr4g0                       
dr4g0n                      
dr4g0n_                     
dr4g0n_o                    
dr4g0n_or                   
dr4g0n_or_                  
dr4g0n_or_p                 
dr4g0n_or_p4                
dr4g0n_or_p4t               
dr4g0n_or_p4tr              
dr4g0n_or_p4tri             
dr4g0n_or_p4tric            
dr4g0n_or_p4tric1           
dr4g0n_or_p4tric1a          
dr4g0n_or_p4tric1an         
dr4g0n_or_p4tric1an_        
dr4g0n_or_p4tric1an_i       
dr4g0n_or_p4tric1an_it      
dr4g0n_or_p4tric1an_it5     
dr4g0n_or_p4tric1an_it5_    
dr4g0n_or_p4tric1an_it5_L   
dr4g0n_or_p4tric1an_it5_LL  
dr4g0n_or_p4tric1an_it5_LLV 
dr4g0n_or_p4tric1an_it5_LLVM
% ./wyvern_c85f1be480808a9da350faaa6104a19b 
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
    brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: dr4g0n_or_p4tric1an_it5_LLVM
success

[+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}

Crypto 500 - Bricks of Gold

The mysterious file

No stegano involved, pure crypto, I swear.

First of all we have that crucial description:

We’ve captured this encrypted file being smuggled into the country. All we know is that they rolled their own custom CBC mode algorithm - its probably terrible.

I’m not going to explain what CBC is, just look it up if you don’t know. You should also have very quickly discovered at the end of the file this line:

000bbc90: 7ba3 b08e 01dc dae0 49b0 a5ff 5448 455f  {.......I...THE_
000bbca0: 5345 4352 4554 5f49 563d 4341 5348 0a    SECRET_IV=CASH.

So we have the IV, “CASH”, and thus also the block size: 32 bits. And that’s all we have. So how do we break an unkown crypto function?

Any good crypto should behave as a PRNG thus if we want to break it we should find something not random, a pattern, and understand why it exists. The best place to check in a file is always the beginning (because there’s usually a header there) and the end (that’s purely empirical).

And indeed at the start of the file there are those weird 32 bytes:

00000130: 166f 76c6 0462 6abc 6962 5d87 4c4e 45fd  .ov..bj.ib].LNE.
00000140: 214e 72c6 0462 6bbc 6962 5c87 4c4e 4ffd  !Nr..bk.ib\.LNO.

This should never happen in any serious crypto implementation.

If you ignore the first 2 bytes of each line, the only difference is in the 6th nibble (hex digit) of each block. And if you start looking at the binary encoding of those nibbles you will see there’s only one bit flipped in that nibble. What does this means?

Remember it’s CBC, each ciphertext block is used to xor the next block of plaintext. So if one bit is flipped in a ciphertext block, the same bit will be flipped in the plaintext block before it goes through the encryption function. And in a good cipher this should have a pseudo random effect on the output known as diffusion. Here the exact same bit is flipped at the output of the encryption function and this happens on 3 consecutive blocks.

This means there’s no diffusion. This means it’s a bit level substitution function. This means this is a xor cipher (the only binary operation which is bijective).

So we know it’s a CBC mode xor cipher (seriously, wtf) and we know the IV, now we need the key.

Again this is a file, and a file has a header which often contains a predictable magic number.

So what we can do is:

  • guess a file format
  • use the magic to compute the key on the first block
  • decrypt the whole file and attempt to open it
  • repeat those steps on the most common file format until it works

After a few tries, the pdf magic number worked. Here’s the python script:

bog_decrypt.py

 
import sys

data = list(open(sys.argv[1], 'rb').read())
block_size = 4
iv = 'CASH'
magic = '\x25PDF'
key = []

# compute the key from the first block
for i in range(0, block_size):
    key.append(chr(ord(iv[i]) ^ ord(data[i]) ^ ord(magic[i])))

# decrypt it from end to start, easier to do since it's in place decryption
# make sure idx is block_size aligned
idx = len(data) - (len(data)%block_size) - block_size
while idx >= block_size:
    for i in range(0, block_size):
        data[idx+i] = chr(ord(data[idx+i])^ord(data[idx-block_size+i])^ord(key[i]))
    idx -= block_size

for i in range(0, block_size):
    data[i] = chr(ord(data[i])^ord(iv[i])^ord(key[i]))

open(sys.argv[2], 'wb').write(''.join(data))

Look at that beauty!

Decrypted PDF

Anyway, thanks a lot to ISIS lab for those cool challenges ;)

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License .