首页
社区
课程
招聘
[原创]CTF2019_Q1_挖宝
2019-3-16 20:21 2518

[原创]CTF2019_Q1_挖宝

2019-3-16 20:21
2518

最近几天事情比较多,抽几天晚上看了一下,这题最后没有解决(虽然还剩一星期,不过想来下个星期更没时间了,应该不碰了),把踩坑过程写一下。
首先通过字符串判断是个go程序,使用golanghelper恢复符号表:
符号表
首先可以看到四个宝藏位置:(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
图片描述
即:

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)(不排除有办法但是我审错了,但是实在没时间看了)
最后结果:
syscall
可以看到因为后面两个参数无法控制失败了,不过确实劫持了程序流到伪造的syscall_Syscall
虽然失败了,不过想来以后这个可能能用上,先记下来
实际上如果有leak,leak程序加载基址后完全可以伪造整个Scanner结构体造成任意地址写(buf可控),而栈地址已知,后面很简单就可以拿shell,或者leak libc后直接one_gadget来获得shell都可以,不过没想到leak方法才采取syscall方案,感觉预期解是利用go内存分配机制伪造span list指针啥的,不过没想到好的利用技巧,再加上时间上的不允许,只试验了这个方法,赛后来看夜影师傅的操作。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-3-25 13:25 被梅零落编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回