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.
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)