How does virus protection in CPUs work?

suszterpatt

Senior member
Jun 17, 2005
927
1
81
Is there some way a CPU can decide if it's executing a virus? Or is it just protection against virii that try to damage the hardware?
 

CTho9305

Elite Member
Jul 26, 2000
9,214
1
81
No, it can't tell it's executing a virus, and it isn't just to protect the hardware. What the technology AMD calls "Enhanced Virus Protection" does is protect against a certain type of exploit commonly used by attackers. Ironically, a classical virus wouldn't be affected (i.e. the type that modifies executables on your hard drive when you run them).

The short version is this:
Sometimes, programmers make mistakes when they read data from a file or the internet. They allocate a buffer (just some storage space) to hold some data, but don't allocate enough space. Often, this is because the file they're reading says, "I'm 100 bytes long", but it's really longer.

For example, your web browser might download an image which claims to be 10 pixels wide by 10 pixels tall, but actually contains a lot more data. If it allocates a buffer with only enough space for a 10x10 image, and then writes all of the data into that buffer, whatever sits after the end of the buffer is going to get overwritten. This is called a "buffer overflow", because, well, you fill up the buffer completely, and keep writing more stuff, so you overflow it.

Now, this is already bad, since some of the data your web browser is using just got clobbered. However, it gets worse - because of where buffers are often allocated, and how programs actually work, by overflowing a buffer you can actually cause a program to actually start executing instructions from anywhere you want. If this image file were carefully crafted, it could actually contain valid instructions, and get the computer to start executing the instructions contained in the image, at which point it can take over your computer.

Fortunately, there's a way to make this type of exploit much harder to pull off. The data buffers are usually allocated in two areas of memory, one called the "heap" and one called the "stack" (in this case, I described a stack buffer overflow, because exploiting one in the heap is harder to explain). The program's instructions live in a third area, often called "text". These three areas are located in different parts of memory.

What "Enhanced Virus Protection" (also called "NX" for No-eXecute or Non-eXecutable) does is let you mark certain areas of memory as not-executable. If we make the "heap" and the "stack" not -xecutable and only mark the "text" area as executable, then when an exploit overflows a buffer and tries to start executing the instructions it contains, the CPU will see that the location of the instructions is part of the memory that should not be executed, and tell the operating system that something very bad is happening, so the OS can kill the program.

It's not a cure-all, but it does help against many common types of attacks.

edit:
I guess that didn't end up being so short ;).

Now, a classical virus won't be stopped. However, they are somewhat rare, and can't spread on their own, so they aren't a big deal nowadays. The reason a classical virus won't be stopped is how it works. You (the virus writer) start the virus off by infecting a program and distributing it. When people run the program, the virus looks for other programs on your computer and modifies them to include the virus in the "text" section. Since the "Enhanced Virus Protection" just makes sure that only instructions in the "text" section execute, it won't stop the virus from running.
 

CTho9305

Elite Member
Jul 26, 2000
9,214
1
81
I have not proof-read this post.

I'll expand on buffer overflows a bit more. I'll set up a hypothetical image format, some code that reads the images, and an exploit. It's all contrived, to hopefully make it easy to follow.

Let's make a simple image format that stores an image caption and the picture data. The format is as follows:
1. the text "CAPTION"
2. a number, specifying the length of the caption (not allowed to be longer than 20 letters)
3. the caption string
4. the text "IMAGE"
5. image data (this part doesn't matter).

Let's write a function to read the caption from this format:
function readCaption() {
// look for the caption
while (readString() != "CAPTION") ;// keep looking for the string
captionLength = readNumber(); // get the length of the caption
buffer = createBuffer(20); // get a buffer that can hold the maximum size - 20 characters
nextWord = 0;
while (nextWord != "IMAGE") { // keep going until we hit the image
nextWord = readByte();
buffer = buffer + nextWord; // append the next word from the caption to the buffer
}
return buffer; // return the complete caption string
}

What happens if we give it the following image?
CAPTION20A picture of my cat.IMAGE{rest of the image}
Everything works properly. The buffer can hold 20 characters, and we read a word at a time until we see "IMAGE". The caption is 20 characters long, so it fits in the buffer.

What about this image?
CAPTION20Ha Ha! This image is very evil! IMAGE{rest of the image}
The program creates a buffer that can hold 8 characters, and starts copying the string into the buffer. Let's look at what the buffer contains after a few iterations:
|H|a|_|H|a|!|_|_|_|_|_|_|_|_|_|_|_|_| (after two)
|H|a|_|H|a|!|_|T|h|i|s| |i|m|a|g|e|_| (after four)
What happens when it hits "is"? It keeps on appending...
|H|a|_|H|a|!|_|T|h|i|s| |i|m|a|g|e| |i|s|

But where do the letters "is" go? To understand that, we need to understand where the buffer is located, and a bit about how programs actually work. Sorry this is so long!

Consider a program like this:
function main () {
doSomething();
print("I did something!");
}

function doSomething() {
print("I'm doing something!");
return; // go back to whoever called doSomething()
}
When the program starts, it begins in the function main. From main, it needs to jump to the function doSomething. It does this, and prints the message. But how does doSomething know where to jump to when it finishes? After all, in a bigger program, doSomething could be called from lots of different places, and it needs to go back to the right spot after it finishes. The solution to this is a "stack" - just think of it like a stack of paper. You can put something new on the top, or take the top item off the stack. Each piece of paper can only hold 1 number (so, if you want store two numbers, use two items). When main calls doSomething(), it puts a new item on the stack, which says, "when you're done, go to line 3" (so, the piece of paper says, "3"). Now, when doSomething has finished, it takes the top item from the stack and goes to that line in the program. This works no matter how many functions we have. One thing I didn't tell you about stacks before is that you can read and edit items on the stack that aren't at the top of the stack (but you can't add or delete things other than at the top). In a computer the stack starts at the top of memory and grows downwards (it's not as stupid as it sounds ;)), so the older elements on the stack are at addresses that are higher numbered.

Return addresses aren't the only thing we can put on the stack. Allocating memory on the stack is very simple (just add some items to the top), so variables and buffers (sometimes) are put on the stack (instead of the heap, which I mentioned in my first reply but am not going to describe now - allocating space in the heap is slow). If you have a function like this:
function doStuff() {
num1 = 5;
num2 = 6;
num2 = num1+num2;
return;
}
then you can add two items to the stack for num1 and num2 for the first two lines, and the third line would be "read the first item on the stack, read the second item on the stack, add the results, and edit the first item to hold the result" at the assembly language level. When the function finishes, it has to clean the stack up by taking anything off that it added, so it would pull num1 and num2 off the stack, and return to whoever called doStuff.

Let's look at a similar function:
function doStuff2() {
num1 = 5;
num2 = 6;
buffer = createBuffer(10);
num2 = num1+num2;
return;
}
Now, the first two lines would put two items on the stack, and the third line would add 10 items (to hold the buffer's data). The fourth line would now translate to assembly language as "read the 11th item on the stack, read the 12th item, add them, and edit the 11th to hold the result". As before, when the function returns, it has to clean up the stack, so it takes the top 12 things off the stack (nu1, num2, and the 10-item buffer) and returns.

You can skip the next paragraph if you don't care why you can't always put buffers on the stack.

This is fine, but what happens if we don't know ahead of time how big the buffer will be? Now, the fourth line will have to refer to different stack positions depending on how big the buffer is. When the program is compiled, the instructions to read num1 and num2 will be a problem, since the compiler doesn't know how deep in the stack they should look. Buffers that don't have a fixed size are put on the heap instead of the stack.

Hopefully you can see how we're setting ourselves up for trouble - we put buffers on the stack, and we also have return addresses in there too. Consider this function:
function bufferOverflow() {
buffer = createBuffer(4); //make space for 4 characters
buffer = "1a2e3"; // write 5 characters
return;
}
Let's follow what's on the stack when this function gets called from somewhere. Pretend the return address happens to be 7.
First, whoever calls bufferOverflow saves the return address:
<- lower addresses, top of stack
7 - return address
<- higher addresses, bottom of stack
Next, the buffer is created by adding 4 blank items
<- lower addresses, top of stack
blank
blank
blank
blank
7 - return address
<- higher addresses, bottom of stack
Let's watch the stack as each letter of the string gets written into the buffer, from low addresses to high addresses:
<- lower addresses, top of stack
1
blank
blank
blank
7 - return address
<- higher addresses, bottom of stack
<- lower addresses, top of stack
1
a
blank
blank
7 - return address
<- higher addresses, bottom of stack
<- lower addresses, top of stack
1
a
2
blank
7 - return address
<- higher addresses, bottom of stack
<- lower addresses, top of stack
1
a
2
b
7 - return address
<- higher addresses, bottom of stack
What happens next? Well, the stack doesn't really care that you're about to write beyond the end of the buffer - it doesn't actually know what buffer is, it just writes things where you tell it to.
<- lower addresses, top of stack
1
a
2
b
3 <- uh oh!!!
<- higher addresses, bottom of stack
We've finished copying the string. We're done, so clean up the stack by removing the 4 things we added to it...
<- lower addresses, top of stack
3
<- higher addresses, bottom of stack
[/quote]
All that's left to do is return. Remember, we return by pull an address off the top of the stack and continuing execution from there. But the return address has been replaced with 3, so we execute whatever instructions happens to be there.

You can see now how the overflow allows an attacker to cause a computer to execute instructions from places it shouldn't - by overwriting the return address. A careful attacker can use this by actually putting instructions in the string that overflows the buffer, and causing the return address to be overwritten with the address of the buffer itself. By doing that, the attacker has a set of instructions he wrote, and your computer executing them. Voila, your computer is now his.

Now, back to the image caption function:
function readCaption() {
// look for the caption
while (readString() != "CAPTION")
;// keep looking for the string
captionLength = readNumber(); // get the length of the caption
buffer = createBuffer(20); // get a buffer that can hold the maximum size - 20 characters
nextWord = 0;
while (nextWord != "IMAGE") { // keep going until we hit the image
nextWord = readByte();
buffer = buffer + nextWord; // append the next word from the caption to the buffer
}
return buffer; // return the complete caption string
}
As an attacker, we'd have 20 characters of junk in the caption name, and the 21st spot would actually be an address - the address where the buffer is. The rest of the string would be the exploit program, which might do something like overwrite your music folder or search all text files on your computer for credit card info, or whatever evil you'd like.

One thing you might wonder about is the address of the buffer - how the attacker knows it. Well, if the attacker runs the vulnerable program himself he can just check where the buffer was for him. In the real world, it's unlikely that the address will be the same for everyone, for all attacks, so you might use an exploit program that starts with thousands of "NOP" instructions (do-nothing instrutions) and pick an address a bit higher than where you expect the buffer to be (if the buffer can be anywhere between, say, address 0x7FFF0123 and 0x7FFF1000 (which you can see by running the program many times in different situations and on different computers), just guess an address of 0x7FFF1000 and have enough NOP instructions to fill memory between 0x7FFF0123 and 0x7FFF1000. If the buffer is actually at 0x7FFF0123, you'll jump right to the start of the real instructions, and if it's at a higher address, you'll execute the NOPs until you reach the start of the code.
 

borealiss

Senior member
Jun 23, 2000
913
0
0
Originally posted by: suszterpatt
Is there some way a CPU can decide if it's executing a virus? Or is it just protection against virii that try to damage the hardware?

here virii=worms.

yes, sort of. there are ways for a cpu to detect that a virus is trying to run, but unless the cpu has very intricate hooks into the code that is running, there is almost no definite way. there are systems that use software/hardware co-design, especially in rtos or specialized embedded applications, where you could have monitoring facilities tied specifically to chunks of code. usually these chunks of code have a checksum that is compared by specific hardware that audits the running code. if a checksum is invalid, throw an exception. this is a pretty specialized scenario, however, where all of the code that is being run in the system is known by the hardware it runs on, and even then, it might not be enough.

specifically to x86, there's almost no way to tell. you could theoretically implement counters that monitor how often a program tries to elevate its current IOPL, DPL, or any other privilege level by directly modifying a task switch segment or some other permission bitmap in the machine. or NX bit violation. the cpu could be programmed via ucode to perform a specific "harmful code" exception a certain threshold has been surpassed, but its accuracy could be questioned if the code causing these exceptions is just crappy to begin with.