Another NDH quals another VM wait ...
Introduction
Last weekend I participated in the Nuit du Hack CTF Quals 2013 with my teamate delroth, you can find an excellent write-up about escaping a Python sandbox on his blog.
So I decided to post a writeup too about the last crackme challenge "Crackme #1".
This Reverse Engineering challenge was a virtual machine, so I decided to reverse the full vm, but today I figured out something ...
Do you remember last ndh prequals ? no no I will not talk about bmp chall but about VMNDH-2k12, it was exactly the same VM (I'm very sad to have lost some hours to reverse something that I know), the only changes was the opcode values, so this post will not deal with the vm stuff you have all the information on this website.
VM Opcode
You can find the vm dump at :
LOAD:000000000048F2E8 vm_dump db 7Fh ;
LOAD:000000000048F2E9 db 45h ; E
LOAD:000000000048F2EA db 4Ch ; L
LOAD:000000000048F2EB db 46h ; F
LOAD:000000000048F2EC db 72h ; r
LOAD:000000000048F2ED db 3
LOAD:000000000048F2EE db 0Ah
Real vm code start at offset 0x06 (byte 0x0A) and the size of the vm code is 0x371 (881 bytes).
Do you want to mary me ?
I love IDA, and I like writting plugins, scripts, processor (when I don't have to reverse IDA for understanding how their api works :p) in python, so after reversing almost the same vm, I decided to write my ndh2k13 processor, here is the code :
from idaapi import *
class DecodingError(Exception):
pass
class NDHProcessor(processor_t):
id = 0x8000 + 5855
flag = PR_ADJSEGS | PRN_HEX
cnbits = 8
dnbits = 8
psnames = ["ndh2k13"]
plnames = ["ndh2k13 VM CPU"]
segreg_size = 0
instruc_start = 0
assembler = {
"flag": AS_NCHRE | ASH_HEXF4 | ASD_DECF1 | ASO_OCTF3 | ASB_BINF2
| AS_NOTAB,
"uflag": 0,
"name": "NDH assembler",
"origin": ".org",
"end": ".end",
"cmnt": ";",
"ascsep": '"',
"accsep": "'",
"esccodes": "\"'",
"a_ascii": ".ascii",
"a_byte": ".byte",
"a_word": ".word",
"a_bss": "dfs %s",
"a_seg": "seg",
"a_curip": "PC",
"a_public": "",
"a_weak": "",
"a_extrn": ".extern",
"a_comdef": "",
"a_align": ".align",
"lbrace": "(",
"rbrace": ")",
"a_mod": "%",
"a_band": "&",
"a_bor": "|",
"a_xor": "^",
"a_bnot": "~",
"a_shl": "<<",
"a_shr": ">>",
"a_sizeof_fmt": "size %s",
}
reg_names = regNames = [
"R0", "R1", "R2", "R3", "R4",
"R5", "R6", "R7", "SP", "BP",
"CS", "DS"
]
instruc = instrs = [
{ 'name': 'PUSH', 'feature': CF_USE1 },
{ 'name': 'PUSHB', 'feature': CF_USE1 },
{ 'name': 'PUSHW', 'feature': CF_USE1 },
{ 'name': 'NOP', 'feature': 0 },
{ 'name': 'POP', 'feature': CF_USE1 },
{ 'name': 'MOV', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'MOVB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'MOVW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ADD', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ADDB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ADDW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'SUB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'SUBB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'SUBW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'MUL', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'MULB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'MULW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'DIV', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'DIVB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'DIVW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'INC', 'feature': CF_USE1 },
{ 'name': 'DEC', 'feature': CF_USE1 },
{ 'name': 'OR', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ORB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ORW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'AND', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ANDB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'ANDW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'XOR', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'XORB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'XORW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'NOT', 'feature': CF_USE1 },
{ 'name': 'JZ', 'feature': CF_USE1 },
{ 'name': 'JNZ', 'feature': CF_USE1 },
{ 'name': 'JMPS', 'feature': CF_USE1 | CF_STOP },
{ 'name': 'TEST', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'CMP', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'CMPB', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'CMPW', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'CALL', 'feature': CF_USE1 | CF_CALL },
{ 'name': 'RET', 'feature': CF_STOP },
{ 'name': 'JMPL', 'feature': CF_USE1 | CF_STOP },
{ 'name': 'END', 'feature': CF_STOP },
{ 'name': 'XCHG', 'feature': CF_USE1 | CF_USE2 },
{ 'name': 'JA', 'feature': CF_USE1 },
{ 'name': 'JB', 'feature': CF_USE1 },
{ 'name': 'SYSCALL', 'feature': 0 },
]
instruc_end = len(instruc)
def __init__(self):
processor_t.__init__(self)
self._init_instructions()
self._init_registers()
def _init_instructions(self):
self.inames = {}
for idx, ins in enumerate(self.instrs):
self.inames[ins['name']] = idx
def _init_registers(self):
self.reg_ids = {}
for i, reg in enumerate(self.reg_names):
self.reg_ids[reg] = i
self.regFirstSreg = self.regCodeSreg = self.reg_ids["CS"]
self.regLastSreg = self.regDataSreg = self.reg_ids["DS"]
def _read_cmd_byte(self):
ea = self.cmd.ea + self.cmd.size
byte = get_full_byte(ea)
self.cmd.size += 1
return byte
def _read_reg(self):
r = self._read_cmd_byte()
if r >= 0x0A:
raise DecodingError()
return r
def _ana_ntypeinstr(self, name, valid):
cmd = self.cmd
optype = self._read_cmd_byte()
if optype not in valid:
raise DecodingError()
if optype not in (4, 5, 6):
cmd[0].type = o_reg
cmd[0].dtyp = dt_word
cmd[0].reg = self._read_reg()
if optype == 0x0:
cmd.itype = self.inames[name]
cmd[1].type = o_reg
cmd[1].dtyp = dt_word
cmd[1].reg = self._read_reg()
elif optype == 0x1:
cmd.itype = self.inames[name + "B"]
cmd[1].type = o_imm
cmd[1].dtyp = dt_byte
cmd[1].value = self._read_cmd_byte()
elif optype == 0x2:
cmd.itype = self.inames[name + "W"]
cmd[1].type = o_imm
cmd[1].dtyp = dt_word
cmd[1].value = self._read_cmd_byte()
cmd[1].value |= self._read_cmd_byte() << 8
elif optype == 0x3:
cmd.itype = self.inames[name]
elif optype == 0x4:
cmd.itype = self.inames[name + "B"]
cmd[0].type = o_imm
cmd[0].dtyp = dt_byte
cmd[0].value = self._read_cmd_byte()
elif optype == 0x5:
cmd.itype = self.inames[name + "W"]
cmd[0].type = o_imm
cmd[0].dtyp = dt_word
cmd[0].value = self._read_cmd_byte()
cmd[0].value |= self._read_cmd_byte() << 8
elif optype == 0x6:
cmd.itype = self.inames[name + "B"]
cmd[0].type = o_phrase
cmd[0].dtyp = dt_word
cmd[0].reg = self._read_reg()
cmd[1].type = o_reg
cmd[1].dtyp = dt_word
cmd[1].reg = self._read_reg()
elif optype == 0xA:
cmd.itype = self.inames[name]
cmd[1].type = o_phrase
cmd[1].dtyp = dt_word
cmd[1].reg = self._read_reg()
else:
raise DecodingError()
def _ana_one_r(self, name):
cmd = self.cmd
cmd.itype = self.inames[name]
cmd[0].type = o_reg
cmd[0].dtyp = dt_word
cmd[0].reg = self._read_reg()
def _ana_two_r(self, name):
cmd = self.cmd
cmd.itype = self.inames[name]
cmd[0].type = o_reg
cmd[0].dtyp = dt_word
cmd[0].reg = self._read_reg()
cmd[1].type = o_reg
cmd[1].dtyp = dt_word
cmd[1].reg = self._read_reg()
def _ana_jmp(self, name, size=16):
cmd = self.cmd
cmd.itype = self.inames[name]
addr = self._read_cmd_byte()
if size == 16:
addr |= self._read_cmd_byte() << 8
if (addr & 0x8000):
addr -= 0x10000
else:
if addr & 0x80:
addr -= 0x100
addr += cmd.ea + cmd.size
cmd[0].type = o_near
cmd[0].dtyp = dt_word
cmd[0].addr = addr
def _ana(self):
cmd = self.cmd
opcode = self._read_cmd_byte()
if opcode == 0x1F:
self._ana_ntypeinstr("PUSH", valid=(3, 4, 5))
elif opcode == 0x0A:
cmd.itype = self.inames["JMPL"]
self._ana_jmp("JMPL")
elif opcode == 0x1C:
self._ana_ntypeinstr("MOV", valid=(0, 1, 2, 6, 7, 8, 9, 10))
elif opcode == 0x0C:
cmd.itype = self.inames["CALL"]
flags = self._read_cmd_byte()
if flags == 0x4:
addr = self._read_cmd_byte()
addr |= self._read_cmd_byte() << 8
if (addr & 0x8000):
addr -= 0x10000
addr += cmd.ea + cmd.size
cmd[0].type = o_near
cmd[0].dtyp = dt_word
cmd[0].addr = addr
elif flags == 0x3:
reg = self._read_reg()
cmd[0].type = o_reg
cmd[0].dtyp = dt_word
cmd[0].reg = reg
else:
raise DecodingError()
elif opcode == 0x30:
cmd.itype = self.inames["SYSCALL"]
elif opcode == 0x0B:
cmd.itype = self.inames["RET"]
elif opcode == 0x1A:
self._ana_ntypeinstr("SUB", valid=(0, 1, 2))
elif opcode == 0x0D:
self._ana_ntypeinstr("CMP", valid=(0, 1, 2))
elif opcode == 0x11:
self._ana_jmp("JZ")
elif opcode == 0x09:
cmd.itype = self.inames["END"]
elif opcode == 0x1F:
self._ana_jmp("JMPS", size=8)
elif opcode == 0x17:
self._ana_one_r("INC")
elif opcode == 0x10:
self._ana_jmp("JNZ")
elif opcode == 0x16:
self._ana_one_r("DEC")
elif opcode == 0x13:
self._ana_ntypeinstr("XOR", valid=(0, 1, 2))
elif opcode == 0x0E:
self._ana_two_r("TEST")
elif opcode == 0x1D:
self._ana_one_r("POP")
elif opcode == 0x07:
self._ana_jmp("JA")
elif opcode == 0x0F:
self._ana_jmp("JMPS", size=8)
elif opcode == 0x06:
self._ana_jmp("JB")
elif opcode == 0x1B:
self._ana_ntypeinstr("ADD", valid=(0, 1, 2))
elif opcode == 0x08:
self._ana_two_r("XCHG")
elif opcode == 0x19:
self._ana_ntypeinstr("MUL", valid=(0, 1, 2))
else:
raise DecodingError()
return cmd.size
def ana(self):
try:
return self._ana()
except DecodingError:
return 0
def _emu_operand(self, op):
if op.type == o_mem:
ua_dodata2(0, op.addr, op.dtyp)
ua_add_dref(0, op.addr, dr_R)
elif op.type == o_near:
if self.cmd.get_canon_feature() & CF_CALL:
fl = fl_CN
else:
fl = fl_JN
ua_add_cref(0, op.addr, fl)
def emu(self):
cmd = self.cmd
ft = cmd.get_canon_feature()
if ft & CF_USE1:
self._emu_operand(cmd[0])
if ft & CF_USE2:
self._emu_operand(cmd[1])
if ft & CF_USE3:
self._emu_operand(cmd[2])
if not ft & CF_STOP:
ua_add_cref(0, cmd.ea + cmd.size, fl_F)
return True
def outop(self, op):
if op.type == o_reg:
out_register(self.reg_names[op.reg])
elif op.type == o_imm:
OutValue(op, OOFW_IMM)
elif op.type in [o_near, o_mem]:
ok = out_name_expr(op, op.addr, BADADDR)
if not ok:
out_tagon(COLOR_ERROR)
OutLong(op.addr, 16)
out_tagoff(COLOR_ERROR)
QueueMark(Q_noName, self.cmd.ea)
elif op.type == o_phrase:
out_symbol('[')
out_register(self.reg_names[op.reg])
out_symbol(']')
else:
return False
return True
def out(self):
cmd = self.cmd
ft = cmd.get_canon_feature()
buf = init_output_buffer(1024)
OutMnem(15)
if ft & CF_USE1:
out_one_operand(0)
if ft & CF_USE2:
OutChar(',')
OutChar(' ')
out_one_operand(1)
if ft & CF_USE3:
OutChar(',')
OutChar(' ')
out_one_operand(2)
term_output_buffer()
cvar.gl_comm = 1
MakeLine(buf)
def PROCESSOR_ENTRY():
return NDHProcessor()
If you have trouble for dumping vm_opcode, you can find the dump here.
Load the vm into IDA, choose "ndh2k13 VM CPU: ndh2k13", when IDA ask you for the memory organisation tell him :
- ROM START ADDRESS : 0x8000
- Loading address : 0x8000
Now you can start reverse :)
Solution
We have got only 3 syscall for this version of the vm :
- R0 = 0x01 : exit
- R0 = 0x03 : read
- R0 = 0x04 : write
The code of the vm start here :
ROM:8317 MOVW R0, aPassword
ROM:831C CALL write_msg
ROM:8320 SUBB SP, $20
ROM:8324 MOV R2, SP
ROM:8328 MOVB R1, 0
ROM:832C MOVB R3, $1F
ROM:8330 MOVB R0, $3
ROM:8334 SYSCALL ; SYSCALL READ
ROM:8335 MOV R0, R2
ROM:8339 CALL check_password
And the pseudo - code of the function check_password is :
char *key = 0x8342;
if (strlen(buf_password) == 9) // "\n" include
if (buf_password[0] ^ key[0] == 'S')
if (buf_password[1] ^ key[1] == '[')
if (buf_password[2] ^ key[2] == 'K')
if (buf_password[3] ^ key[3] == ')')
... etc ...
print("GOOD PASSWORD\n")
exit
print("BAD PASSWORD\n")
exit
And the good password is :
key = [0x12, 0x21, 0x02, 0x19, 0x25, 0x34, 0x29, 0x11]
res = ['S', '[', 'K', ')', 'R', 'v', 'Z', 'I']
password = ""
for i in xrange(0, 8):
password += chr(ord(res[i]) ^ key[i])
print password
The flag was 'AzI0wBsX'.
Conclusion
I'm an idiot to reverse an entire vm and not figure out it was the same than last year, but during ctf you want to be the fatest and don't (always) think about old challenges.
Another solution was to count the number of instructions executed by the vm, yeah because when you write crackme, I think you should compute hash, or something like that, because consecutive if statements is just lulz.
Enjoy :
#! /bin/sh
charset="a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
charset="$charset _ - ! $ % ^ \\& \\* + = 0 1 2 3 4 5 6 7 8 9 > < ,"
cat > gdbscript <<EOF
set height 0
define countinstrs
p "instr"
c
countinstrs
end
b *0x0000000000400C7F # call handler opcode
run < guess
countinstrs
countinstrs
countinstrs
countinstrs
countinstrs
countinstrs
countinstrs
countinstrs
countinstrs
EOF
for c in $charset; do
guess="$c"
echo -n "$guess" > guess
echo "AAAAAAA" >> guess
echo -n "trying key `cat guess`... "
gdb ./simple < gdbscript 2>&1 \
| grep '^\$.*= "instr"$' \
| tail -1 \
| cut -d ' ' -f 1 \
| cut -c 2-
done
First letter :
trying key zAAAAAAA... 245
trying key AAAAAAAA... 252
trying key BAAAAAAA... 245
Edit script for replacing first letter by "A"
trying key AyAAAAAA... 252
trying key AzAAAAAA... 259
trying key AAAAAAAA... 252
And do it for the 8 letters.
Write up RMLL crackme level 2
C'est parti pour le level suivant du RMLL.
Première approche :
w4kfu@vm-debian:~/RMLL$ ./mars1
serial : 1234556065906954904560950654
Bad serial :/
On sort gdb pour voir tout ca :
w4kfu@vm-debian:~/RMLL$ gdb ./mars1
(gdb) disas main
....
0x0804888c <main+9>: movl $0xd6eddb56,0x1e(%esp)
0x08048894 <main+17>: movl $0x8086fb8b,0x22(%esp)
0x0804889c <main+25>: movl $0x9e0847d1,0x26(%esp)
0x080488a4 <main+33>: movl $0x638ce745,0x2a(%esp)
0x080488ac <main+41>: movb $0x0,0x2e(%esp)
...
0x080488f2 <main+111>: call 0x804842c <ptrace@plt>
...
0x080489cd <main+330>: call 0x804844c <strlen@plt>
0x080489d2 <main+335>: cmp $0x10,%eax
...
0x08048a76 <main+499>: call 0x8048554 <chiffre>
...
0x08048ac5 <main+578>: call 0x804840c <memcmp@plt>
....
On retrouve les même symptômes que le premier challenge, à la différence du ptrace et du code complètement offusqué. Si on s'amuse à disas la fonction chiffre c'est pas beau à voir.
On va devoir nettoyer tout ça car on peut y voir des trucs qui vont bien nous ennuyer :
0x0804857e <chiffre+42>: rdtsc
0x0804859e <chiffre+74>: (bad)
0x080485a0 <chiffre+76>: leave
0x080485a3 <chiffre+79>: sub %ecx,%eax
0x080485a5 <chiffre+81>: cmp $0x3000,%eax
0x080485aa <chiffre+86>: ja 0x80485aa <chiffre+86>
J'ai donc écrit le code suivant pour enlever toutes ces cochonneries :
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> void look_pattern(int fd); void replace_pattern(int fd, off_t offset, int nb); int main(void) { int fd; if ( (fd = open("./copy_mars1", O_RDWR) ) == -1 ) perror("open()"); look_pattern(fd); close(fd); return 0; } void look_pattern(int fd) { off_t offset = 0x554; off_t offset_end = 0x87b; char pattern[7]; while ( offset < offset_end) { if (read(fd, pattern, 7) <= 0) perror("read()"); if ( pattern[0] == 0x77 ) replace_pattern(fd, offset, 2); else if ( pattern[0] == 0xc9 ) replace_pattern(fd, offset, 1); else if ( pattern[0] == 0xf1 ) replace_pattern(fd, offset, 1); else if (!strncmp(pattern, "\xdb\xe6", 2)) replace_pattern(fd, offset, 2); else if (!strncmp(pattern, "\x0f\x31", 2)) replace_pattern(fd, offset, 2); else if(!memcmp(pattern, (void*)"\x2b\xc1\x3d\x00", 4)) replace_pattern(fd, offset, 9); offset++; lseek(fd, offset, SEEK_SET); } } void replace_pattern(int fd, off_t offset, int nb) { int i = 0; lseek(fd, offset,SEEK_SET); for ( i = 0 ; i < nb ; i++) if ( write(fd, "\x90", 1) == -1) perror("write()"); }
Il faut aussi s'occuper du ptrace, voici le code du wrapper de ptrace :
int ptrace(int a, int b, int c, int d) { return 0; }
Pour le compiler et l'utiliser au sein de gdb :
>gcc -shared my_ptrace.c -o ptrace.so
(gdb) set environment LD_PRELOAD ./ptrace.so
Mais malheureusement, ça n'a pas suffit, ca m'a nettoyé un peu le disas.. Alors j'ai décidé de m'écrire un script gdb pour tracer TOUT ce que fait la fonction chiffre :
w4kfu@vm-debian:~$ cat .gdbinit
define test
x/i $eip
ni
if ( $eip != 0x08048a7b)
test
Ce script affiche le code exécuté par mon processeur puis fait un next instruction. Tout ça jusqu'à arriver à l'eip 0x08048a7b ( quand on sort de la fonction chiffre ). Je pose donc un breakpoint, j'active les logs et je bourrine ma touche "ENTREE".
(gdb) set environment LD_PRELOAD ./ptrace.so
(gdb) b* chiffre
Breakpoint 1 at 0x8048554
(gdb) r
Starting program: /home/w4kfu/RMLL/copy_mars1
serial : 1234567890123456
Breakpoint 1, 0x08048554 in chiffre ()
(gdb) set logging file mars1_disas
(gdb) set logging on
Copying output to mars1_disas.
(gdb) test
....
BOURRINAGE TOUCHE ENTREE \o/
....
(gdb) set logging off
Bon par contre y'a 500 trucs qui se répètent et des lignes useless, bon bah on sort notre couteau suisse :
sed "/(gdb)/d" mars1_disas > mars1_disas2
cat mars1_disas2 | sort | uniq > real_disas
On arrive au résultat suivant : mars1_disas. On retranscrit le tout en C :
#include <stdio.h> int main(void) { unsigned char in_fct[16] = "\x06\x7f\x92\x55\xea\x97\x0e\x4a\x22\x21\x07\xc7\x7f\x65\x9c\x93"; unsigned char input[16] = "1234567890123456"; unsigned int ebp_14; unsigned int ebp_c; unsigned int eax; unsigned int ecx; unsigned int edx; unsigned char wtf[2]; ebp_14 = 0; while ( ebp_14 <= 0xf ) { ebp_c = 0; while ( ebp_c <= ebp_14 ) { input[ebp_14] = in_fct[ebp_c] ^ input[ebp_14]; edx = input[ebp_14] << ( ebp_14 & 0x7 ); wtf[0] = edx; eax = ebp_14 & 0x7; ecx = 0x8 - eax; edx = input[ebp_14] >> ecx; wtf[1] = edx; input[ebp_14] = wtf[1] | wtf[0]; ebp_c++; } printf("%x\n",input[ebp_14]); ebp_14++; } return 0; }
Dans le code ci-dessus, j'ai déclaré les noms de façon assez explicite histoire de pas me tromper dans ce que je faisais. Maitenant qu'on sait comment fonctionne la fonction chiffre, j'ai "bruteforcé" le tout étant donné que l'on a les 16 octets auxquels on doit arriver :
void bruteforce(void) { unsigned char a; unsigned char b; unsigned char in_fct[16] = "\x06\x7f\x92\x55\xea\x97\x0e\x4a\x22\x21\x07\xc7\x7f\x65\x9c\x93"; unsigned char input[17] = {0}; unsigned char result[16] = "\x56\xdb\xed\xd6\x8b\xfb\x86\x80\xd1\x47\x08\x9e\x45\xe7\x8c\x63"; unsigned int ebp_14; unsigned int ebp_c; unsigned int eax; unsigned int ecx; unsigned int edx; unsigned char wtf[2]; ebp_14 = 0; while ( ebp_14 <= 0xf ) { a = '\x00'; while ( b != result[ebp_14]) { a += 1; b = a; ebp_c = 0; while ( ebp_c <= ebp_14 ) { b = in_fct[ebp_c] ^ b; edx = b << ( ebp_14 & 0x7 ); wtf[0] = edx; eax = ebp_14 & 0x7; ecx = 0x8 - eax; edx = b >> ecx; wtf[1] = edx; b = wtf[1] | wtf[0]; ebp_c++; } } input[ebp_14] = a; ebp_14++; } printf(" input = %s\n", input); }
On teste tout ça :
w4kfu@vm-debian:~/RMLL$ gcc -o reverse reverse.c
w4kfu@vm-debian:~/RMLL$ ./reverse
input = POGddyrbtjYIoXfv
w4kfu@vm-debian:~/RMLL$ ./mars1
serial : POGddyrbtjYIoXfv
Good job !
Use your serial as password for the next level !
Voilà c'est fini! Je n'ai pas le level 3 :(, si quelqu'un passe sur le blog et l'a disponible je suis intéressé car une fois le level2 résolu la machine était down.
Write up RMLL crackme level 1
Ce billet aurait dû sortir il y a une semaine mais le week-end dernier se déroulait le smpCTF auquel j'ai participeéau sein de l'équipe BiG-DadDy ( arrivée 12ème ) ou avec dad nous nous sommes occupés surtout des épreuves p0wnMe, bref maintenant j'ai le temps donc je le consacre à faire ce write up.
Du 6 au 11 Juillet 2010, à Bordeaux se déroulait les Rencontres Mondiales du Logiciel Libre. Je fais actuellement un stage dans la boite XXXX et mon maître de stage se rendait sur place ( je n'ai pas eu la chance de l'accompagner :( ). Bref, dès son premier jour d'arrivée il m'envoie un e-mail avec ce lien.
Quoi des challenges de sécurité !?! Je me précipite sur les crackme.
Première approche :
w4kfu@vm-debian:~/RMLL$ ./mars0
serial : 34534343423432403245-450-435-043-50435
Nice try ! But ... no ...
Allez hop on sort gdb :
w4kfu@vm-debian:~/RMLL$ gdb ./mars0
...
(gdb) disas main
....
0x0804855b <main+9>: movl $0xc49c6547,0x1e(%esp)
0x08048563 <main+17>: movl $0xee987f5e,0x22(%esp)
0x0804856b <main+25>: movl $0x4cced336,0x26(%esp)
0x08048573 <main+33>: movl $0xee7c11e4,0x2a(%esp)
.....
0x080485a9 <main+87>: call 0x80483e4 <strlen@plt>
0x080485ae <main+92>: cmp $0x10,%eax
....
0x080485d2 <main+128>: call 0x80484f4 <chiffre>
....
0x080485ee <main+156>: call 0x80483b4 <memcmp@plt>
0x080485f3 <main+161>: test %eax,%eax
0x080485f5 <main+163>: jne 0x804860a <main+184>
....
Déjà ici on peut commencer à comprendre un peu le fonctionnement de notre crackme :
- 16 octets qui sont placés à la suite dans la stack.
- Notre serial doit être d'une longueur de 16 ( 0x10 ) caractères.
- Une fonction chiffre.
- Le test se fait à partir d'un memcmp().
Posons un breakpoint sur le memcmp() :
(gdb) b* main+156
Breakpoint 1 at 0x80485ee
(gdb) r
Starting program: /home/w4kfu/RMLL/mars0
serial : 1234567890123456
Breakpoint 1, 0x080485ee in main ()
(gdb) x/4x $esp
0xbfffecc0: 0xbfffecef 0xbfffecde 0x00000010 0x080483a0
(gdb) x/4x 0xbfffecef
0xbfffecef: 0xb5dd0032 0x85f83d00 0x3ca1842f 0xca5e21a6 <<< Notre SERIAL
(gdb) x/4x 0xbfffecde
0xbfffecde: 0xc49c6547 0xee987f5e 0x4cced336 0xee7c11e4 <<< Ce a quoi il est comparee
Regardons maitenant du côté de la fonction chiffre :
(gdb) disas chiffre
0x080484f5 <chiffre+1>: mov %esp,%ebp
0x080484f7 <chiffre+3>: sub $0x20,%esp
0x080484fa <chiffre+6>: movl $0x82e83303,-0x15(%ebp)
0x08048501 <chiffre+13>: movl $0xbac50639,-0x11(%ebp)
0x08048508 <chiffre+20>: movl $0x19abd6e,-0xd(%ebp)
0x0804850f <chiffre+27>: movl $0x8f1d6099,-0x9(%ebp)
0x08048516 <chiffre+34>: movb $0x0,-0x5(%ebp)
0x0804851a <chiffre+38>: movl $0x0,-0x4(%ebp)
0x08048521 <chiffre+45>: jmp 0x804854a <chiffre+86>
0x08048523 <chiffre+47>: mov -0x4(%ebp),%eax
0x08048526 <chiffre+50>: add 0x8(%ebp),%eax
0x08048529 <chiffre+53>: mov -0x4(%ebp),%edx
0x0804852c <chiffre+56>: add 0x8(%ebp),%edx
0x0804852f <chiffre+59>: movzbl (%edx),%ecx
0x08048532 <chiffre+62>: mov -0x4(%ebp),%edx
0x08048535 <chiffre+65>: lea (%ecx,%edx,1),%edx
0x08048538 <chiffre+68>: mov %edx,%ecx
0x0804853a <chiffre+70>: mov -0x4(%ebp),%edx
0x0804853d <chiffre+73>: movzbl -0x15(%ebp,%edx,1),%edx
0x08048542 <chiffre+78>: xor %ecx,%edx
0x08048544 <chiffre+80>: mov %dl,(%eax)
0x08048546 <chiffre+82>: addl $0x1,-0x4(%ebp)
0x0804854a <chiffre+86>: cmpl $0xf,-0x4(%ebp)
0x0804854e <chiffre+90>: jle 0x8048523 <chiffre+47>
0x08048550 <chiffre+92>: leave
0x08048551 <chiffre+93>: ret
Le disas ci-dessus peut être traduit de la sorte en C :
#include <stdio.h> char *chiffre(char *cherche, char *sec); int main(void) { int i; char *mot_chercher = "1234567890123456"; char *second_word = "\x03\x33\xe8\x82\x39\x06\xc5\xba\x6e\xbd\x9a\x01\x99\x60\x1d\x8f"; char *mot_crypt; mot_crypt = chiffre(mot_chercher,second_word); for ( i = 0 ; i < 16 ; i++) printf("%x ",mot_crypt[i]); return 0; } char * chiffre(char *cherche, char *sec) { int i = 0; static char found[16]; for ( i = 0; i < 16; i++) found[i] = (cherche[i] + i)^ sec[i]; return found; }
On teste pour voir si on retrouve bien le même résultat :
w4kfu@vm-debian:~/RMLL/mars$ gcc -o mars0 mars0.c
w4kfu@vm-debian:~/RMLL/mars$ ./mars0
32 0 dd b5 0 3d f8 85 2f 84 a1 3c a6 21 5e ca
Notre code est correct, bien maintenant il faut retrouver le serial valide, ce n'est pas très compliqué car nous savons à quoi il doit être égal. Nous n'avons qu'à faire l'opération inverse :
#include <stdio.h> char *chiffre(char *cherche, char *sec); int main(void) { char *mot_chercher = "\x47\x65\x9c\xc4\x5e\x7f\x98\xee\x36\xd3\xce\x4c\xe4\x11\x7c\xee"; char *second_word = "\x03\x33\xe8\x82\x39\x06\xc5\xba\x6e\xbd\x9a\x01\x99\x60\x1d\x8f"; printf("Le serial est %s\n", chiffre(mot_chercher,second_word)); } char * chiffre(char *cherche, char *sec) { int i = 0; static char found[16]; for ( i = 0; i < 16; i++) found[i] = (cherche[i] ^ sec[i])-i; return found; }
On exécute tout ça :
w4kfu@vm-debian:~/RMLL/mars$ gcc -o mars0 mars0.c
w4kfu@vm-debian:~/RMLL/mars$ ./mars0
Le serial est DUrCctWMPeJBqdSR
Et on teste notre serial :
w4kfu@vm-debian:~/RMLL$ ./mars0
serial : DUrCctWMPeJBqdSR
Good job !
Use your serial as password for the next level !
Pages : 1