pwnable.kr ELF writeup

0x01 分析

首先ssh上去看看。

elf.py:

1
2
3
4
5
6
7
8
9
10
...
libc = CDLL('libc.so.6')
flag = CDLL('./libflag.so')

...
for i in xrange(25):
sys.stdout.write('addr?:')
sys.stdout.flush()
addr = int(raw_input(), 16)
libc.write(1, c_char_p(addr), 32)

有25次机会,每次可以读进程地址空间的32字节…

再看看gen.py。

gen.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
code = '''
#define _GNU_SOURCE
#include <stdio.h>
'''

for i in xrange( random.randrange(10000) ):
code += 'void not_my_flag{0}(){{printf("not a flag!\\n");}}\n'.format(i)

FLAG = ''
with open('flag', 'r') as f:
FLAG = f.read().strip()

code += 'void yes_ur_flag(){{ char flag[]={{"{0}"}}; puts(flag);}}\n'.format(FLAG)

for i in xrange( random.randrange(10000) ):
code += 'void not_ur_flag{0}(){{printf("not a flag!\\n");}}\n'.format(i)

with open('./libflag.c', 'w') as f:
f.write(code)

os.system('gcc -o ./libflag.so ./libflag.c -fPIC -shared -ldl 2> /dev/null')
...

随机生成N个not_my_flag和N个not_ur_flag函数,N范围0到10000,flag在yes_ur_flag栈帧里,换句话说,flag位于代码段。所以这题只要使用好25次机会,然后读到yes_ur_flag函数就可以拿到flag。

0x02 调试

上去checksec一下python二进制,如下:

1
2
3
4
5
6
7
[*] '/usr/bin/python'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

这个python有点怪,没有开PIE,应该是故意的。如果开了,这题就很难了。

但是ASLR开了的,so库每次会加载到python进程里的随机地址。

这里的libflag.so的not_ur_flag在text段的位置也不总是固定的,elf.py会随机调用gen.py重新生成新的libflag.so,先gdb看看maps吧,重点看libc和libflag。

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
gdb-peda$ set args elf.py
gdb-peda$ aslr on
gdb-peda$ r
...
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x006dd000 r-xp /usr/bin/python2.7
0x008dd000 0x008de000 r--p /usr/bin/python2.7
0x008de000 0x00955000 rw-p /usr/bin/python2.7
0x00955000 0x00978000 rw-p mapped
0x016a4000 0x017c9000 rw-p [heap]
0x00007f2901cdf000 0x00007f2901def000 r-xp /tmp/ll/libflag.so
0x00007f2901def000 0x00007f2901fee000 ---p /tmp/ll/libflag.so
0x00007f2901fee000 0x00007f2901fef000 r--p /tmp/ll/libflag.so
0x00007f2901fef000 0x00007f2901ff0000 rw-p /tmp/ll/libflag.so
0x00007f2901ff0000 0x00007f2902030000 rw-p mapped
...
0x00007f290289c000 0x00007f290289f000 rw-p mapped
...
0x00007f2903569000 0x00007f2903729000 r-xp /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f2903729000 0x00007f2903929000 ---p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f2903929000 0x00007f290392d000 r--p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f290392d000 0x00007f290392f000 rw-p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f290392f000 0x00007f2903933000 rw-p mapped
...
0x00007f2903b4c000 0x00007f2903b50000 rw-p mapped
...
0x00007f2903bb0000 0x00007f2903d66000 rw-p mapped
0x00007f2903d74000 0x00007f2903d75000 rwxp mapped
...
0x00007f2903d77000 0x00007f2903d78000 rw-p mapped
0x00007ffc8ea65000 0x00007ffc8ea86000 rw-p [stack]
0x00007ffc8ebf1000 0x00007ffc8ebf4000 r--p [vvar]
0x00007ffc8ebf4000 0x00007ffc8ebf6000 r-xp [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]

可以看到,libc起始地址为0x00007f2903569000,libflag.so结束地址为0x00007f2901def000,偏移为0x177A000,多次测试发现这个总是不变(为什么libc跟libflag在内存中的偏移总是不变?我不知道,也许是为了so加载到内存中比较整齐?不同的so每次加载的相对位置都是固定的,一家人就要整整齐齐?)

再dump一下内存,把libflag.so加载到的地址:0x00007f2901cdf000 ~ 0x00007f2901def000,dump下来ida静态分析。

可以看到离libflag结束地址不远处是.dynamic段,这里距离结束地址的偏移也是固定的。

所以这题不难了。

0x03 思路

我采取的方法是,泄露libc起始地址,计算出libflag.so结束地址,再计算DT_INIT_ARRAY地址(指向包含一些函数地址的数组):DT_INIT_ARRAY = libflag_end - 0x1b8 - 0x48,然后从.dynamic段读DT_INIT_ARRAY的偏移,这样偏移和地址都拿到了,可以根据偏移读想要的任何内存了(不知道为什么,DT_INIT_ARRAY的偏移地址比实际的大了0x200000)。

接下来读一下字符串表的偏移地址DT_STRTAB和字符串表的总大小DT_STRSZ,这样可以跳转到字符串表的尾部,看看有多少个not_ur_flag_*,然后根据终止函数DT_FINI的偏移,以DT_FINI地址计算yes_ur_flag大概地址:

一个not_ur_flag_*占用19字节,所以最后的地址大概在DT_FINI - (count * 19)这个位置附近。置于为什么是大概,因为libflag.so可能重新生成,指令需要对齐,yes_ur_flag的位置可能会上下变动一点点。

最后把这片内存dump出来,然后print一下手动把flag拿出来即可。

dump出来的内存大概是这样:

Author

lyq1996

Posted on

2021-11-27

Updated on

2022-06-05

Licensed under

Comments