Slammer

Xiomara CTF 2018 - RE (150 pts).

Xiomara CTF 2018: Slammer

Event Challenge Category Points Solves
Xiomara Slammer Reverse 150 ¯\(ツ)

Description

Slammer is a reverse engineering challenge, we must find the good password to validate this challenge.

$ ./slammer
password: flag
Wrong!

The associated file is here.

TL;DR

The binary is a 64 bits ELF executable. After having disassembled the program with IDA I saw that the code is decrypting itself. Each character of the flag is a key which serves to decrypt the next block of code. Deciphering the code with a debugger can be very tedious. So I wrote a script with r2pipe to decrypt each block of code.

Static analysis

Use file command to find the file type.

$ file slammer
slammer: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Okay, it’s a 64 bits ELF we can open it with IDA. As you will see below, program use syscall to interact with user.

Identification with CFF Explorer

Remember that arguments are passed through register and syscall number through rax register.

rax system call rdi rsi rdx
0 sys_read unsigned int fd char* buf size_t count
1 sys_write unsigned int fd const char* buf size_t count

The program prints “password: “ and reads 50 characters on stdin. The following piece of code is interesting,

cmp byte ptr [rcx],'x'
jz short loc_600199
mov eax, 1   ; sys_write
mov edi, 1   ; stdout
mov rsi, "Wrong!\n"
mov edx, 8
syscall      
mov eax, 3Ch ; sys_exit
mov edi, 1
syscall      ; exit(1)

The program checks if the first character entered by the user is a ‘x’. If the condition is not satisfied the program will exit. The following piece of code prepares registers for a decryption loop. As you can see the first character is stored in rax.

loc_600199:
  mov rax,[rcx]  ; rcx points to first character entered by user
  mov edi, 0CB6h
  xor esi,esi    

This piece of code decrypt code at loc_6001B2, it uses the first character ( stored in al ) as key.

loc_6001A3:
  cmp esi,edi
  jz short loc_6001B2
  xor byte ptr loc_6001B2[esi],al ; decipher code
  inc esi
  jmp short loc_6001A3
loc_6001B2:                ; ciphered code
  xor [rdi+1141F8B9h],al
  ...

Dynamic Analysis

We can use radare2 to debug the binary. Place breakpoint in the loop, do some iteration and you will see the deciphered code. We must know the following radare2 commands.

  • db to place a breakpoint
  • dc to continue execution
  • s to seek at address
  • pd [n] to disassemble n instructions
$ radare2 -d slammer
[0x00600120]> db 0x006001a5
[0x00600120]> dc
password: xiomara
hit breakpoint at: 6001a5
[0x006001a5]> 40dc
hit breakpoint at: 6001a5
[...]
[0x006001a5]> s 0x006001b2
[0x006001b2]> pd 8
            0x006001b2      48ffc1         inc rcx
            0x006001b5      803969         cmp byte [rcx], 0x69        ; [0x69:1]=255 ; 'i' ; 105
        ,=< 0x006001b8      7427           je 0x6001e1
        |   0x006001ba      b801000000     mov eax, 1
        |   0x006001bf      bf01000000     mov edi, 1
        |   0x006001c4      48bef3004000.  movabs rsi, 0x4000f3
        |   0x006001ce      ba08000000     mov edx, 8
        |   0x006001d3      0f05           syscall

The rcx register, which pointed to the beginning of character string, points to the second character. Remove the last breakpoint and put a new breakpoint after the loop, on the deciphered instruction.

[0x006001b2]> db -0x006001a5 # delete breakpoint
[0x006001b2]> db 0x006001b8
[0x006001b2]> dc
hit breakpoint at: 6001b8

As you will see below, the code checks if the second character is ‘i’ and enter in a new decryption loop.

[0x006001b2]> pd 3
            0x006001b2      48ffc1         inc rcx
            0x006001b5      803969         cmp byte [rcx], 0x69        ; [0x69:1]=255 ; 'i' ; 105
        ,=< ;-- rip:
        ,=< 0x006001b8 b    7427           je 0x6001e1
[0x006001b2]> pd 20 @ 0x6001e1
            0x006001e1      488b01         mov rax, qword [rcx]
            0x006001e4      bf6e0c0000     mov edi, 0xc6e              ; 3182
            0x006001e9      31f6           xor esi, esi
        .-> 0x006001eb      39fe           cmp esi, edi
       ,==< 0x006001ed      740b           je 0x6001fa
       ||   0x006001ef      673086fa0160.  xor byte [esi + 0x6001fa], al
       ||   0x006001f6      ffc6           inc esi
       | =< 0x006001f8      ebf1           jmp 0x6001eb

It seems there is one decryption loop by characters in the flag. It might be very tedious to debug this program by hand.

Scripting

We will use r2pipe to interact with radare2 in order to extract each character of the flag. Simply open the program with the debugger as follows:

r2 = r2pipe.open("./slammer",['-d','-e','dbg.profile=profile.rr2'])

The profile script allows passing input to the program during debugging session.

#!/usr/bin/rarun2
stdin=./input.txt

To begin we need to find a cmp byte [rcx], const8 instruction. cmd function allows to execute command in radare2 session. The script below debugs the program step by step (ds) and disassemble each instruction until you find a cmp byte [rcx] instruction.

pdj 1 command returns a formatted JSON structure which represents the current instruction. We use the ptr field to retrieve the operand of cmp instruction. To satisfied the condition, we must have entered a correct password. We use the wx command to modify entered password pointed by rcx during the execution.

r2 = r2pipe.open("./slammer",['-d','-e','dbg.profile=profile.rr2'])

while 'invalid' not in r2.cmd('s'):
  r2.cmd('ds')
  r2.cmd('sr rip')
  ins_list = r2.cmdj('pdj 1')
  if not ins_list:
    continue
  ins = ins_list[0]
  opcode = ins['opcode']

  if 'cmp byte [rcx]' in opcode:
    char = ins['ptr']
    r2.cmd('wx {} @ rcx'.format(hex(char)))
    password+=chr(char)
    print(password)

It works but it’s very slow because of the use of step by step mode.

Remember what you have done with the debugger :

  • place a breakpoint in the loop
  • do some iterations
  • delete the breakpoint in the loop
  • place a new breakpoint after the loop
  • continue

We will make exactly the same things. Each decryption routine begins with the same code :

0x006001e4      bfXXXXXXXX     mov edi, 0xXXXXXXXX
0x006001e9      31f6           xor esi, esi
0x006001eb      39fe           cmp esi, edi
0x006001ed      74XX           je 0xXXXXXXXX

In the script, we will add the following code to detect a mov edi, 0xXXXXXXXX instruction followed by a je 0xXXXXXXXX instruction. The script puts a breakpoint on je 0xXXXXXXXX instruction and do some iteration. After it deletes the breakpoint and puts another breakpoint on the end of the decryption routine. The program will continue the execution until it reaches the breakpoint at the end of decryption routine.

if 'mov edi' in opcode:
  ins = r2.cmdj('pdj 4')[3]
  if ins['type'] == 'cjmp':
      # place breakpoint in the loop
      r2.cmd('db {}'.format(hex(ins['offset'])))
      # do some iterations
      r2.cmd('dc')
      r2.cmd('dc')
      r2.cmd('dc')
      r2.cmd('dc')
      # delete breakpoint in the loop
      r2.cmd('db -{}'.format(hex(ins['offset'])))
      # place breakpoint after the loop
      print("place breakpoint at %x" % ins['jump'])
      r2.cmd('db {}'.format(hex(ins['jump'])))
      # continue
      r2.cmd('dc')
      print("breakpoint hit end loop ")

We will wait few seconds to get the flag:

$ ./slammer_solve.py
[...]
xiomara{cool_thumbs_up_if_solved_using_r2pip
hit breakpoint at: 600dbd
hit breakpoint at: 600dbd
hit breakpoint at: 600dbd
hit breakpoint at: 600dbd
hit breakpoint at: 600dca
xiomara{cool_thumbs_up_if_solved_using_r2pipe
hit breakpoint at: 600e05
hit breakpoint at: 600e05
hit breakpoint at: 600e05
hit breakpoint at: 600e05
hit breakpoint at: 600e12
xiomara{cool_thumbs_up_if_solved_using_r2pipe}

Appendices

The complete script can be found here :

#!/usr/bin/python3
import r2pipe

password=""
r2 = r2pipe.open("./slammer",['-d','-e','dbg.profile=profile.rr2'])

while 'invalid' not in r2.cmd('s'):
    r2.cmd('ds') # single step
    r2.cmd('sr rip') # seek to rip
    # disassemble
    ins_list = r2.cmdj('pdj 1')
    if not ins_list:
        continue

    ins = ins_list[0] # first instruction
    opcode = ins['opcode']

    #print(opcode)
    if 'mov edi' in opcode:
        ins = r2.cmdj('pdj 4')[3]
        if ins['type'] == 'cjmp':
            r2.cmd('db {}'.format(hex(ins['offset'])))
            r2.cmd('dc')
            r2.cmd('dc')
            r2.cmd('dc')
            r2.cmd('dc')
            # delete breakpoint in loop
            r2.cmd('db -{}'.format(hex(ins['offset'])))
            # place breakpoint after loop
            print("place breakpoint at %x" % ins['jump'])
            r2.cmd('db {}'.format(hex(ins['jump'])))
            # loop
            print("pass loop")
            r2.cmd('dc')
            print("breakpoint hit")

    elif 'cmp byte [rcx]' in opcode:
        char = ins['ptr']
        r2.cmd('wx {} @ rcx'.format(hex(char)))
        password+=chr(char)
        print(password)