checksec
# file EasiestPrintf
EasiestPrintf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=61cd88e3d189854473fddf7c0ace6450986e4b02, not stripped
$ checksec -f EasiestPrintf
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 6 EasiestPrintf
可见代码开启了完全只读重定向、栈溢出检查、不可执行栈等。
程序分析
main函数接给出了一个4byte任意地址读取。可以用来获取lib基址。
而后在leave函数中有一个格式化字符串漏洞,执行后直接退出程序。 此处可写入0xA0byte。
80487ab: 83 c4 10 add $0x10,%esp
80487ae: c7 85 50 ff ff ff 00 movl $0x0,-0xb0(%ebp)
80487b5: 00 00 00
80487b8: eb 49 jmp 8048803 <leave+0x92>
80487ba: 8b 85 50 ff ff ff mov -0xb0(%ebp),%eax
80487c0: 8d 95 54 ff ff ff lea -0xac(%ebp),%edx
80487c6: 01 d0 add %edx,%eax
80487c8: 83 ec 04 sub $0x4,%esp
80487cb: 6a 01 push $0x1
80487cd: 50 push %eax
80487ce: 6a 00 push $0x0
80487d0: e8 bb fd ff ff call 8048590 <.plt.got>
80487d5: 83 c4 10 add $0x10,%esp
80487d8: 83 f8 01 cmp $0x1,%eax
80487db: 74 0a je 80487e7 <leave+0x76>
80487dd: 83 ec 0c sub $0xc,%esp
80487e0: 6a ff push $0xffffffff
80487e2: e8 e9 fd ff ff call 80485d0 <.plt.got+0x40>
80487e7: 8d 95 54 ff ff ff lea -0xac(%ebp),%edx
80487ed: 8b 85 50 ff ff ff mov -0xb0(%ebp),%eax
80487f3: 01 d0 add %edx,%eax
80487f5: 0f b6 00 movzbl (%eax),%eax
80487f8: 3c 0a cmp $0xa,%al
80487fa: 74 15 je 8048811 <leave+0xa0>
80487fc: 83 85 50 ff ff ff 01 addl $0x1,-0xb0(%ebp)
8048803: 81 bd 50 ff ff ff 9e cmpl $0x9e,-0xb0(%ebp)
804880a: 00 00 00
804880d: 7e ab jle 80487ba <leave+0x49>
804880f: eb 01 jmp 8048812 <leave+0xa1>
8048811: 90 nop
8048812: 83 ec 0c sub $0xc,%esp
8048815: 8d 85 54 ff ff ff lea -0xac(%ebp),%eax
804881b: 50 push %eax
804881c: e8 77 fd ff ff call 8048598 <.plt.got+0x8>
8048821: 83 c4 10 add $0x10,%esp
8048824: 83 ec 0c sub $0xc,%esp
8048827: 6a 00 push $0x0
8048829: e8 72 fd ff ff call 80485a0 <.plt.got+0x10>
GOT表地址
readelf -r EasiestPrintf
Relocation section '.rel.dyn' at offset 0x4cc contains 18 entries:
Offset Info Type Sym.Value Sym. Name
08049fc4 00001006 R_386_GLOB_DAT 00000000 read@GLIBC_2.0
08049fc8 00001106 R_386_GLOB_DAT 00000000 printf@GLIBC_2.0
08049fcc 00001206 R_386_GLOB_DAT 00000000 _exit@GLIBC_2.0
08049fd0 00000b06 R_386_GLOB_DAT 00000000 sleep@GLIBC_2.0
08049fd4 00000806 R_386_GLOB_DAT 00000000 alarm@GLIBC_2.0
08049fd8 00000e06 R_386_GLOB_DAT 00000000 __stack_chk_fail@GLIBC_2.4
08049fdc 00000c06 R_386_GLOB_DAT 00000000 puts@GLIBC_2.0
08049fe0 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
08049fe4 00000606 R_386_GLOB_DAT 00000000 exit@GLIBC_2.0
08049fe8 00000206 R_386_GLOB_DAT 00000000 open@GLIBC_2.0
08049fec 00000906 R_386_GLOB_DAT 00000000 __libc_start_main@GLIBC_2.0
08049ff0 00000506 R_386_GLOB_DAT 00000000 setvbuf@GLIBC_2.0
08049ff4 00000306 R_386_GLOB_DAT 00000000 memset@GLIBC_2.0
08049ff8 00000406 R_386_GLOB_DAT 00000000 __isoc99_scanf@GLIBC_2.7
08049ffc 00000f06 R_386_GLOB_DAT 00000000 close@GLIBC_2.0
0804a020 00000a05 R_386_COPY 0804a020 stderr@GLIBC_2.0
0804a040 00000d05 R_386_COPY 0804a040 stdin@GLIBC_2.0
0804a044 00001305 R_386_COPY 0804a044 stdout@GLIBC_2.0
测试格式化字符串
root@shell:~/data/CTF/0ctf2017/pwn_01# ./EasiestPrintf
Which address you wanna read:
134520772
0xf76b8b80
Good Bye
%x %x %x %x %x %x
ffe9478e 1 1 a f7795000 12
gdb-peda$ context_stack
[------------------------------------stack-------------------------------------]
0000| 0xffffd2b0 --> 0xffffd2cc --> 0xa7825 ('%x\n')
0004| 0xffffd2b4 --> 0xffffd2ce --> 0xa ('\n')
0008| 0xffffd2b8 --> 0x1
0012| 0xffffd2bc --> 0xf7e834ed (<_IO_do_write+29>: cmp ebx,eax)
0016| 0xffffd2c0 --> 0xf7fcad60 --> 0xfbad2887
0020| 0xffffd2c4 --> 0x80489c0 ("Which address you wanna read:")
0024| 0xffffd2c8 --> 0x2
0028| 0xffffd2cc --> 0xa7825 ('%x\n')
可见,printf格式化参数可控,且修改字符串头部可以控制参数。
配合%p参数,可以实现任意地址写入1~2byte数据。
比赛时想通过修改got表修改exit函数地址实现利用。 但是没注意到程序开启了Full RELRO。 此时GOT表不可修改。而exit函数本身没有可劫持程序执行流的地方,只好放弃。
看到writeup才知道问题出在printf函数。 在printf函数处理完字符串后修改用到的虚表实现劫持。