ENEI is a portuguese meeting for students of Computer Science. This year, it took place in my city, in the University of Coimbra.
NEI invited me and Leandro Braguês, from DOGNÆDIS, to organize a CTF for the event. We were the evil minds behind all the challenges whose solutions will be presented below.
Our server is not available anymore, but you can download the final scoreboard here.
Can you find the secret inside this file?
Download: sorry
Let's execute it:
$ ./sorry I'M S0RRY
We need to inspect the program to see what it does besides printing this message. Disassembling the main function seems to be a good start.
main: 40071c mov rbp, rsp 40071f sub rsp, 0x30 400723 mov eax, 0x0 400728 call check 40072d test eax, eax 40072f je 0x40082b ...
If check returns true, the program jumps to 0x40082b
:
40082b mov edi, 0x4008f7 ; "I'M S0RRY" 400830 call puts@PLT
We just need to bypass the check procedure, avoiding that jump. However, we can also try to make it return false.
check: 4006f1 mov rbp, rsp 4006f4 mov ecx, 0x0 ; argument "data" for method ptrace@PLT 4006f9 mov edx, 0x0 ; argument "addr" for method ptrace@PLT 4006fe mov esi, 0x0 ; argument "pid" for method ptrace@PLT 400703 mov edi, 0x0 ; argument "request" for method ptrace@PLT 400708 mov eax, 0x0 40070d call ptrace@PLT 400712 shr rax, 0x3f 400716 movzx eax, al 400719 pop rbp 40071a ret
This function clearly inspects the presence of a debugger, since it calls ptrace. Let's run it using gdb!
$ gdb sorrygdb-peda$ r Starting program: sorry Good job, GDB is now your friend :) Flag: e37b4a153e3151a817aced31bc4bf9a2448d4483
We are investigating a new case: Xiene got a new sweatshirt for free!
As far as we know, he used an old executable from a webstore, that generates a payment hash trusted by the seller.
Can you help us find the hash he used?
Purchase details:
Product ID: 2893
Xiene's size: L
Date: 15 Mar 2015
Time: 18h:25m:49s
Download: sweat
The program produces the following output when we provide the input specified:
$ ./sweat Product ID: 2893 Size (S, M, L): L Price: 0 Invalid ID.
Once again, we need to disassemble the program to see what's going on.
main: ... 4019f9 mov edi, 0x40643d ; "Product ID: " 4019fe mov eax, 0x0 401a03 call printf@PLT ... 401a19 call scanf@PLT ... 401a45 call fgets@PLT ... 401a85 call time@PLT 401a8a mov qword [ss:rbp+var_40], rax 401a8e lea rax, qword [ss:rbp+var_40] 401a92 mov rdi, rax ; argument "clock" 401a95 call localtime@PLT ... 401ae1 cmp dword [ss:rbp+var_14], 0x0 401ae5 jne 0x401af1 401ae7 mov edi, 0x406463 ; "360 NO SCOPE" 401aec call puts@PLT 401af1 mov rdi, qword [ss:rbp+var_38] 401af5 movsx esi, byte [ss:rbp+var_1] ; argument #2 for method _Z8calcHashicdiiiiii 401af9 mov eax, dword [ss:rbp+var_2C] 401afc mov r9d, dword [ss:rbp+var_20] ; argument #7 for method _Z8calcHashicdiiiiii ... 401b2a call _Z8calcHashicdiiiiii ; calcHash(int, char, double, int, int, int, int, int, int) ...
The program calls calcHash using the input values. "360 NO SCOPE" was a fake flag and there was a lot of people submitting it. Not so easy! We need to understand what the calcHash procedure does.
calcHash: 401527 mov rbp, rsp 40152a push rbx 40152b sub rsp, 0x1a8 401532 mov dword [ss:rbp+var_184], edi 401538 mov eax, esi 40153a movsd xmmword [ss:rbp+var_190], xmm0 401542 mov dword [ss:rbp+var_194], edx 401548 mov dword [ss:rbp+var_198], ecx 40154e mov dword [ss:rbp+var_19C], r8d 401555 mov dword [ss:rbp+var_1A0], r9d 40155c mov byte [ss:rbp+var_188], al 401562 cmp byte [ss:rbp+var_188], 0x53 401569 je 0x40158c 40156b cmp byte [ss:rbp+var_188], 0x4d 401572 je 0x40158c 401574 cmp byte [ss:rbp+var_188], 0x4c 40157b je 0x40158c 40157d mov edi, 0x4063f8 ; "Invalid size. 401582 call puts@PLT 401587 jmp 0x4019e8 40158c cmp dword [ss:rbp+var_184], 0x0 401593 js 0x4015a1 401595 cmp dword [ss:rbp+var_184], 0xa7a 40159f jle 0x4015b0 4015a1 mov edi, 0x406406 ; "Invalid ID." 4015a6 call puts@PLT 4015ab jmp 0x4019e8 4015b0 movsd xmm0, qword [ds:0x406478] 4015b8 ucomisd xmm0, xmmword [ss:rbp+var_190] 4015c0 jae 0x4015d4 4015c2 movsd xmm0, qword [ss:rbp+var_190] 4015ca ucomisd xmm0, xmmword [ds:0x406480] 4015d2 jbe 0x4015e3 4015d4 mov edi, 0x406412 ; "Invalid price." 4015d9 call puts@PLT 4015de jmp 0x4019e8 ... 4015f0 mov esi, 0x406421 ; argument "format" for method sprintf@PLT 4015f5 mov rdi, rax ; argument "str" for method sprintf@PLT 4015f8 mov eax, 0x0 4015fd call sprintf@PLT ... 40167f mov esi, 0x40642b ; "%d/%d/%d-%d:%d:%d", argument "format" for method sprintf@PLT 401684 mov rdi, rax ; argument "str" for method sprintf@PLT 401687 mov eax, 0x0 40168c call sprintf@PLT ... 4016c9 mov rdi, rax ; argument #1 for method _Z4sha1RKSs 4016cc call _Z4sha1RKSs ; sha1(std::string const&) ... 4019ec leave 4019ed ret
This function checks if the arguments are valid. Then, it builds a string containing these values and uses SHA-1 algorithm to calculate the hash.
The valid sizes are: "S" (0x53), "M" (0x4d) and "L" (0x4c)
.
The product ID is valid if 0 (0x0) < ID < 2682 (0xa7a)
.
The price is stored as double. Let's inspect the addresses used in this verification in order to understand it.
406478 dq 0x4014000000000000 406480 dq 0x409f3c0000000000
Thus, the price is valid if 5.0 (0x4014000000000000) < price <= 1999.0 (0x409f3c0000000000)
.
These verifications must be bypassed in order to obtain the desired hash. However, it's very to modify the stack. I'm going to set a breakpoint just after the last sprintf and before SHA-1 calls begin, and then search for the input values in memory.
$ gdb sweatgdb-peda$ b *0x401699gdb-peda$ r Starting program: sweat Product ID: 2001 Size (S, M, L): L Price: 8[------------------------------------stack-------------------------------------] 0000|0x7fffffffdf00 -->0x7fffffffdfd0 -->0x7fffffffdfea --> 0x4560000000010000 0008|0x7fffffffdf08 --> 0x4020000000000000 ('') 0016|0x7fffffffdf10 --> 0x7df0000000f 0024|0x7fffffffdf18 --> 0x1100000008 0032|0x7fffffffdf20 --> 0x4020000000000000 ('') 0040|0x7fffffffdf28 --> 0x7d1ffffdf4c 0048|0x7fffffffdf30 ("16/8/2015-15:19:49") 0056|0x7fffffffdf38 ("5-15:19:49")[------------------------------------------------------------------------------] Breakpoint 1, 0x0000000000401699 in calcHash(int, char, double, int, int, int, int, int, int) ()gdb-peda$ find 8.00 Searching for '8.00' in: None ranges Found 2 results, display max 2 items: [stack] :0x7fffffffd510 ("8.000000") [stack] :0x7fffffffdf70 ("8.000000")gdb-peda$ find 2001 Searching for '2001' in: None ranges Found 2 results, display max 2 items: [heap] :0x618c78 --> 0x31303032 ('2001') [stack] :0x7fffffffdfb0 --> 0x31303032 ('2001')
The strings are stored in the heap and the stack. We just need to inject the correct values and let the program do the rest to get the flag!
gdb-peda$ set {char [5]} 0x618c78 = "2893"gdb-peda$ set {char [5]} 0x7fffffffdfb0 = "2893"gdb-peda$ set {char [9]} 0x7fffffffd510 = "0.000000"gdb-peda$ set {char [9]} 0x7fffffffdf70 = "0.000000"gdb-peda$ set {char [19]} 0x7fffffffdf30 = "15/3/2015-18:25:49"gdb-peda$ c Continuing. 45acc0998f92fd333ca53aced2073e50ae694e16
This executable is protected with a password. Can you crack it?
Download: goodbye
The program asks for a password. Our goal is to find the correct one.
$ ./goodbye Please enter your password: 12345678 Sorry, that didn't work! Please try again.
Main function disassembled:
main: 400e68 mov rbp, rsp 400e6b mov edi, 0x493e0 ; argument "useconds" for method usleep@PLT 400e70 mov eax, 0x0 400e75 call usleep@PLT 400e7a mov edi, 0x400f9b ; "Please enter your password: ", argument "format" for method printf@PLT 400e7f mov eax, 0x0 400e84 call printf@PLT 400e89 mov rax, qword [ds:stdin@@GLIBC_2.2.5] ; stdin@@GLIBC_2.2.5 400e90 mov rdx, rax ; argument "stream" for method fgets@PLT 400e93 mov esi, 0x64 ; argument "size" for method fgets@PLT 400e98 mov edi, 0x601540 ; password, argument "str" for method fgets@PLT 400e9d call fgets@PLT 400ea2 mov edi, 0x601540 ; password, argument #1 for method check 400ea7 call check 400eac mov eax, 0x0 400eb1 pop rbp 400eb2 ret
The main procedure calls check, a function that processes the password provided. Let's see what it does:
check: ... 400d1d mov dword [ss:rbp+var_14], 0x5a 400d24 mov dword [ss:rbp+var_18], 0x7e ... 400d4d mov rdi, rax ; argument "s" for method strlen@PLT 400d50 call strlen@PLT 400d55 mov rdx, rax 400d58 mov eax, dword [ss:rbp+var_18] 400d5b cdqe 400d5d cmp rdx, rax 400d60 jne 0x400e54 400d66 mov ebx, 0x0 400d6b jmp 0x400dda 400d6d cmp ebx, 0x7 ; LOOP BEGIN 400d70 jg 0x400da3 400d72 movsxd rdx, ebx 400d75 mov rax, qword [ss:rbp+var_28] 400d79 add rax, rdx 400d7c movzx eax, byte [ds:rax] 400d7f movsx eax, al 400d82 sub eax, dword [ss:rbp+var_14] 400d85 mov r12d, eax 400d88 mov edi, ebx ; argument #1 for method f1 400d8a call f1 400d8f cmp r12d, eax 400d92 je 0x400dd7 400d94 mov eax, 0x0 400d99 call error 400d9e jmp 0x400e5e 400da3 movsxd rdx, ebx 400da6 mov rax, qword [ss:rbp+var_28] 400daa add rax, rdx 400dad movzx eax, byte [ds:rax] 400db0 movsx eax, al 400db3 sub eax, dword [ss:rbp+var_14] 400db6 mov r12d, eax 400db9 lea eax, qword [ds:rbx+0xfffffffffffffff8] 400dbc mov edi, eax ; argument #1 for method f2 400dbe call f2 400dc3 cmp r12d, eax 400dc6 je 0x400dd7 400dc8 mov eax, 0x0 400dcd call error 400dd2 jmp 0x400e5e 400dd7 add ebx, 0x1 ... 400ddd mov rax, qword [ss:rbp+var_28] 400de1 mov rdi, rax ; argument "s" for method strlen@PLT 400de4 call strlen@PLT 400de9 sub rax, 0x1 400ded cmp r12, rax 400df0 jb 0x400d6d ; END OF LOOP 400df6 mov edi, 0x400f52 ; "Well done! Your flag is: " ... 400e52 jmp 0x400e5e 400e54 mov edi, 0x400f70 ; "Sorry, that didn't work! Please try again." 400e59 call puts@PLT ... 400e65 pop rbp 400e66 ret
This procedure is a bit confusing, but let's try to translate it to pseudocode.
check(string s): a = 0x5a = 90; b = 0x7e = 126; ... (instructions involving variable b) ... if (len(s) == b): for i in (0, len(s)): tmp = s[i] - a; if (i < 8): if (tmp != f1(i)): print("Nice try!"); return; else: if (tmp != f2(i-8)): print("Nice try!"); return; print("Well done! Your flag is:") ... else: print("Sorry, that didn't work! Please try again.")
Guessing the correct length of the password seems a good idea. After a few attemps, the "Nice try!" message appears when the length is 16. Now, we just need to reverse f1 and f2. We can extract these functions from the binary. However, I'm going to use gdb to call the f1 and f2 using the proper arguments. But wait... If you try to run the program using gdb, you will get a segmentation fault.
$ gdb goodbyegdb-peda$ r Stopped reason:SIGSEGV 0x00007ffff776c5f8 in raise () from /usr/lib/libc.so.6
This happens because this executable has an anti-debugging protection. Fortunately, it can be bypassed using various techniques. I'm going to override ptrace, since it usually does the job.
$ echo "long ptrace(int a, int b, int c){return 0;}" > override.c $ gcc override.c -shared -fPIC -o override.so $ gdb goodbyegdb-peda$ set environment LD_PRELOAD ./override.sogdb-peda$ b maingdb-peda$ r Breakpoint 1, 0x0000000000400e6b in main ()gdb-peda$ call f1(0) $3 = 0x1d = 29gdb-peda$ call f1(1) $4 = 0xf = 15 ...
29+90 = 119 = 'w'
, 15+90 = 105 = 'i'
, and so on.
The final password is "winter_is_coming".
$ ./goodbye Please enter your password: winter_is_coming Well done! Your flag is: 77696e7465725f69735f636f6d696e67