Playing Hacks and Stuffs!
This challenges were made by @mug3njutsu and I really enjoyed solving it :)
Attached file: link
We are given a zip file which contained a binary, libc and ld file when unzipped
First thing I did was to patch the binary using pwninit so as to make sure the binary uses the same libc as the remote instance does
Now I checked the file type and protections enabled on it
So we’re working with a x64 binary which is dynamically linked and not stripped
And from the result gotten from running checksec
we can see that all protections are enabled!
I ran the binary to get an overview of what it does
Hmmmm it seems to receive our input and prints it out back
To find the vulnerability I decompiled the binary in Ghidra and here’s the main function
undefined8 main(void)
{
long in_FS_OFFSET;
char choice [5];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
setup();
write(1,&DAT_00100b70,0xbb);
read(0,choice,5);
if (choice[0] == '1') {
puts("Go back to where you came!");
/* WARNING: Subroutine does not return */
exit(0);
}
if (choice[0] == '2') {
question();
write(1,"You wanna tell me a little bit more about pointers?(y/n): ",0x3a);
read(0,choice,5);
if (choice[0] == 'y') {
question();
}
else if (choice[0] == 'n') {
puts("Cheers mate!!");
}
else {
puts("It\'s either yay or nay :)");
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
puts("It\'s either 1 or 2 :)");
/* WARNING: Subroutine does not return */
exit(0);
}
So…. if we choose the first option it would just exit which is not so helpful
And the next choice is option 2 which would call the question()
function
Here’s the pseudo code for the function
void question(void)
{
long in_FS_OFFSET;
char buffer [136];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
memset(buffer,0,0x80);
write(1,"Educate me, what\'s so interesting about pointers: ",0x32);
read(0,buffer,256);
printf(buffer);
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
What this does is basically:
Looking at the code they are two obvious vulnerability:
There’s a buffer overflow because it reads in at most 256 bytes of input which is stored in a buffer that can only hold up 136 bytes giving us an extra 120 bytes to write. And the format string bug exists because it prints out our input without using a format specifier
So what now?
Well after the function is called we are given the choice to call it again
write(1,"You wanna tell me a little bit more about pointers?(y/n): ",0x3a);
read(0,choice,5);
if (choice[0] == 'y') {
question();
}
else if (choice[0] == 'n') {
puts("Cheers mate!!");
}
What next? How do we go about exploitation!
Because FULL RELRO
is enabled this means the global offset table is just read only which we can confirm by looking at the memory mapping in gdb
The values in the got are in that binary address range which has just read
permission set on it, so Got Overwrite
isn’t liable
What next?
Well since there’s a buffer overflow we can just overwrite the instruction pointer to jump to a one_gadget
The catch there is that Canary is watching 👀 which would prevent us from doing a stack based overflow
But that isn’t an issue because we can use the format string bug to leak the canary
The idea of canary is simple in the sense that it would generate a random value which would be stored on the stack and later compared before the program returns
So the canary is placed after our input buffer meaning if we do an overflow it would overwrite the value stored in the canary and when the comparism which checks if the canary still has it’s right value is done at the time the program wants to return it would return False
because we have overwritten it therefore it calls the __stack_chk_fail
function
The way to go around this is to overwrite the canary to it’s right value this is going to be possible because we can leak the canary via the format string bug
One thing about canary which can be used to identify it is that it ends with a null byte 00
Now let’s do the good stuff :)
The exploit chain is simple:
To do the leak I wrote a fuzz script which basically leaks values off the stack and shows me it’s offset
Here’s the fuzz script: link
Running it gives this
From the result being leaked off the stack we can see that at offset 23 hold the canary value which we can confirm it being right
And at offset 29 holds some libc address value
If we have a libc leak we can calculate the libc base address by subtracting it with (known leak - known libc base)
At this point we should have the libc base address now we need to make use of the buffer overflow to overwrite the RIP to a one gadget
That’s all!
So the final thing is to overwrite the canary with it’s original value and you can easily get the offset from looking at Ghidra stack layout
The offset to reach the canary is 0x98 - 0x10 = 0x88
, then we need to overwrite saved rbp with random values since it’s not going to be later used and then finally the return address to the one gadget address
Here’s the final exploit script: link
Running it would spawn a shell
Attached file: link
We are given a zip file which contained a binary, libc and ld file when unzipped
I basically followed the same process I did in this first binary challenge (patching, file type & protection)
But later on you will see there’s no need to patch the binary to use the remote libc it’s just a good practice I guess 🤔
Checking the file type and the protection enabled on the binary shows this
So we’re working with a x64 binary which is dynamically linked and not stripped
The only protection not enabled is “Canary”
I ran the binary to get an overview of what it does
So it’s clear that it would receive our input twice while the first one prints our input back and the second one just receives our input and exit
On decompiling the binary with Ghidra here’s the main function
undefined8 main(void)
{
undefined8 buffer;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
code *notcalled;
undefined8 *ptr;
setup();
ptr = (undefined8 *)mmap((void *)0x999999000,0x1000,3,0x21,-1,0);
notcalled = ::notcalled;
fwrite("Tell me, what\'s your strategy here: ",1,0x24,stdout);
read(0,&buffer,64);
printf("Riiiiight, %s",&buffer);
*ptr = buffer;
ptr[1] = local_50;
ptr[2] = local_48;
ptr[3] = local_40;
ptr[4] = local_38;
ptr[5] = local_30;
ptr[6] = local_28;
ptr[7] = local_20;
fwrite("This might actually come to fruition. Try fire it up: ",1,0x36,stdout);
fgets((char *)&buffer,200,stdin);
return 0;
}
Here’s what it does:
0x999999000
of size 0x1000
with PROC_READ & PROC_WRITE
permissionLooking at this we can tell there’s a buffer overflow during the time it receives our input the second time
But we can’t do anything for now because PIE is enabled
There’s a way to leak it and that’s through using printf since it receives 64 bytes of our input, and the thing about printf is that it would print until receiving a null byte
Looking at the stack after it receives the first input I saw this
So if we are to fill up the buffer it would overwrite the null byte then printf would leak that notcalled
function address
And the amount of bytes to fill up the buffer is 64 before we can leak the notcalled
function address
At this point we know there’s a way to leak an elf section addresses therefore having the elf base address
This chall so far is somewhat similar to the one I solved at Cyberlympics 2023 Prequal here’s the writeup: link
But the difference now is that I was able to ROP using Ret2Libc in that challenge but it doesn’t work here?
Since printf
is being used, normally it can be used to leak the got values but I tried to see what the value is during runtime and I got this
Wow pwntools is saying there’s no attribute called got
which means there’s no resolved libc function during the execution?
I then confirmed by running that script in a debugger and on checking the got shows this
We can clearly see that no any resolved libc function is there which means we can’t possibly leak libc address
At this point what next?
Since this is mug3njutsu
I had it in mind that this is solvable even though it starts looking hard (but the challenge name is simple
🥲)
So I decided to check the available rop gadgets present in the binary
Looking at that I could clearly see a pop rax & syscall
gadget
With this we can potentially call execve('/bin/sh', NULL, NULL)
But we need three gadget to set up the argument in the register to achieve that
I saw an easy [pop rdi; ret] & [pop rsi, pop r15; ret]
gadget but that wasn’t the case for rdx
There’s no gadget that allows us control the rdx
register
But looking at other gadgets I came across:
Ok this is worth using cause when you xor a register with it’s register it will null out the register
So at this point we have a way to set rsi & rdx
to 0
and now we need to set rdi --> addr --> '/bin/sh'
And yes to pass in /bin/sh
to the rdi
register we need to pass it as an address pointing to /bin/sh
and not a string
They wasn’t any mov
gadget to gives us the write-what-where
primitive so I had to come up with another way of using /bin/sh
already in memory
I ran the binary in gdb and after it receives our first input I searched for it and it happens to be stored in the virtual memory created
That’s so because the buffer content is going to be stored in the virtual memory space created:
ptr = (undefined8 *)mmap((void *)0x999999000,0x1000,3,0x21,-1,0);
*ptr = buffer;
Because the address 0x999999000
is always going to be the same this means we have a way of writing ‘/bin/sh’ to memory
The way I went about writing to memory is by using the last 8 byte address 0x999999038+1
So instead of me spamming with ‘A’s like I’d normally do for the leaking part I actually spammed it with a space character:
- payload = ' '*57 + '/bin/sh'
This is to make sure that the address would just have /bin/sh
and no other messy characters
At this point we have a way to set all register and now the rop chain should work
Here’s my final exploit: link
Attached file: link
Only a binary was given and checking the file type and protection enabled I got this
So we’re working with a x64 binary which is statically linked and not stripped
The protections enabled are the standard mug3njutsu
protection but something interesting is that it has RWX segments basically that means during the program execution the stack would be readable, writeable and executable
And yes by the way it’s a Statically linked binary meaning that all C functions are going to be in the binary rather than it being called from a libc library file
Then I ran the binary to get an overview of what it does
Nice it prints out some fancy keyboard banner then receives our input and seems to exit
Next thing I did was to decompile the binary in Ghidra and it generously directs us to to main function
undefined8 main(void)
{
setup();
junk();
chall();
if (x != 0xcc07c9) {
notcalled();
}
return 0;
}
So basically it calls the setup()
function which does some buffering
Next it calls the junk()
function which display the fancy keyboard design
And finally the chall()
function
void chall(void)
{
undefined buffer [48];
write(1,
"Gotta say, coming up with a whole backstory for a chall is hard! Anyway, what\'s your appro ach here: "
,100);
read(0,buffer,256);
return;
}
So it will display some words then receive our input using read()
which will be stored in a buffer that can only hold up 48 bytes but we are given at most 256 bytes to read in
Therefore there’s a buffer overflow in this function
Then after the program returns it does this comparism of the global variable x
to 0xcc07c9
The value stored there is already 0xcc07c9
meaning the comparism would return False
But if it was to be different it would call the notcalled()
function
void notcalled(void)
{
undefined shellcode [8];
write(1,"You managed to get here, awesome! Can i please have your autograph: ",0x44);
read(0,shellcode,8);
(*(code *)shellcode)();
return;
}
And what this function will basically do is to run a 8 bytes shellcode received from us
We can’t really reach the function for now because there’s no way of overwriting the global variable x
to another value
I’m thinking it’s there so as for us to see there’s a function which isn’t called by main directly
But the fact we can’t overwrite the global variable doesn’t mean we can’t call the function cause after all there’s a buffer overflow?
Hmmmm but remember that PIE is enabled meaning that the binary memory address would be different each time the program runs
So I decided to see how it looks like in a debugger when I overwrite the instruction pointer
Ok cool the offset is 56 now I decided to look at the way the overwrite works and saw this
That’s nice we can see that it overwrote the last 2 bytes of the rip which basically means overwriting would start from the end of the address (lsb or ??)
In our case we can see 0xa4141
the 0xa
is caused when i entered the “Enter” key
At this point we can basically call the notcalled
function but how?
The address of the function is always going to be different because of PIE but the last offset won’t change
So that address would be always different but the last 3 nibbles would always be the same
And because we can overwrite the last two bytes this means during the time we are overwriting we need to know the first nibble (??)
For example:
Hopefully you get what I mean!
So it sounds maybe not possible because it always changes. But the chances of getting it right is 1 outta 15 trials
That means we just need to make the exploit script run 15 times and then in one of the iteration you would get the right value
Now what next?
In order to achieve the loop part I made this function:
It works! But I prefer working with ASLR disabled so that the offset would be the same and less tiresome (afterall we will be waiting for the script to loop 15 times)
So I made this function to work with that
To disable ASLR run this:
- echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Now we can safely jump to the notcalled
function
Let’s see what this function provides for us
From the decompilation earlier we saw that it would run 8 bytes shellcode given
But ideally that’s too small for us to do anything
In this case I would love to spawn a shell but 8 bytes is just too small for our shellcode size!
So what now?
Well why don’t read in a larger input size and cast the input as code 🤔
That’s exactly what I did:- A two staged shellcode (i think that’s what they call it)
For us to read in a larger input size I used the read syscall
rax = 0x0
read(unsigned int fd, char *buf, size_t count)
If we try to use something like that we would see the size exceeds 8
Jeez 25 bytes (But of cause they are ways to make it way smaller….. but I couldn’t achieve that!)
So instead this is what i did:
If we take a look at the register before it calls our shellcode you should see this
RAX is already set to 0x0: mov eax,0x0
RDI is already set to 0x0: mov edi,0x0
RDX size is already too much: lea rdx,[rbp-0x8]
So our only concerning register is RSI
though it’s going to be the same value as RDX
but I had issue with that so I decided to use another register
Now let us inspect the registeres during runtime before it calls our shellcode
They are other registers that hold a stack address but I made use of the R13
register which is actually a pointer to another stack address and that pointer holds the binary full path name
Now we can limit the shellcode size
Cool our first staged shellcode size is just 7 bytes
Then we can make the second staged shellcode call execve('/bin/sh', NULL, NULL)
which would spawn a shell 🙂
This the shellcode I’ve edited and used
Compared to pwntools shellcraft generated shellcode length it’s just 23 bytes
With that said that’s all :P
We can test it by running it
Oh yes ASLR is still disabled :(
Well you can still enable it and the exploit would work as long as you comment the aslr_off
function and uncomment the main
function
To enabled ASLR do this:
- echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
On running the exploit it would spawn a shell
Ah the moment the shell finally spawned I was like woohhhhhoooooooooo cause I spent 4 good hours trying to solve this 🥰
The challenge I’ve not solved so far is: