DEF CON CTF Quals 2015 - catwestern (coding challenge 1) Writeup

This challenge asks us to solve a programming problem. The TCP service is running on port 9999. When you connect to it, it tells you:

****Initial Register State****
rax=0xef91c9bc5e06177b
rbx=0x2182d45a3d3e608c
rcx=0x6bd55e3cde9d0e55
rdx=0xdd59f3fc1c666a5e
rsi=0xbc9ec68be44252db
rdi=0x3327008906dea77c
r8=0xdca252f662c1eede
r9=0xb9b44beb597bc359
r10=0xbd6649285ef50a50
r11=0x37b751981d3582d
r12=0xfa34e739634ccb7b
r13=0x6824becdc88f4ed
r14=0xebd638495d51e14a
r15=0x6935dba2deba8f6d
****Send Solution In The Same Format****
About to send 76 bytes:

Then, it sends that many bytes of x86_64 bytecode. It is a backwards challenge! You write the shellcode-running service this time.

The register state changes each time, as does the length of the bytecode. The problem, intuitively enough, is asking us to execute the code it sends us with the initial register state, and print the register state back to it.

This is not very hard, if you know a little assembly. All you have to do is read the register state into memory, put the code somewhere executable, and do some marshalling. An observation of the code indicates that it is basically all ALU instructions, which makes it pretty safe, but I ran it in an EC2 instance anyway so I didn't even have to worry about running the solution as root.

Here is some very simple code that does this.

parse.cpp

 1 #include <stdio.h>
 2 #include <stdint.h>
 3 #include <stdlib.h>
 4 #include <string.h>
 5 #include <sys/mman.h>
 6 
 7 uint64_t regstate[14]; //a, b, c, d, si, di, 9-15
 8 const char* regnames[] = {"rax", "rbx", "rcx", "rdx", "rsi", "rdi",
 9 "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"};
10 
11 extern "C" void shellcall(void* regstate, void* shellcode);
12 
13 int main() {
14         char* foo = 0;
15         size_t foo_sz = 0;
16         size_t ri=0;
17 
18         getline(&foo, &foo_sz, stdin); //get header
19         fprintf(stderr, "Got header: %s", foo);
20 
21         while (getline(&foo, &foo_sz, stdin) > 0 && ri<14) {
22                 //fprintf(stderr, "l=%d %s\n", ri, foo);
23                 regstate[ri++] = strtoull(strstr(foo, "0x"), 0, 16);
24                 fprintf(stderr, "%s=0x%016llx\n", regnames[ri-1], regstate[ri-1]);
25         }
26 
27         getline(&foo, &foo_sz, stdin); //get header
28         fprintf(stderr, "Got header: %s", foo);
29 
30         size_t n_bytes = 0;
31         sscanf(foo, "About to send %u", &n_bytes);
32 
33         fprintf(stderr, "Expecting %u bytes\n", n_bytes);
34         uint8_t* shellcode = (uint8_t*)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
35 
36         for (int i=0;i<n_bytes;i++) shellcode[i] = fgetc(stdin);
37 
38         mprotect(shellcode, 4096, PROT_READ|PROT_EXEC);
39 
40         fprintf(stderr, "Exec shellcode: regstate at 0x%016lX  shellcode at 0x%016lX\n", (uint64_t)regstate, (uint64_t)shellcode);
41         shellcall(regstate, shellcode);
42 
43         for (int i=0;i<14;i++) fprintf(stderr, "%s=0x%016llx\n", regnames[i], regstate[i]);
44         for (int i=0;i<14;i++) printf("%s=0x%016llx\n", regnames[i], regstate[i]);
45         printf("\n");
46         fflush(stdout);
47 
48         while (getline(&foo, &foo_sz, stdin) > 0) fprintf(stderr, "%s", foo);
49 
50         return 0;
51 }

shellcall.S

 1 .section .text
 2 .globl shellcall
 3 .type shellcall, @function
 4 
 5 shellcall:
 6 // rdi = regstate, rsi = shellcode
 7 push %rbp
 8 push %rax
 9 push %rbx
10 push %rcx
11 push %rdx
12 push %rsi
13 push %rdi
14 push %r8
15 push %r9
16 push %r10
17 push %r11
18 push %r12
19 push %r13
20 push %r14
21 push %r15
22 
23 push %rsi
24 movq %rdi,%rbp
25 
26 movq 0(%rbp),%rax
27 movq 8(%rbp),%rbx
28 movq 16(%rbp),%rcx
29 movq 24(%rbp),%rdx
30 movq 32(%rbp),%rsi
31 movq 40(%rbp),%rdi
32 movq 48(%rbp),%r8
33 movq 56(%rbp),%r9
34 movq 64(%rbp),%r10
35 movq 72(%rbp),%r11
36 movq 80(%rbp),%r12
37 movq 88(%rbp),%r13
38 movq 96(%rbp),%r14
39 movq 104(%rbp),%r15
40 
41 push %rbp
42 call *8(%rsp)
43 pop %rbp
44 
45 movq %r15,104(%rbp)
46 movq %r14,96(%rbp)
47 movq %r13,88(%rbp)
48 movq %r12,80(%rbp)
49 movq %r11,72(%rbp)
50 movq %r10,64(%rbp)
51 movq %r9,56(%rbp)
52 movq %r8,48(%rbp)
53 movq %rdi,40(%rbp)
54 movq %rsi,32(%rbp)
55 movq %rdx,24(%rbp)
56 movq %rcx,16(%rbp)
57 movq %rbx,8(%rbp)
58 movq %rax,0(%rbp)
59 
60 pop %rax //discard
61 pop %r15
62 pop %r14
63 pop %r13
64 pop %r12
65 pop %r11
66 pop %r10
67 pop %r9
68 pop %r8
69 pop %rdi
70 pop %rsi
71 pop %rdx
72 pop %rcx
73 pop %rbx
74 pop %rax
75 pop %rbp
76 
77 ret

The mmap and mprotect in parse.cpp take care of putting the shellcode in an executable place.

As you can see, I am very lazy and did not want to worry about what can be clobbered or not. The only trick to this assembler function is the state marshalling. We need to set a lot of registers for the initial state, but the problem mercifully leaves RBP and RSP alone, and although the shellcode uses the stack, it only ever does push and pop, and never does base pointer dereferences at all. So, RBP is free for our use. I put the location of my register state array in RBP and proceed to set up the registers. The call address is placed on the stack (via push rsi), and we save RBP across the shellcode call even though it isn't likely necessary. Then, we just call the saved shellcode pointer, and run that.

The shellcode that gets sent to us (yes, it isn't strictly shellcode, just code) always ends in a ret. So, we are very safe. Just wait for it to ret, save off state, pop all our saved state off the stack, and return into parse.cpp.

Then, we can print the register state back to the service, in the same format as before, and it will tell us the flag. Here is a transcript of my session:

[root@ip-172-31-23-134 ec2-user]# socat TCP4:catwestern_631d7907670909fc4df2defc13f2057c.quals.shallweplayaga.me:9999 EXEC:./a.out
Got header: ****Initial Register State****
rax=0x9f9da7286ac5281e
rbx=0x92fbcd1edafe5bb2
rcx=0xf34f594b5cc1e0b2
rdx=0x1b1c24508b7cb519
rsi=0x902a02cae9a0e63f
rdi=0xc56beb738c5b1b11
r8=0xd13193869ca5ad2a
r9=0x41e6fc66e96f9cd6
r10=0x9741e20d00176ab2
r11=0x906cd1136b84cc21
r12=0xa0bb2b7f459a649d
r13=0x54188af1909e1eb7
r14=0x603511b2ade4e546
r15=0x87a16908441ab303
Got header: About to send 87 bytes:
Expecting 87 bytes
Exec shellcode: regstate at 0x00000000006014A0  shellcode at
0x00007FE0236EA000
rax=0x9f9da7286ac5281e
rbx=0xe562c9d8e0e6f14b
rcx=0x0cb0a6b4a33e1f4d
rdx=0x1b1c24508b7cb519
rsi=0xa0bb2b7f459a649c
rdi=0x000000001947faf8
r8=0x2ece6c797ffef2f6
r9=0x41e6fc66e96f9cd6
r10=0xaadb2fb59e405196
r11=0xf143280600000000
r12=0x000000001947804c
r13=0xa7571845f5f58250
r14=0x603511b2a44847da
r15=0x87a16907fe758657
The flag is: Cats with frickin lazer beamz on top of their heads!
[root@ip-172-31-23-134 ec2-user]#

Posted on May 18, 2015, 3:06 p.m. by FalconK