Quick announcement, I'm gonna use laser pointer and I'm gonna use this screen. So those people sitting here, please move either to the center of the room or to the right side. You prefer to stay there? Well I think it depends on how close I am to the mic rather than... Okay, so let's start. Thank you for coming for my talk. It's the first talk in the morning, so it might be pretty difficult for you to get up early and come here, so thank you for showing up. The title of my talk is JIRA, which is a tool for automatic detection, identification and repair of control hijacking attacks. I am Alexey Smirnov and this is a joint work with Tricker Chair, who is my advisor. We are from Stony Brook University. Here is the outline of my talk. First I'm gonna give a brief introduction on control hijacking attacks. Then I'm gonna talk about the related work in the areas of attack detection, identification and repair. Then I'll present our architecture, JIRA, which has these three components. The attack detection component, the attack identification and attack repair component. The purpose of attack detection component is to detect a buffer overflow or control hijacking attack. So it's similar to the goal of the tools like StackGuard and Propolis. The attack identification component builds up the signature that can be supplied to intrusion detection system so that these attack packets can be filtered out in the future. The attack repair component repairs the program and allows it to continue its execution as if the attack never happened. In the performance evaluation part we're gonna talk about the overheads, the runtime overhead and the compile time overheads of our compiler. And we'll conclude with a summary of research contributions and with the direction of future work. Buffer overflow attacks are the most common type of attacks. Computer warms use buffer overflow attacks as a basic building block for the propagation. So as soon as they arrive to a new host, they perform a buffer overflow attack, hijack control of the underlying program or of the host, and then scan for more vulnerable machines by performing random IP scanning. Once they find a new vulnerable machine, they send their body to the new machine and perform the control hijacking attack on that new host. A comprehensive protection strategy should include the following three components. The attack detection component, which will detect the control hijacking attack when it happens. That is, when the worm tries to get control of the underlying computer. The attack identification component is responsible for building the signature of the attack. That is, for identifying malicious packets and representing them in the most general form so that polymorphic worms or polymorphic attacks can be detected by the same signature. The attack repair component allows the compromised program to continue its execution as if the attack never happened. That is, it should restore the memory image or the environment of the program as it was in the pre-attack time so that the program will think that it has never received any attack packets. We propose a compiler, Jira is a compiler, that instruments programs, source code of a program so that the instrumented program can identify, detect, and repair itself after a control hijacking attack. What is a buffer overflow attack? Well, the idea behind control hijacking attacks is to operate a control sensitive data structure such as a return address, function pointer, or any other control sensitive data structure. By the way, I've got some t-shirts here and books and I'm going to give them away and I'm going to ask you questions and who answers first gets a t-shirt or a book. So here's the first question. The return address and the function pointer are two possible types of control sensitive data structure. What are other data structures that a worm or a virus can override? That's what I said, function pointer. What else? Okay. What about HIP? What do you mean by HIP? Well, okay, I got a t-shirt. What else? That's what he said probably, HIP. Okay, that was what I... So the name of the control sensitive data structure is... just word. Okay, any other ideas? Okay. Well, for example, GOT, which is global offsets table. Anyway, buffer overflows are possible when the length of the destination buffer is shorter than the length of the data that's been written into it. So whenever you write something that is longer, it can override something that is behind this buffer, such as a return address. And that's when the buffer overflow happens. Standard libc functions, such as stringcpy or sprintf, are responsible for most buffer overflow attacks. So we're going to review related work in the areas of attack detection, identification, and repair. StackGuard is the first tool for buffer overflow attack detection. It works by placing a canRevert in the stack frame in the function prologue, and it checks this canRevert at function epilogue. If the values are different, then the attack has happened, and the application is terminated. RADint is another tool that protects programs from buffer overflow attacks. It works by saving the original return address in a special well-protected buffer at function prologue, and compares it with the values stored in the stack at function epilogue. If the values stored in the protected buffer end in the function frame when the function returns are different, then an attack is dejected. Can anyone think of any other tools for attack detection? Nah, Snore is content filter engine. Okay, I got it. You got a T-shirt? Who was that? So, let's move to attack identification. We need automatic ways for attack identification, because it's very important to generate signatures fast. Computer warms infect vulnerable machines very fast. It's a rate of thousands per minute, or maybe tens of thousands per minute. Therefore, if we wait for a human expert to come up with a signature, which is going to take a few hours, then the warm is going to hijack the most vulnerable host on the Internet. Recent epidemics propagate at a very, very high speed, so it's very difficult for a human expert to come up with a signature quickly. Previous systems for automatic signature generation either produced signatures that consisted of only one packet, or required a large pool of malicious network data to come up with a comprehensive signature. Signature that consists of one packet is sometimes insufficient, because a complex attack can involve multiple packets, such as, let's say, FTP server attack, it involves sending out authentication packets, and only then sending out the final attack packet. A comprehensive signature should include all attack packets, not just the last one. So here are some relevant projects. TaintCheck is a tool for... It uses the taint analysis to generate attack signature, and it reports the value of the return address, the form used to hijack control as a signature. So it's only a four-byte signature. Autograph uses so-called Rabin fingerprints to find common subsequences in a large pool of malicious network data. Polygraph and naming also use a large pool of malicious data, and to use machine learning algorithms to construct a multi-packet signature. AttackRepair relies on checkpointing. That is, one first checkpoints the program state, and then jumps to a previous point in the past to restore program's execution. So two approaches here are either keeping complete execution history, or do periodic stack checkpointing. That is, to take a snapshot of the memory image of the program. And the second approach relies on a fork system call, which is available under Linux, but might not be available under other operating systems. And Shadow Honeypot project runs two versions of the application, instrumented, that is protected and non-protected, and switches between them dynamically when it detects an attack. Now we move on to DIRA architecture. DIRA is a compiler that provides all these three components. AttackDetectionComponent, AttackIdentification, and AttackRepairComponent. As I said, DIRA is an extension to GCC, version 3.4.1 currently, but it can be used with other versions as well. The idea behind DIRA is memory updates logging, and this approach solves all these three problems at the same time. So the idea is to maintain a run-time log of all changes that the program performs. Assignments such as A equals B, or ellipsis functions call, such as memcpy, change memory state of the program, and therefore their effects should be saved. For example... Okay, we will see some examples later. For each memory update, DIRA stores its source address, its destination address, its length, and the preimage. So in the case of the assignment A equals B... Where's my pointer? Here. The source is the address of variable B, because that's where the new data comes from. The right address is the address of A, because that's the address that has been written into. The length of the data is the size of type of variable B. So if it's integer, it's 4-byte. The preimage is the old value of A, and it's used to restore memory image to the program. So the DIRA compiler instruments the source code of the program in compile-time. It adds the log-in code for variable assignments and for standard library functions. At run-time, this log-in code, this memory update log, is generated, and it's used at repair time to detect, to identify, and to repair the attack. So how can we detect, identify, and repair an attack if we have this memory update log? To detect an attack, we need to compare the current value of the return pointer, or the return address, saved in the stack frame with the value saved in the memory update log. When we invoke a function, we lay out a new stack frame, we save the old return address, we save any registers that are used in this function, and therefore, this is a changed memory state of the program, which should be saved in the memory update log according to our concept. Therefore, the old value of the return address will be saved in the memory update log when the function is invoked. When the function is returning, this saved value should be compared with the value that is currently on the stack. If these two values are different, then there is an attack. To identify an attack using the memory update log, we can trace back the data that overrode the control-sensitive data structure to the point where it was first received in the program. For example, through a receive call. To repair an attack, we need to restore the memory image of the program by using the pre-images that are also saved in the memory update log. This idea of keeping the memory update log of program memory state changes is the essential idea behind our tool. It allows us to solve all these three problems of attack detection, identification, and repair in the same uniform way. Memory update log is a circle buffer. Each record of this buffer has four fields. Read address, write address, length, and data. Dero saves the effect of each assignment operation, such as x equals y, where x and y are either directly referenced variables, array references, or dereferenced variables. As I said previously, the read address is set to the address of the right-hand side, the write address is set to the address of the left-hand side, the length is set to the size of the memory update, and the data is set to the pre-image that stores the data that they're going to overwrite, that is, the old value of x. If the right-hand side is a complex expression, then a log record is created for each variable in the right-hand side. To handle memory change performed by standard library functions, Dero proxies a few of them, such as string manipulation functions, format string functions, file network IO functions, and other functions that change memory state of the program. So the proxy function constructs a new memory update log record that summarizes the changes performed by the corresponding ellipsis function. And the memory update log is also used to store tags, which are special records that indicate change to memory to the state of the program. For example, when the function is invoked, function enter tag is inserted, and when the function returns, function exit tag is inserted. These tags are used at repair and identification time to build up signature and to repair program after an attack. So here are a few examples. At compile time, let's say there is a statement x equals y plus z. So the compiler adds two logging codes. The first code logs the effect of variable y, sorry, of variable z. The other of the variable y. And finally, there is this original statement. At run time, these two logging codes produce these two log records with the read and write addresses set as I described previously. Suppose there is the string CPY code in the source code of the program. The debug compiler replaces this code with a proxy function. And at run time, this proxy function inserts a log record with the read address of b, because that's where the data is coming from, with the write address of a, because that's where the write data enter. With the length, strlen of b plus one, because they're going to write strlen of b bytes plus non-terminating character. And with the preimage is a, which is the old value of a, Now let's talk about attack detection. Jira uses a RAD-like approach. That is, it saves the original return address when the function is invoked and compares the saved value with the value that is currently on stack at function epilogue. If these two values are different, then an attack is detected. Jira can protect other control-sensitive data structures, such as function pointers, GOT, signal-handling variables, and a similar function, but it's not implemented yet. Now let's talk about attack identification. So here are the desired properties of an ideal signature. A signature should be context-aware, which means that it should include all the attack packets that participate in a given attack. And this allows to reduce the rate of false positives. It should be also semantics-aware, that it ideally should incorporate the knowledge about the protocol. For example, if this is an HTTP attack, and the attack request is a GET request, then the signature should specify that this is GET, so that the intrusion detection system can filter out, keep all the requests of other types, of other protocols, and even if it's HTTP, but not GET, it should also discard those packets. It should check the signature only against packets that are HTTP GET kind of packets. And this also allows to reduce the rate of false positives. And the signature should provide a certain degree of flexibility within each packet, so that polymorphic warnings that change their body as they propagate can be matched using the same signature. The attack packets should be represented as regular expressions, and that's the desired flexibility that a signature should have. So, as I said, these three properties are the desired properties, and now let's see how JIRA satisfies all those properties. JIRA signatures are kind of severe, because they consist of multiple packets. They consist of all packets that an attack consists of. And each of those packets is represented as a regular expression. And there's a feature called length constraint that allows to limit the attack in part of the packet. That is, it allows to specify that a terminating character, such as null character, should occur between offset A and offset B. If the terminating sequence occurs within this offset, this is not an attack packet. If there is no such a terminating character in the packet, then this is an attack packet. So, in some sense, this is equivalent to semantics awareness, because it allows to narrow down the part of the packet that the intrusion detection system searches. So it doesn't search the specified sequence in the whole packet, but rather this length constraint allows to limit the... or narrow down the scope where we search for the... required terminating constraint. For example, if this is a... same example, if this is a GET attack, and there's an overly long URL, which is terminated by a null character, here it allows to specify that this null character should occur between the fourth and, let's say, twentieth-bit byte of the packet. Because the first three characters are GET characters, and then the remaining part of the packet is the URL. If the terminating character is found in the first twenty-four bytes of the packet, then this is not an attacking packet. If the null terminating character is not found in the first twenty-four bytes, then this is an attack packet, because the URL is too long. Memory update slog allows us to construct signatures that consist of multiple packets, to represent each packet as a regular expression, and to generate a length constraint. And we're going to give an example, and we will show how all this functionality is implemented. Two types of dependencies exist in the program, data and control dependency. Data dependency is created when one variable is assigned to another, for example, A equals B, same example. In this case, A depends on B. A control dependency is created whenever variable X and variable Y is used... I'm sorry, variable X is used in conditional expressions, such as if, and Y is in this conditional body. So here's an example. In this case, even though there is no direct assignment between X and Y, Y depends on X, because the value that Y will take depends on the value of X. This type of dependency is called control dependency. No direct assignment, but dependency is there, because value of Y depends on value of X. Why do we need control dependencies? So here's an example. FTP server attack involvement authentication. Here's an example of vulnerable FTP server. Here's the vulnerable buffer. The program has two state variables, isAuthentication, isUser. And here is the main loop of this server. First, it receives a packet. Then it compares the four bytes of the packet. If it's QuickCommand, the loop breaks. If it's UserCommand, then isUser variable is set to 1. If it's PasswordCommand, then isUser and isUser has been set. Then authentication has been completed, and isAuthenticated variable is set to 1. If authentication was not performed previously, then the program continues. The client requests are handled only in the case if the authentication has been completed previously. So if the user is authenticated, then the program checks the first three bytes of the packet. If it's a GET packet, then there's a GET request, and it returns the file that the user requested. But this request is only processed if the user has previously authenticated. So these are the three packets used in the attack. There is a file name that is too long and that overrides the buffer, that overrides this buffer, which is only 16 characters bytes long. Let's look at lock records generated at run time. First, there is a lock record here at receive for the receive function. For the proxy function, proxy is standard receive call, and it has this tag. Here's the tag. This is the write address. That's the destination, where the network data is written into 11 different things. And here's also the image of the data that varies from network. Then there is a proxy function for SDR and CMP, which also has the write address... I'm sorry, it's read address of the variable. 4 is the length of the argument. This record specifies... indicates that we search buffer P and we look at first four bytes of this buffer. Then this record is created for conditional expression and it means that variable P participates in the if expression. So for every variable in the if expression, we save this variable into memory and this log. And this is also... this is the record for the statement that updates each user variable. So in this case, the read address is set now because we don't know where the data that is written into isUser comes from. And it's just a constant. So the write address is isUser. The length is 4 and isUser is preimage. Similarly, for the second packet, we have the receive record here. We have the string comparison record that checks the first four bytes of the packet. We have two conditional variables now because two variables are checked, P and isUser. And finally, this is the record that updates the isAuthentication variable. So finally, let's look at the third packet. So we also have a record when the packet is received. Then we also have the conditional record for variable isAuthenticated. Then there is the record for string comparison that checks the first three bytes of the packet. Then there is the conditional record for P. And finally, this is the record that was created by string.cpyProxy function. And it describes the fact that the data from buffer here, I'm sorry, the data from buffer P plus 4 was written into buffer. And the length of the update is the length of P minus 4 plus 1 because all bytes of P except the first four bytes were copied into buffer buff. And we also save the preimage of P. So this is the summary. These are all log records that are generated at runtime when this server attack is executed. And suppose that the return address is located after the buff variable. That is, the return address is saved at the offset buff plus 17. Buffer is a buffer of 16 bytes. So the return address is saved at the offset 17. Now let's see how we can detect the control hijacking attack using this memory on this log. So we start with log record, with the last log record that overrode this buffer. And here is its write address. So we look for the source of this data. That is, we try to find out where the data came from. And this log record allows us to conclude that this data came from P. Return address was saved at the offset buff plus 17. Therefore, the data was read from the offset P plus 4 plus 17, which is P plus 21. That is, the return address value came from memory address P plus 4 plus 17, which is P plus 21. So this is the new address that we look for. Now we search memory on this log for the address P plus 21. And if we traverse this log record, this memory on this log upward, we can find that there is this receive record that read data from network into buffer P and the length was 62. And we look for memory... memory location with the address P plus 21. And this is the record that has written data from network into that memory location. So we conclude that here is the read operation that read data from network and wrote it into address P plus 21. And because this is the operation that reads data from network, we conclude that this is the attack in packet. So this way we identify the first attack in packet. Is it the only packet in the attack in packet? Probably not, because the user is only processed if the user has authenticated. So we need to continue our search and try to find more attack in packets. Sure. Well, debugging is not automatic. Our tool is completely automatic. It doesn't require any user involvement at all. So the process that I described here is completely automatic. It's performed by our program, not by a user. Not manually. It's not log analysis performed by a human expert. It's log analysis performed automatically. Well, you are right. Well, that's the question of our performance evaluation. We will get to this question later. And if I don't give a clear answer, I'd like to talk to you offline. So as I said previously, we were able to identify the first attack in packet. And now we look for more attack in packets. And here's where we use conditional dependencies. Let's look at the first memory update statement. If we go upward, we can see that this statement depends on two conditions. That is, it's inside two outer if statements. The first if statement checks p variable, and the second checks is authenticate variable. Therefore, this memory update was only possible because of these two checks that involve these two variables. Variable p is, has been found before. So, this record should be discarded. Whereas, this variable is authenticated should be used for later search. So we continue with variable is authenticated. And we try to find out whether this variable depends on network packet or not. So, if we continue traversing the block upward, we can see this memory update block operation. And the read address of this operation is unknown because this variable was assigned to a constant. Therefore, we should again use condition dependencies to find out what our variable is authenticated depends on. And we can see two conditional lock records isUser and p here. Which means that variable is authenticated depends conditionally on isUser and p. So, we now look for p and isUser. We can find p here. Therefore, we conclude that variable isUser depends on network packet that contains password. And this turns out to be true because variable p is set only if the attacker sends password packet. That variable isUser also depends sorry, variable isAuthenticated also depends on isUser. So, we continue with this variable isUser and we can find it here. This is the memory update long operation that sets variable isUser. Its readers is also unknown because it's set to constant. Therefore, we should use control dependencies to find out whether variable isUser depends on any network packets. And once again, we can see this conditional string expression that uses variable P. Therefore, we now search for variable P and try to find out whether it depends on any network data. And it turns out to be true. Variable P contains the packet payload of the user command. So what we can see now, we have found three attack packets. And so the get packet, the pass packet, the user packet. The first packet was found using direct dependencies. The remaining two packets were found using control dependencies. So the control dependencies are really important to link the first attack packet to all the remaining attack packets. Because of this state variables that are used in network daemons. Well, this is just our assumption. Well, the real value of the return errors is returned by the attack detection component. Well, this attack detection component tells you which data structure was damaged. So it tells you the return address of the compromised data structure, which in our case is the return address. So this assumption I just made it up. But this address is always known at runtime. Let's now give a precise definition of control dependencies. We say that whenever a variable x can prevent control flow from reaching variable y, a control dependency is created between x and y. Let's look at the value of variable cond is. However, if you look at the right-hand side, then one of these branches here, this branch, has a return. And therefore, if condition is takes such a value from x to then value is returned y. And therefore, the value of condition is returned from x to y. So this the value of condition. therefore, the value of condition is returned from x to y. And therefore, the value of condition is returned from x to y. therefore, the of condition is returned from x y. And therefore, value of condition is returned from x to y. And therefore, the value of condition returned from y. And therefore, the value is returned from y. And therefore, the value of is returned from x And therefore, the value of condition is returned from x to y. And therefore, the value of condition is from x to y. And therefore, the value of condition is returned from x y. And therefore, the value condition is returned from And therefore, the is returned from x to y. And therefore, value of condition is returned from y. And therefore, the value of condition is returned from x to y. And therefore, the value of condition from y. And therefore, of condition is returned from x therefore, the value of condition is from x to And therefore, the value of condition is y. And therefore, condition is returned from x to y. therefore, the value of condition is returned from y. And therefore, the value of condition is returned from x to y. And therefore, the value of condition is returned x And therefore, the value of condition returned from x to y. And therefore, the of condition is returned from x to y. And therefore, value of is returned x to y. And therefore, the value of is returned from x to y. And therefore, the value of from And therefore, value of condition is returned from x to y. And therefore, the value of condition is returned from x to y. And therefore, value of condition is returned from x y. And therefore, the value condition is returned from x to y. And therefore, the value of condition is x to y. And therefore, the value of condition is returned from x to y. And therefore, the value returned from to y. And therefore, the value of condition is returned from x y. And therefore, the value of condition is returned from x to y. And therefore, the value condition is returned from x to y. And therefore, the value of is returned from x to y. And therefore, condition is y. And therefore, is returned from x y. And therefore, the value of condition is returned x to And therefore, of is returned from x to y. And therefore, condition x to y is returned to y. And therefore, value of condition is returned from x to y. And therefore, value of condition is returned from x to y. And value of condition returned from x y. And therefore, returned x to y. And therefore, condition is returned from x to y. And therefore, value of condition is returned from y. And therefore, condition is returned to y. And therefore, value condition is returned from x to y. And therefore, value is returned x to y. And therefore, is returned x to y. And therefore, condition is returned from x And therefore, value of condition is returned y. And y. And therefore, condition is returned x to y. And therefore, value returned This is the length of packet, this is the packet that represents the regular expression, this is the length constraint. This is pretty much the same as the signature that we constructed in our example. This is the bind signature, signature for DNS daemon. So one packet, this is the length, this is the length constraint. Remote cat daemon has a signature that consists of three packets because this cat daemon requires user authentication. So here we can see user packet, password packet, the length constraint and the, yes, and the last packet. All the signatures will generate automatically from a single attack instance. Now let's talk about the recovery. Is it really useful? The overhead is pretty high at 70%. Maybe we can do well without recovery. And we think that recovery is really useful because many programs are single-thread and they are, like, event-driven. And if you just terminate the program, then you disconnect all the clients. So you have only one bad client, you have, let's say, a dozen of good clients. You disconnect one bad and disconnect a dozen of good, which is very bad for good clients. And the same attack can be reviewed in the future, and therefore the attacker can implement the denial of service attack if you simply terminate the program instead of recovering. If you recover it, then repair the effect of the bad guy, but you allow good guys to continue their execution. And the same trade-off exists in the case of source code analysis tools. Whenever you have a bug, if you use static code analysis tool, then you first need to find out this bug, and these tools are prone to false positives, so we need to determine whether this is a false positive or a real bug. If it turns out to be a real bug, then you have to come up with a fix. You have to upload this patch to the developer's side. The developer has to verify this patch. So it's also a lengthy process, and it involves a lot of human experience. So there is the same trade-off. You either invest the same amount of runtime as we do, or you invest the same amount of developers' time, which is probably more expensive. Now we arrive at the conclusion. DIRA is the first system that solves the problems of attack detection and identification in a unified way using memory on this log. It can produce accurate multi-packet signatures from a single attack instance, which we believe is a big achievement, because all the previous systems either reported less attack packets, or they required a large pool of network data to generate an accurate multi-packet signature. Dynamic slicing of the memory on this log is the underlying technique behind our tool. And this is a well-known technique from programming languages, but I think we just found some cool applications for this programming languages technique. And we plan to apply the same technique in the future to generate patches for buffer overflow attacks. This is what we're working on. We plan to work in the next few months. Our tool is available for download. Please visit our website and download it and play with it. So if you find bugs or if you have any experiences to share, please mail me and I'll be happy to answer you. Thank you. I have one book, one question. Yes. I'm sorry. Well, our program generates a signature whenever an attack happens. It doesn't use history. It doesn't look for previous packets. It doesn't analyze them. So whenever you perform an attack, a signature will be generated. So the question is whether the signature will be different from previously generated signatures or not. And I think the answer is yes, because if the length is different, then the regular expressions will also be different. Yes, this is the kind of denial-of-service attack that we think is like more lightweight than complete program restart, because a complete program restart disconnects all clients, all benign clients, and recovery doesn't disconnect anybody except attacker. And therefore, the benign clients will be able to perform their requests slower than when the program runs normally, but at least they won't be disconnected, which we believe is pretty good. Remember, AtlasLocker is a circular buffer. Yes, the log records are used constantly and previous records are freed up when the record is about to be used. Yes. Okay, thank you very much.