Yawa
Prologue¶
Difficulty: beginner
Category: binary exploitation
Solved: 184
Description
Yet another welcome application.
Input files:
yawa.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
NB:
-
Following indices bases system is used to avoid ambiguity. Whenever element of a collection is referenced by number, 0-based index implied.
Ie, element
0
of list[1, 2, 4, 8, 16]
is1
, Element3
is8
.When element is reference in explanation with word (first, third...), 1-based system is implied.
Ie, first character of string
Hello World!
isH
, fifth iso
.
- Solution code was redacted for readability purposes. Due to time pressure during the competition I was using a lot of one-letter variables and questionable code structure.
- I am using gdb with pwndbg plugin
My struggle¶
Task includes binary(with libc library and ld linker to run it) and source code. First thing I review source code to build a plan.
There is no win()
function or equivalent so we likely have to get RCE (shell or similar). Source code is so short that it don't
take long to find a bug in code that we can exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Variable name
is a buffer of 88 characters, but read(0, name 0x88)
reads up to 0x88 = 136 characters. Means we can overflow
stack and get code execution.
Now that we know what to do lets check what type of binary we have and what types of protection are enabled:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
So, we have full package:
- Canary stack protection - stack is protected from overwrites;
- NX enabled - stack is not executable;
- PIE enabled - every time you run the file it gets loaded into a different memory address.
If you are not familiar with any of above techniques and would like to learn about them (I am going to briefly touch them, but there is goal to include in-detail explanation of each of the techniques in this writeup) I recommend reading following gitbook notes: https://ir0nstone.gitbook.io/notes. This is by a long shot the best resource I have seen on internet on the topic both quality of explanation and completeness of content is superb.
Next I use pwninit to be able to run binary on my machine without need to juggle with environment variable paths etc:
1 |
|
Exploit algorithm:
- Leak canary value from stack to bypass canary protection;
- Leak libc address to find system call;
- Use buffer overflow to get remote shell and obtain flag.
Here is sample memory layout of stack that we will be working with:
Memory layout | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Variable name is stored at address 0x7fffffffdc60 (line 1).
Canary value is stored at address 0x7fffffffdcb8 (line 12), it always ends with 0x00 on linux (remember - its little endian). Therefore, its value is 0x4b6f9c3c15608700.
At address 0x7fffffffdcb8 we see some 8byte long number 1 (not sure what it is - I've left it untouched in my work).
At address 0x7fffffffdcc8 (line 14) we see return address from the main function (0x00007ffff7c29d90), here we want to place jump to system call.
Leak canary value¶
Canary protection (read more here) puts a random value on stack before execution and checks if hasn't been modified while function was running (if it is - program exits).
If we want overwrite return address at 0x7fffffffdcc8, by overflowing variable name
at 0x7fffffffdc60,
we will have overwrite canary address at 0x7fffffffdcb8 as we can only write continues block of memory.
Therefore, we will have to leak canary value from stack and when we overflow buffer put exactly same bytes in the same place
to prevent canary protection from triggering.
To leak address we can take advantage of "print your name functionality":
1 2 3 |
|
%s
modifier of printf
prints all bytes starting from address name
until it reaches nullbyte. So if we put 'a' from
0x7fffffffdc60 till 0x7fffffffdcb8, it is going to print aaaaa...
and won't stop just on 'a' (as there is no
nullbyte) it will continue printing up until 0x7fffffffdcc0 (including entire canary value) and then stop:
leak canary block | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Leak libc address¶
Next step is to find base virtual address of libc, so we can find system
call. Address on stack of return address from our
main function is address of __libc_start_call_main
- as you might guess its inside libc library. To read address of the function, we can use same technique as
we used previously to reading canary value. When library is loaded into memory, its loaded as a
single blob, so even though virtual location will be different each time, blob content is always same. Therefore we can calculate
position of elements inside the library relative to each other and it won't change doesn't matter where library is loaded:
1 2 3 4 |
|
system
function will be always 0x50d70-0x29d10 = 0x27060
bytes apart from __libc_start_call_main
function.
For our our sample memory dump we saw return address 0x7ffff7c29d90 - we return to the middle of libc_start_call_main
,
so we can infer that libc library is mapped to address 0x7ffff7c29d90 - 0x29d90 = 0x7ffff7c00000
(segments are typically round numbers).
So system
address is 0x7ffff7c00000 + 0x50d70 = 0x7ffff7c50d70
.
We can also double check our calculations by checking process memory mapping using gdb or cat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Code snippet to get libc base address:
leak libc base address | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Remote shell¶
With canary value and libc address in hands we are ready to get remote shell. Given there is NX protection and stack code
is not executable we will use ROP (more on ironstone) to execute return to libc technique.
First we need to get address of string "/bin/sh" in libc library (system
function takes argument what to launch):
1 2 |
|
Using ROPgadget we find tons of useful gadgets in libc library:
1 |
|
- 0x000000000002a3e5: pop rdi
- 0x00000000000baaf9: xor rax, rax, ret
With all of this our payload will look like:
- 88 bytes of padding for buffer
- 8 bytes of canary value
- 8 bytes value of 1 - unchanged
- 8 bytes pop rdi gadget
- 8 bytes address of "/bin/sh" string (this is argument for pop rdi gadget). As a result we set register RDI to /bin/sh
- 8 bytes xor rax, rax, ret - this is NOP operation for stack alignment
- 8 bytes address for
system
call
Full python script:
solve.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
Epilogue¶
- Official website: https://downunderctf.com/
- Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public