Analysis of the bug =================== The cycle at lines 61--67 can read past the end of `argv[2]` if the last character of the string is a backslash. This is because scan is advanced at line 63, and then again at the end of the for-loop, and only then is the check for `\0` performed. The null byte that terminates `argv[2]` is copyied into `buf`, and then the loop continues to copy bytes after the end of `buf` (allocated at line 48), up to the next null byte. Attack plan =========== Since `buf` is allocated in the heap just before `c`, it is very likely that the two chunks are contiguous (in this simple example there are no doubts about it, since there are no other allocations in the program). We can then exploit the overflow to overwrite the `c->fn` field with 2, thus resulting in a call to `system(c->arg)`, where `c->arg` is just `argv[2]` with any backslash removed. To control the bytes that overwrite `c` we can exploit the fact that the program continues even if it receives more than the required two arguments (lines 44--46). Attack implementation ===================== ``` sh export PYTHONIOENCODING=iso-8859-1 ./mycmd 0 'sh\' $(python3 -c 'print("A"*(32-3) + "\x02")') ``` The first argument goes into `c->fn`, but we are going to overwrite it, so it can be anything. The second argument has two purposes: it must terminate with a backslash in order to trigger the bug, and it must contain the string that, after, backslash removal, will be passed to `system()`. In this case we can just execute `sh`, or `/bin/sh` if we are not sure about the contents of `PATH`. The third argument contains some padding bytes until we reach the `c->fn` field, which we overwrite with 2 (`FN_SYSTEM`). To compute the required number of padding bytes we can run the program in a debugger, or we can observe that, since `strlen(argv[2]) + 1` is 4, which is smaller than 16, `malloc()` will allocate 16 bytes for `buf` at line 69 (the minimum allocation size for 64b systems), immediately followed by the 16-byte header of the chunk containing `c` (where the first 8 bytes of the header are actually usable by the previous chunk), followed by `c`. Therefore, we need to skip 32 bytes, minus the length of `sh\0`. Suggested fix ============= We should first state what we want to happen when there is a backslash at the end of the string. Since that backslash is not escaping anything, it probably makes sense to interpret it literally and leave it alone, i.e., not remove it from the string. This can be accomplished by replacing line 62 with ``` c if (*scan == '\\' && *(scan + 1) != '\0') ``` Further reading =============== This exercise was inspired by a recently discovered vulnerability in `sudo`. You can read the details here: https://lwn.net/Articles/844789/