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出来的内存大概是这样: