Lab 06 - Application Security

Resources

Setup

  • Open a lab VM instance on OpenStack, image: ISC 2023 rev 2, flavor: m1.medium.
  • Install the 32-bit libc and gcc-multilib packages:
sudo apt install libc6-dev-i386 gcc-multilib
  • Install the PwnDbg plugin:
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

To check if everything is OK, run the command gdb with no arguments. The prompt should be similar to this:

➜ gdb
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
...
pwndbg>

Enter q to exit GDB. We are using PwnDbg instead of the classic GDB because it is much more user friendly. Hope you'll like it ;)

Overview

This representation of the stack is valid for 32 bit programs. The calling convention is to save the parameters on the stack.

To find out what's different for a 64 bit program check this website.

A buffer overflow occurs when data written to a buffer overruns its boundary and overwrites adjacent memory locations, due to insufficient bounds checking.

GDB tutorial

PwnDbg

Loading a program

In order to start debugging using GDB, you need to specify the program to be inspected. There are two options for doing this:

  • When launching GDB:
➜ gdb buggy
  • After launching GDB:
➜ gdb
...
pwndbg> file buggy
Reading symbols from buggy...

Once the debugging symbols from the executable were loaded, you can start executing your program using the run command.

pwndbg> run

You do not need the specify the full command, GDB can fill in the rest of a word in a command for you, if there is only one possibility.

E.g.: r, ru and run are equivalent; c, co, continue are equivalent.

In order to specify arguments for the debugged program, you can either:

  • Specify them prior to starting the program:
pwndbg> set args a b
  • Specify them when starting the program:
pwndbg> run a b

You do not need to specify the arguments each time: run with no arguments uses the same arguments used by the previous run, or those set by the set args command.

Breakpoints

Breakpoints represent places in your program where the execution should be stopped. They are added using the break command. Here are the most common usages:

  • break function - Set a breakpoint at function entry. When using source languages that permit overloading of symbols, such as C++, function may refer to more than one possible place to break.
  • break linenum - Set a breakpoint at line linenum in the current source file. The current source file is the last file whose source text was printed. The breakpoint will stop your program just before it executes any of the code on that line.
  • break filename:linenum - Set a breakpoint at line linenum in source file filename.
  • break filename:func - Set a breakpoint at entry of the function func found in file filename. Specifying a file name as well as a function name is superfluous except when multiple files contain similarly named functions.
  • break *addr - Set a breakpoint at address addr. You can use this to set breakpoints in parts of your program which do not have debugging information or source files.

You can see an overview of the current breakpoints using the info break command.

pwndbg> info b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x000011de in wanted at buggy.c:6
2       breakpoint     keep y   0x00001229 in copy at buggy.c:16
3       breakpoint     keep y   0x00001281 in main at buggy.c:22

Short for info break is i b.

In order to remove breakpoints, you can use the clear or the delete (d) command. With the clear command you can delete breakpoints according to where they are in your program. With the delete command you can delete individual breakpoints by specifying their breakpoint numbers.

pwndbg> delete 2
pwndbg> clear buggy.c:6
Deleted breakpoint 1

Once you want to resume execution, you can use the continue (c) command.

pwndbg> continue
Continuing.
...
[Inferior 1 (process 11131) exited normally]

Start

The start command is very similar to run, but instead of running the program until it ends (or until it crashes), it sets a breakpoint at the beginning of the main function.

22              if (argc == 1) {
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────
*EAX  0xffffd880 ◂— 0x2
*EBX  0x56558fcc (_GLOBAL_OFFSET_TABLE_) ◂— 0x3ed4
*ECX  0xffffd880 ◂— 0x2
*EDX  0xffffd8a0 —▸ 0xf7fa9000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0xffffd934 —▸ 0xffffdabd ◂— '/home/student/appsec/buggy'
*EBP  0xffffd868 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 —▸ 0x56555000 ◂— 0x464c457f
*ESP  0xffffd850 —▸ 0xffffd890 —▸ 0xf7fa9000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*EIP  0x56556281 (main+31) ◂— cmp dword ptr [eax], 1
───────────────────────────────────[ DISASM / i386 / set emulate on ]────────────────────────────────────
 ► 0x56556281 <main+31>    cmp    dword ptr [eax], 1
   0x56556284 <main+34>    jne    main+61                    <main+61>
    ↓
   0x5655629f <main+61>    mov    dword ptr [ebp - 0xc], 0x796568
   0x565562a6 <main+68>    mov    eax, dword ptr [eax + 4]
   0x565562a9 <main+71>    add    eax, 4
   0x565562ac <main+74>    mov    eax, dword ptr [eax]
   0x565562ae <main+76>    sub    esp, 4
   0x565562b1 <main+79>    push   eax
   0x565562b2 <main+80>    lea    eax, [ebp - 0xc]
   0x565562b5 <main+83>    push   eax
   0x565562b6 <main+84>    lea    eax, [ebx - 0x1f6e]
────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────
In file: /home/student/appsec/buggy.c
   17         gets(name);
   18         printf("bye\n");
   19 }
   20
   21 int main(int argc, char **argv) {
 ► 22         if (argc == 1) {
   23                 puts("Usage: %s <name>\n");
   24                 return 1;
   25          }
   26         char buf[] = "hey";
   27
────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────
00:0000│ esp 0xffffd850 —▸ 0xffffd890 —▸ 0xf7fa9000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
01:0004│-014 0xffffd854 —▸ 0xf7fbe66c —▸ 0xf7ffdba0 —▸ 0xf7fbe780 —▸ 0xf7ffda40 ◂— ...
02:0008│-010 0xffffd858 —▸ 0xf7fbeb20 —▸ 0xf7d99cc6 ◂— 'GLIBC_PRIVATE'
03:000c│-00c 0xffffd85c ◂— 0x1
04:0010│-008 0xffffd860 —▸ 0xffffd880 ◂— 0x2
05:0014│-004 0xffffd864 —▸ 0xf7fa9000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
06:0018│ ebp 0xffffd868 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 —▸ 0x56555000 ◂— 0x464c457f
07:001c│+004 0xffffd86c —▸ 0xf7da0519 (__libc_start_call_main+121) ◂— add esp, 0x10
──────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────
 ► 0 0x56556281 main+31
   1 0xf7da0519 __libc_start_call_main+121
   2 0xf7da05f3 __libc_start_main+147
   3 0x565560cb _start+43
─────────────────────────────────────────────────────────────────────────────────────────────────────────

Let's take a look at the previous output that PwnDbg prints. You can see it is seprated into 5 sections: REGISTERS, DISASM, SOURCE, STACK and TRACE. With the original GDB you would have to manually print registers, disassemble code and inspect the stack. Thanks, God, for PwnDbg!

Step

There might be situations when you only want to execute one line of source code, or one machine instruction from your program. This action is called step and can be categorized as follows:

  • step or s (step into) - Continue running your program until control reaches a different source line, then stop it and return control to GDB. If the line you are stepping over represents a function call, this command will step inside it.
  • next or n (step over) - Continue to the next source line in the current stack frame. This is similar to step, but function calls that appear within the line of code are executed without stopping.

There are also equivalent functions for the machine instructions: stepi and nexti.

If you stepped into a function and you want to continue the execution until the function returns, you can use the finish or f (step out) command.

Printing variables and memory

No need to manually print registers anymore, but you still might need to print the content of a variable:

pwndbg> print argc
$1 = 2

The print or p command allows you to specify the format of the output like this (you can find a full list of possible format specifiers here):

pwndbg> p/x $esp
$1 = 0xffffd860

pwndbg> x/2x 0xffffd860
0xffffd860:     0xffffd8a0      0xf7fbe66c

pwndbg> p/d $esp
$2 = -10144

pwndbg> p/s buf
$3 = "hey"

pwndbg> p/x buf
$4 = {0x68, 0x65, 0x79, 0x0}

Reading and modifying memory

You can use the command x (for examine) to examine memory in several formats, independently of your program's data types.

pwndbg> x/nfu addr

n, f, and u are all optional parameters that specify how much memory to display and how to format it.

addr is an expression giving the address where you want to start displaying memory.

  • n - The repeat count is a decimal integer; the default is 1. It specifies how much memory (counting by units u) to display.
  • f - The display format is one of the formats used by print.
  • u - The unit size is any of b (bytes), h (halfwords), w (words)

E.g.: Print 10 words in hexadecimal format, starting from the address of the current stack pointer.

pwndbg> x/10wx $esp
0xffffd850:     0xffffd890      0xf7fbe66c      0xf7fbeb20      0x00796568
0xffffd860:     0xffffd880      0xf7fa9000      0xf7ffd020      0xf7da0519
0xffffd870:     0xffffdabd      0x00000070

In order to change the value of a variable or of a specific memory area, you can use the set command:

pwndbg> set g = 4
pwndbg> set {int}0xffffd890 = 4

Stack info

A backtrace is a summary of how your program got where it is. It shows one line per frame, for many frames, starting with the currently executing frame (frame zero), followed by its caller (frame one), and on up the stack.

backtrace or bt - Print a backtrace of the entire stack: one line per frame for all frames in the stack.

E.g.:

pwndbg> backtrace
#0  copy () at buggy.c:16
#1  0x565562ca in main (argc=2, argv=0xffffd934) at buggy.c:29
#2  0xf7da0519 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

It is also possible to move up or down the stack using the following commands:

  • up n - Move n frames up the stack. For positive numbers n, this advances toward the outermost frame, to higher frame numbers, to frames that have existed longer. n defaults to one.
  • down n - Move n frames down the stack. For positive numbers n, this advances toward the innermost frame, to lower frame numbers, to frames that were created more recently. n defaults to one.

Another useful command for printing information related to the current stack frame is info frame. This command prints a verbose description of the selected stack frame, including:

  • the address of the frame
  • the address of the next frame down (called by this frame)
  • the address of the next frame up (caller of this frame)
  • the language in which the source code corresponding to this frame is written
  • the address of the frame's arguments
  • the address of the frame's local variables
  • the program counter saved in it (the address of execution in the caller frame)
  • which registers were saved in the frame

Exercises

For exercises 02-04 do not quit GDB.

01. [5p] Our test program

Compile the following code:

buggy.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void wanted(int a) {
	if (a == 0xcafebabe) {
		puts("well done, you're cool!");
	} else {
		puts("at least you tried");
	}
}

void copy() {
	char name[12];

	printf("what's ur last name?\n");
	gets(name);

	printf("bye\n");
}

int main(int argc, char **argv) {
  if (argc == 1) {
    puts("Usage: %s <name>\n");
    return 1;
  }
	char buf[] = "hey";

	printf("%s, %s\n", buf, argv[1]);
	copy();

  exit(0);
}

Use this command:

gcc buggy.c -o buggy -fno-stack-protector -m32 -g

02. [5p] Run, break, step

Run the program using GDB, setting the argument Florin.

Set a breakpoint at the beginning of the main function.

Continue execution until you hit the breakpoint.

Try to reach the beginning of the copy function without setting another breakpoint.

Hint: Use step over and step into.

03. [5p] Printing stuff

Remove the existing breakpoint and set a new one at the beginning of the copy function.

Run again the program and continue execution until you hit the breakpoint.

Print the value and the address of name. Print the value again after gets(name) is executed.

04. [5p] ASLR

Start the execution and print the address of buf from the main function, and then repeat. What do you notice?

Check from GDB if ASLR is enabled. What happens and how can you fix it?

Hint: http://visualgdb.com/gdbreference/commands/set_disable-randomization

05. [15p] Address investigation

Restart gdb and run until the beginning of the copy function.

  • At what address is name located?
  • At what address is the saved return address located?
  • How many bytes of input do you need in order to overwrite the return address?

Hint: Display stack info using info frame.

06. [5p] Buffer overflow

We want to overflow the buffer name from the copy() function. Run the program and provide an input so that the program crashes.

You can use run args < <(python3 -c 'import sys; sys.stdout.buffer.write(b"A" * N)') for stdin redirection directly within GDB! ;)

Do not use print in Python for this purpose as some installations (especially on Ubuntu) use a default UTF-8 encoding and auto-correct any unknown binary string to a valid sequence.

You can test the binary output using xxd: python3 -c '… write here …' | xxd -g 1

07. [20p] Call the ''wanted'' function

We want to create an attack which invokes the wanted function. What is the address of this function?

Adjust the input so that the return address is overwritten with the address of the wanted function.

Use objdump -d -M intel buggy to list all the addresses from the binary. Look for the address of the wanted function.

You can see that when using objdump the addresses look weird (short):

000011cd <wanted>:
    11cd:       55                      push   ebp
    11ce:       89 e5                   mov    ebp,esp
    11d0:       53                      push   ebx
    11d1:       83 ec 04                sub    esp,0x4

They aren't actually real addresses, they are offsets counting the number of bytes from the beginning of the file.

This happens because the program was compiled as PIC (position independent code). More details can be found here.

Recompile the program without PIC and PIE using -fno-pic -no-pie options for GCC.

080491a6 <wanted>:
 80491a6:       55                      push   ebp
 80491a7:       89 e5                   mov    ebp,esp
 80491a9:       83 ec 08                sub    esp,0x8

08. [20p] Calling the ''wanted'' function with the correct arguments

The wanted function takes an argument.

Adjust the previous payload so that when calling wanted, the message well done, you're cool! is displayed.

09. [20p] Graceful exit

We can see that even if we call wanted with the correct arguments, the program still crashes.

Let's remove any trace that we've been there. Adjust the previous payload so that the program exits without a segmentation fault.

Can you call another function after wanted?

What would be a great function to call?

Where can you get its address from?

After finding out the function, look for its address using objdump, you might find something there.

Feedback

Please take a minute to fill in the feedback form for this lab.

isc/labs/06.txt · Last modified: 2024/04/09 20:48 by alexandru.mircea98
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0