This toy is not one of the challenges. It calls write(1, "hi there\n", 9)
and then exit(0), using nothing but syscalls. Save it as hello.s:
# hello.s -- write(1, msg, 9) then exit(0), using only system calls.
.intel_syntax noprefix
.global _start
_start:
mov eax, 1 # syscall number 1 = write
mov edi, 1 # arg 1: fd 1 = standard output
lea rsi, [rip + msg] # arg 2: address of the bytes to print
mov edx, 9 # arg 3: how many bytes
syscall
mov eax, 60 # syscall number 60 = exit
xor edi, edi # arg 1: exit status 0
syscall
msg:
.ascii "hi there\n"
xor edi, edi sets edi to 0 (any value XOR itself is 0). _start is
where a program begins when there is no C main.
Build and run
$ as hello.s -o hello.o # assemble: text -> machine code
$ ld hello.o -o hello # link: make a runnable program
$ ./hello
hi there
Watch the syscalls happen
strace prints every syscall a program makes:
$ strace ./hello
execve("./hello", ["./hello"], 0x7ffed4b0f620 /* 11 vars */) = 0
write(1, "hi there\n", 9hi there
) = 9
exit(0) = ?
+++ exited with 0 +++
The first line is the shell starting the program. Then exactly our two
syscalls. = 9 is the return value of write (9 bytes written). The
message itself is mixed into the strace line because both print to the
same terminal.
Look at the bytes
$ objdump -d -M intel hello
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov eax,0x1
401005: bf 01 00 00 00 mov edi,0x1
40100a: 48 8d 35 10 00 00 00 lea rsi,[rip+0x10] # 401021 <msg>
401011: ba 09 00 00 00 mov edx,0x9
401016: 0f 05 syscall
401018: b8 3c 00 00 00 mov eax,0x3c
40101d: 31 ff xor edi,edi
40101f: 0f 05 syscall
0000000000401021 <msg>:
401021: 68 69 20 74 68 push 0x68742069
401026: 65 72 65 gs jb 40108e <msg+0x6d>
401029: 0a .byte 0xa
Line by line:
b8 01 00 00 00 is mov eax, 1. b8 means "move a 4-byte number into
eax"; the number follows in little-endian order: 01 00 00 00 = 1.
0x3c is 60 in hexadecimal: the exit syscall number.
lea rsi,[rip+0x10]: while this instruction runs, rip already holds
the address of the next instruction, 0x401011. Plus 0x10 gives
0x401021, which is msg. objdump does that sum for you in the comment.
Because the distance is relative, this code works wherever it is copied.
0f 05 is syscall.
- Under
<msg>, objdump tries to read our text as instructions and
prints nonsense (push, gs jb). The bytes 68 69 20 74 ... are really
the letters h i (space) t ..., and 0a is the newline. A disassembler
cannot tell code from data; you have to.
The whole program is 33 bytes of code plus 9 bytes of text: 42 bytes.
Check the registers right before the syscall (gdb)
(gdb) break *0x401016
(gdb) run
Breakpoint 1, 0x0000000000401016 in _start ()
(gdb) info registers rax rdi rsi rdx rip
rax 0x1 1
rdi 0x1 1
rsi 0x401021 4198433
rdx 0x9 9
rip 0x401016 0x401016 <_start+22>
(gdb) x/s $rsi
0x401021 <msg>: "hi there\n"
(gdb) x/9xb $rsi
0x401021 <msg>: 0x68 0x69 0x20 0x74 0x68 0x65 0x72 0x65
0x401029 <msg+8>: 0x0a
break *0x401016 stops at the syscall instruction, before it runs.
rax = 1 (write), rdi = 1 (stdout), rsi points at the text, rdx =
9. This is the single most useful check when your shellcode misbehaves:
stop on the syscall and read the four registers.
The same thing from Python, with pwntools
pwntools is a Python library for exploitation (installed in the
workspace). asm() assembles text to bytes; disasm() goes back.
from pwn import *
context.arch = "amd64" # tell pwntools which CPU: 64-bit x86
sc = asm(r"""
mov eax, 1
mov edi, 1
lea rsi, [rip + msg]
mov edx, 9
syscall
mov eax, 60
xor edi, edi
syscall
msg:
.ascii "hi there\n"
""")
print(len(sc))
print(sc.hex())
42
b801000000bf01000000488d3510000000ba090000000f05b83c00000031ff0f0568692074686572650a
The same 42 bytes objdump showed. To run raw bytes and see what they do,
pwntools can wrap them into a tiny program: make_elf(sc, extract=False)
returns a file path you can start with process(path). For this toy it
printed b'hi there\n' and exited with code 0.
From the shell, pwn asm and pwn disasm do quick one-offs:
$ pwn asm -c amd64 -f hex "mov eax, 1; syscall"
b8010000000f05
$ pwn disasm -c amd64 b8010000000f05
0: b8 01 00 00 00 mov eax, 0x1
5: 0f 05 syscall
shellcraft: ready-made pieces
shellcraft generates assembly text for common jobs. Always print it and
read it before using it:
print(shellcraft.echo("hi there\n"))
/* push b'hi there\n' */
push 0xb
dec byte ptr [rsp]
mov rax, 0x6572656874206968
push rax
/* call write('1', 'rsp', 9) */
push SYS_write /* 1 */
pop rax
push (1) /* 1 */
pop rdi
push 9
pop rdx
mov rsi, rsp
syscall
This version builds the text on the stack instead of storing it after
the code: push rax puts the 8 letters "hi there" on the stack (written
little-endian, so they read backwards in the number), and rsi = rsp
points at them. Notice the odd push 0xb / dec byte ptr [rsp]: it pushes
11 and subtracts 1 to make 10, the newline. The next lesson explains why it
avoids writing 0a directly. asm(shellcraft.echo("hi there\n") + shellcraft.exit(0)) is 37 bytes and also prints hi there.
Other templates you will meet: shellcraft.cat(path) (open + read +
write a file), shellcraft.sh() (run /bin/sh with execve), and
shellcraft.setreuid() (explained below).