I wrote a small program to explore out-of-bounds reads vulnerabilities in C to better understand them; this program is intentionally buggy and has vulnerabilities:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PAGE_SIZE 5
void print_welcome(void);
unsigned int get_pages(void);
char* string(char *str);
int main(void)
{
char stack_book[] = "This is the stack book.\n";
char *heap_book = malloc(50);
if(!heap_book)
{
fprintf(stderr, "malloc failed so we have to exit.\n");
return EXIT_FAILURE;
}
char *secret = string("secretinfo");
if(!secret)
{
fprintf(stderr, "Failed to allocate memory for secret\n");
return EXIT_FAILURE;
}
char answer[5] = { 0 };
unsigned int num_pages = 0;
strcpy(heap_book, "These are the contents of the heap book!\n");
print_welcome();
printf("First, do you prefer the stack or heap book (stack/heap)?\n");
fscanf(stdin, "%5s", answer);
printf("Now, how many pages do you want to print?\n");
fscanf(stdin, "%u", &num_pages);
if(strstr(answer, "heap") != NULL)
{
printf("Good choice! The heap book is a fantastic read. Printing...:\n\n");
size_t i;
for( i = 0; i < num_pages*PAGE_SIZE; i)
{
putchar(heap_book[i]);
}
}
else if(strstr(answer, "stack") != NULL)
{
printf("Excelente! The stack book is a splendid read. Printing...:\n\n");
size_t i;
for( i = 0; i < num_pages*PAGE_SIZE; i)
{
putchar(stack_book[i]);
}
}
else
{
printf("Come back when you're ready to read, childrens....\n");
}
putchar('\n');
free(heap_book);
free(secret);
return EXIT_SUCCESS;
}
void print_welcome(void)
{
printf("Welcome to speedreader 1.0!\nWe've loaded the books into memory, read at your own speed!\n");
}
char* string(char *str)
{
if(!str)
{
fprintf(stderr,"Runtime error: Bad pointer provided to string create function.\n");
return NULL;
}
size_t len = 0;
char *ret_ptr = NULL;
len = strlen(str);
ret_ptr = calloc(len 1, 1); //Account for null terminator
if(!ret_ptr)
{
fprintf(stderr, "Runtime error(string): calloc failed.\n");
return NULL;
}
strcpy(ret_ptr, str);
return ret_ptr;
}
When run with input: heap\n100\n
, I get output:
Welcome to speedreader 1.0!
We've loaded the books into memory, read at your own speed!
First, do you prefer the stack or heap book (stack/heap)?
heap
Now, how many pages do you want to print?
100
Good choice! The heap book is a fantastic read. Printing...:
These are the contents of the heap book!
!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!
My question is: Why does this program print !secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!secretinfo!
when secretinfo!
only appears on the heap once:
(gdb)
0x4052a0: "These are the contents of the heap book!\n"
0x4052ca: ""
0x4052cb: ""
0x4052cc: ""
0x4052cd: ""
0x4052ce: ""
0x4052cf: ""
0x4052d0: ""
0x4052d1: ""
0x4052d2: ""
0x4052d3: ""
0x4052d4: ""
0x4052d5: ""
0x4052d6: ""
0x4052d7: ""
0x4052d8: "!"
0x4052da: ""
0x4052db: ""
0x4052dc: ""
0x4052dd: ""
0x4052de: ""
0x4052df: ""
0x4052e0: "secretinfo"
0x4052f1: ""
0x4052f2: ""
0x4052f3: ""
0x4052f4: ""
0x4052f5: ""
0x4052f6: ""
0x4052f7: ""
0x4052f8: "\021\004"
0x4052fb: ""
I do not understand why it repeats secretinfo!
so many times.
CPU info (from 1 core):
vendor_id : AuthenticAMD
cpu family : 23
model : 113
model name : AMD Ryzen 9 3900X 12-Core Processor
stepping : 0
microcode : 0x8701013
cpu MHz : 3792.869
cache size : 512 KB
physical id : 14
siblings : 1
core id : 0
cpu cores : 1
apicid : 14
initial apicid : 14
fpu : yes
fpu_exception : yes
cpuid level : 16
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ssbd ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xsaves clzero arat overflow_recov succor
bogomips : 7585.73
TLB size : 3072 4K pages
clflush size : 64
cache_alignment : 64
address sizes : 43 bits physical, 48 bits virtual
Operating System: Fedora 31
Kernel: Linux 5.8.18-100.fc31.x86_64
Compiler: gcc (GCC) 9.3.1 20200408 (Red Hat 9.3.1-2)
Linker: GNU ld version 2.32-33.fc31
glibc: 2.30-13.fc31
Compiler command: gcc ./oob_read.c -o ./oob_read -g3
Additional info could be provided if necessary.
CodePudding user response:
Since stdout
is line buffered, putchar
doesn't write to the terminal directly; it puts the character into a buffer, which is flushed when a newline is encountered. And the buffer for stdout
happens to be located on the heap following your heap_book
allocation.
So at some point in your copy, you putchar
all the characters of your secretinfo
method. They are now in the output buffer. A little later, heap_book[i]
is within the stdout
buffer itself, so you encounter the copy of secretinfo
that is there. When you putchar
it, you effectively create another copy a little further along in the buffer, and the process repeats.
You can verify this in your debugger. The address of the stdout buffer, on glibc, can be found with p stdout->_IO_buf_base
. In my test it's exactly 160 bytes past heap_book
.