This post was originally posted on 12/08/2009.
It was last Friday. We (Plaid parliament of Pwning) took the 4th place in iCTF 2009. This year's iCTF was novel, because it was not attack and defense. Instead, thousands of bots were running on UCSB, and they were connecting to us according to the search rank in the web search engine they provided. All the bots were using more than 15 different versions of browsers including Perl, Python, Erlang (omg...), Java, and C++.
Since my role was binary analysis and CTF attack, I could have looked almost every browser sources. Especially, C++ version of browsers were interesting, and we were the only team who could have found all the c++ browsers' vulnerabilities during the competition.
Basically, we could get a shell using several overflow techniques for all the c++ binaries. Here, I will present a walk-through for the crefox-1.0 problem, which is the first-level c++ problem.
The most interesting part in this problem is that it uses
dlopen function. Thus, at first glance, we thought the program uses "safe_printf" function from a certain library. However, this was just a trick! Let's look at the source code below.
int
print_func(const char *fmt, ...)
{
void *h;
/* lets retrieve the safe printf implementation from the library */
int (*f)(const char *, ...);
if (!strncmp("USESAFEPRINTFUNCTIONA", fmt, 21)) {
h = dlopen(NULL, RTLD_LAZY);
if (!h) {
errnonf("dlopen: %s\n", dlerror());
return -1;
}
f = (int (*)(const char *, ...))dlsym(h, "safe_printf");
f("%s\n", fmt);
}
else
/* ok, lets follow the user will and revert to the unsafe printf :-( */
printf("%s\n", fmt);
return 0;
}
The line of strncmp function is the most tricky part in this problem !! Note that they first check some weird string and if it matches, it will load a function called "
safe_printf". However, the returned function from
dlsym is NULL here !
So what will happen when the function pointer
f is called?
The instruction pointer will go to the address of 0x00000000. So, here, we expect the seg-fault. Right?
However, the program will not terminate. Why? Let's look at the previous part of the source code before the
print_func function is called. The most important part is shown below.
ptr = mmap(NULL, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON|MAP_FIXED, -1, 0);
So, they used
mmap to allocate memory at the address zero! :D
If you remember that this is a browser's source code, our web server's page will be mapped to the address pointer using mmap function, only when the page starts with the string USESAFEPRINTFUNCTIONA. Thus, the instruction pointer will go the address zero, where our page source code resides in. And the source code should start with the specific string.
Now, the secret of the weird string of
strncmp is unveiled. They were just a simple instruction, and even though the program flow goes to the string's address, the program will not terminate. Note that mmap used PROT_EXEC option to run the code.
Okay, so what will be the instruction for the string USESAFEPRINTFUNCTIONA ?
0x0: push %ebp
0x1: push %ebx
0x2: inc %ebp
0x3: push %ebx
0x4: inc %ecx
0x5: inc %esi
0x6: inc %ebp
0x7: push %eax
0x8: push %edx
...
They are just push and increment instructions ! Okay, that makes sense right? So the next step is really simple. We only need to put our shell code right after the string USESAFEPRINTFUNCTIONA. In this way, a web browser who visits our website (including the string USESAFEPRINTFUNCTIONA and shellcode in the page) will run our shellcode, and connect to our web server.
All the binary problems were really intriguing. Look at the entire source code that I attached below, and feel the trickiness of this problem. Actually, the next two versions are more tricky. I will explain them later. :D
Full source code:
/*
* iCTF Crefox browser
*
* Lorenzo ``Gigi Sullivan'' Cavallaro <sullivan@cs.ucsb.edu>
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdarg.h>
#include <errno.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <curl/curl.h>
#include "list.h"
#include <iostream>
#include <string>
using namespace std;
#include <htmlcxx/html/ParserDom.h>
using namespace htmlcxx;
#define MAXINPUT 8192
#define PROMPT "COMMAND>"
#define COOKIEFILE "./.cookiejar.txt"
#define VERSION "1.0.1"
#define UA "Crefox-" VERSION " (+http://ictf.cs.ucsb.edu/)"
extern char *optarg;
extern int optind, opterr, optopt;
extern char *__progname;
struct opts
{
char *proxy;
char *prompt;
char plugins;
};
struct page
{
char *memory;
size_t size;
void *mmap;
size_t mmap_size;
};
struct plugins
{
void *h;
char *name;
struct list_head list;
};
CURL *browser_init(void);
void browser_shutdown(int, CURL *) __attribute__((noreturn));
void parse_options(int, char **, struct opts *, void *);
void split(char **, char *, int);
int uniq(char **, char *, char);
char **parse_input(char *, int *);
size_t map_page(void *, size_t, size_t, void *);
void free_args(char **);
struct plugins *loadlib(const char *);
int load_plugins(struct plugins *);
void unload_plugins(struct plugins *);
int print_func(const char *, ...);
int safe_move(struct page *);
int extract_tags(char *, char *, char *, int (*)(void *));
int print_href(void *);
void __errf(int, const char *, ...) __attribute__((noreturn));
void __errnonf(const char *, ...);
#define errf(x, fmt, args...) \
do { \
fprintf(stderr, "ERROR:%s:(fatal):", __progname), __errf(x, fmt, ##args); \
} while (0)
#define errnonf(fmt, args...) \
do { \
fprintf(stderr, "ERROR:%s:(non fatal):", __progname), __errnonf(fmt, ##args); \
} while (0)
#define debug(args...) \
do { \
fprintf(stderr, "DEBUG:%s:", __progname), fprintf(stderr, ##args); \
} while (0)
int
main(int argc, char **argv)
{
char errbuf[CURL_ERROR_SIZE], quit;
CURL *browser;
int res;
struct opts opts;
struct page page;
struct stat sbuf;
struct plugins plugins;
int (*output_page)(const char *, ...);
browser = browser_init();
if (!browser)
errf(1, "browser_init()\n");
memset(&page, 0, sizeof (page));
memset(&opts, 0, sizeof (opts));
parse_options(argc, argv, &opts, browser);
INIT_LIST_HEAD(&plugins.list);
res = load_plugins(&plugins);
if (res != -1) {
struct list_head *l;
struct plugins *p;
int i = 0;
opts.plugins++;
list_for_each(l, &plugins.list) {
p = list_entry(l, struct plugins, list);
debug("plugin[%i]:%s\n", i++, p->name);
}
}
else {
errnonf("plugins not successfully loaded\n");
}
if (opts.proxy) {
curl_easy_setopt(browser, CURLOPT_PROXY, opts.proxy);
debug("proxy: %s\n", opts.proxy);
}
curl_easy_setopt(browser, CURLOPT_USERAGENT, UA);
curl_easy_setopt(browser, CURLOPT_REFERER, "http://ictf.cs.ucsb.edu");
curl_easy_setopt(browser, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(browser, CURLOPT_ERRORBUFFER, errbuf);
if (!stat(COOKIEFILE, &sbuf))
curl_easy_setopt(browser, CURLOPT_COOKIEFILE, COOKIEFILE);
curl_easy_setopt(browser, CURLOPT_COOKIEJAR, COOKIEFILE);
curl_easy_setopt(browser, CURLOPT_WRITEFUNCTION, map_page);
curl_easy_setopt(browser, CURLOPT_WRITEDATA, &page);
output_page = &print_func;
quit = 0;
while (!quit) {
char in[MAXINPUT];
int num, i;
char **args;
memset(in, 0, MAXINPUT);
memset(errbuf, 0, sizeof(errbuf));
printf("%s", opts.prompt);
if (!fgets(in, MAXINPUT, stdin)) {
if (!errno) {
fprintf(stderr, "\n");
errnonf("Please use 'q' to quit the browser\n");
continue;
}
errnonf("fgets(): error: %d %s\n", errno, strerror(errno));
break;
}
args = parse_input(in, &num);
if (!args) {
errnonf("parse_input()\n");
continue;
}
if (num > 1)
curl_easy_setopt(browser, CURLOPT_URL, args[1]);
if (!strcmp(args[0], "u")) {
long code, redir;
if (num == 2 && args[1]) {
for (i = 0; i < num; i++)
debug("args[%d]: %s\n", i, args[i]);
if (page.memory) {
free(page.memory);
memset(&page, 0, sizeof (page));
}
debug("Getting URL: %s\n", args[1]);
curl_easy_setopt(browser, CURLOPT_POST, 0);
if (curl_easy_perform(browser))
errnonf("URL retrieval: %s\n", errbuf);
else {
curl_easy_getinfo(browser, CURLINFO_RESPONSE_CODE, &code);
debug("HTTP status: %ld\n", code);
curl_easy_getinfo(browser, CURLINFO_REDIRECT_COUNT, &redir);
debug("HTTP redirect #: %ld\n", redir);
}
if (page.memory && code == 200) {
if (safe_move(&page) == -1) {
errnonf("safe_move failed (no output can be generated)\n");
continue;
}
if (output_page((const char *)page.mmap) == -1)
printf("%s\n", (const char *)page.mmap);
memset(page.mmap, 0, page.mmap_size);
munmap(page.mmap, page.mmap_size);
}
}
else
errnonf("malformed 'u' request\n");
free_args(args);
continue;
}
if (!strcmp(args[0], "p")) {
memset(errbuf, 0, sizeof (errbuf));
for (i = 0; i < num; i++)
debug("args[%d]: %s\n", i, args[i]);
if (num == 3 && args[1] && args[2]) {
unsigned long code;
if (page.memory) {
free(page.memory);
memset(&page, 0, sizeof (page));
}
curl_easy_setopt(browser, CURLOPT_POST, 1);
curl_easy_setopt(browser, CURLOPT_POSTFIELDS, args[2]);
debug("Posting to URL: %s with data: %s\n", args[1], args[2]);
if (curl_easy_perform(browser))
errnonf("URL retrieval: %s\n", errbuf);
else {
curl_easy_getinfo(browser, CURLINFO_RESPONSE_CODE, &code);
debug("HTTP status: %ld\n", code);
}
if (page.memory && code == 200)
printf("%s\n", page.memory);
}
else
errnonf("malformed 'p' request\n");
free_args(args);
continue;
}
if (!strcmp(args[0], "l")) {
int res;
res = extract_tags(page.memory, (char *)"a", (char *)"href", print_href);
if (res == -1)
errnonf("error while parsing/retrieving for <a> tags\n");
free_args(args);
continue;
}
if (!strcmp(args[0], "q")) {
if (num != 1)
errnonf("malformed 'q' request\n");
else
quit = 1;
free_args(args);
continue;
}
errnonf("malformed input\n");
}
unload_plugins(&plugins);
exit(0);
}
int
extract_tags(char *page, char *tagstr, char *attr, int (*callback)(void *))
{
string s_page;
HTML::ParserDom parser;
tree<HTML::Node> dom;
tree<HTML::Node>::iterator it;
tree<HTML::Node>::iterator end;
std::pair<bool, std::string> tag;
char lcase_tag[strlen(tagstr) + 1];
unsigned int i;
if (page)
s_page = string(page);
else {
errnonf("zero-length or non existing page\n");
return -1;
}
dom = parser.parseTree(s_page);
it = dom.begin();
end = dom.end();
memset(lcase_tag, 0, sizeof(lcase_tag));
for (i = 0; i < strlen(tagstr); i++)
lcase_tag[i] = tolower(tagstr[i]);
for (; it != end; ++it) {
if (it->tagName() == lcase_tag) {
it->parseAttributes();
tag = it->attribute(attr);
if (tag.first)
(void)callback(&tag.second);
}
}
return 0;
}
int
print_href(void *value)
{
cout << *(std::string *)value << endl;
return 0;
}
int
safe_move(struct page *page)
{
void *ptr;
unsigned int size = (page->size + 4096) & ~4095;
int serrno = errno;
errno = 0;
ptr = mmap(NULL, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON|MAP_FIXED, -1, 0);
if (errno != 0) {
errno = serrno;
printf("errno: %d %s\n", errno, strerror(errno));
return -1;
}
errno = serrno;
memset(ptr, 0, size);
memcpy(ptr, page->memory, strlen(page->memory));
page->mmap = ptr;
page->mmap_size = size;
return 0;
}
int
print_func(const char *fmt, ...)
{
void *h;
/* lets retrieve the safe printf implementation from the library */
int (*f)(const char *, ...);
if (!strncmp("USESAFEPRINTFUNCTIONA", fmt, 21)) {
h = dlopen(NULL, RTLD_LAZY);
if (!h) {
errnonf("dlopen: %s\n", dlerror());
return -1;
}
f = (int (*)(const char *, ...))dlsym(h, "safe_printf");
f("%s\n", fmt);
}
else
/* ok, lets follow the user will and revert to the unsafe printf :-( */
printf("%s\n", fmt);
return 0;
}
struct plugins *
loadlib(const char *n)
{
struct plugins *p;
p = (struct plugins *)calloc(1, sizeof(*p));
if (!p)
return NULL;
p->name = strdup(n);
p->h = dlopen(n, RTLD_NOW);
if (!p->h) {
free(p);
return NULL;
}
return p;
}
int
load_plugins(struct plugins *phead)
{
struct plugins *p;
int res = 0;
p = loadlib("libget.so");
if (p)
list_add(&(p->list), &(phead->list));
else
res = -1;
p = loadlib("libpost.so");
if (p)
list_add(&(p->list), &(phead->list));
else
res = -1;
p = loadlib("liblink.so");
if (p)
list_add(&(p->list), &(phead->list));
else
res = -1;
return res;
}
void
unload_plugins(struct plugins *phead)
{
struct list_head *l, *n;
struct plugins *p;
list_for_each_safe(l, n, &(phead->list)) {
p = list_entry(l, struct plugins, list);
list_del(l);
(void)dlclose(p->h);
free(p->name);
free(p);
}
return;
}
void
free_args(char **args)
{
char **p = args;
for (; *p; p++)
free(*p);
free(args);
}
CURL *
browser_init(void)
{
setbuf(stdout, NULL);
setbuf(stderr, NULL);
if (curl_global_init(CURL_GLOBAL_SSL))
return NULL;
return curl_easy_init();
}
void
browser_shutdown(int exitcode, void *arg)
{
CURL *br = arg;
curl_easy_cleanup(br);
curl_global_cleanup();
exit(exitcode);
}
void
parse_options(int argc, char **argv, struct opts *opts, void *br)
{
char opt;
if (on_exit(browser_shutdown, br))
return;
while ((opt = getopt(argc, argv, "x:p:")) != -1) {
switch (opt) {
case 'p':
opts->prompt = strdup(optarg);
break;
case 'x':
opts->proxy = strdup(optarg);
break;
default: /* '?' */
fprintf(stderr, "Usage: %s [-p prompt] [-x proxy]\n", argv[0]);
exit(2);
}
}
if (!opts->prompt)
opts->prompt = strdup(PROMPT);
return;
}
void
__errf(int code, const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
vfprintf(stderr, fmt, va);
va_end(va);
exit(code);
}
void
__errnonf(const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
vfprintf(stderr, fmt, va);
va_end(va);
return;
}
int
uniq(char **out, char *inarg, char delim)
{
int len, s = 0, i = 0;
char *in, *p, *start, *end;
if (!inarg)
return 0;
start = inarg;
end = inarg + strlen(inarg) - 1;
/* skip heading delim */
while (*start == delim) start++;
/* skip trailing ones */
while (*end == delim) *end-- = 0;
in = start;
len = strlen(start) + 1;
*out = (char *) malloc (len * sizeof (char));
if (!*out)
return -1;
memset(*out, 0, len * sizeof (char));
p = *out;
while (*in) {
if ((*in == delim)) { /* found a delim? */
if (!s) { /* never seen it so far? */
i++; /* keep the real count */
*p++ = *in++;
s = 1; /* record it */
}
else
in++; /* already seen? skip ahead */
}
else {
/* no delim so straight copy */
*p++ = *in++;
s = 0;
}
}
/* counts args[0] as well */
return ++i;
}
void
split(char **arg, char *buf, int num)
{
char *tmp, *p;
int i = 0;
/* make a safe copy of buf since strsep will mangle it */
tmp = (char *) strdup(buf);
do {
p = (char *) strsep(&tmp, " ");
if (!tmp && !i)
p = buf;
arg[i++] = (char *)strdup(p);
p = tmp;
} while (tmp);
arg[i] = NULL;
free(tmp);
return;
}
char **
parse_input(char *in, int *num)
{
char **args, *stripbuf;
int x;
while (strlen(in) > 0 && (in[strlen(in) - 1] == '\n' || in[strlen(in) - 1] == '\r'))
in[strlen(in) - 1] = 0;
*num = uniq(&stripbuf, in, ' ');
x = *num;
args = (char **) malloc((x + /* nil term */1) * sizeof (char *));
memset(args, 0, (x + 1) * sizeof (char *));
/*
* we split buf into chunks delimited by ' ': these represent cmd
* and cmd args. Due to the nature of split, we don't care about
* ' " ` ; and so on
*/
split(args, stripbuf, x + 1);
free(stripbuf);
return args;
}
size_t
map_page(void *ptr, size_t size, size_t nmemb, void *data)
{
size_t realsize = size * nmemb;
struct page *mem = (struct page *)data;
char *tmp;
if (realsize) {
tmp = (char *)malloc((mem->size + realsize + 1) * sizeof(char));
if (!tmp)
return CURLE_WRITE_ERROR;
memset(tmp, 0, mem->size + realsize + 1);
if (mem->size) {
memcpy(tmp, mem->memory, mem->size);
free(mem->memory);
}
memcpy(&tmp[mem->size], ptr, realsize);
mem->size += realsize;
tmp[mem->size] = 0;
mem->memory = tmp;
}
return realsize;
}