Why does this program print characters repeatedly, when they only appear in heap memory once?

Viewed 80

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.

1 Answers

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.

Related