[DH] Unsimple_C_calculator

Writeup
2025. 8. 23.

introduction

ACSC에 출제된 Unsimple_C_calculator에 대한 writeup입니다. 해당 writeup에서는 필요했던 리소스만 정리합니다.

Resources

ABI

eh_frame_hdr

struct unw_eh_frame_hdr
{
  unsigned char version;
  unsigned char eh_frame_ptr_enc;
  unsigned char fde_count_enc;
  unsigned char table_enc;
};

frame_hdr_cache

static struct frame_hdr_cache_element
{
  _Unwind_Ptr pc_low;
  _Unwind_Ptr pc_high;
  _Unwind_Ptr load_base;
  const ElfW(Phdr) *p_eh_frame_hdr;
  const ElfW(Phdr) *p_dynamic;
  struct frame_hdr_cache_element *link;
} frame_hdr_cache[FRAME_HDR_CACHE_SIZE];

CIE Payload

11 00 00 00    ; length = 17
00 00 00 00    ; CIE id = 0
01             ; version
7A 50 00       ; "zP\0"
01             ; code_alignment_factor = ULEB128(1)
78             ; data_alignment_factor = SLEB128(-8)
10             ; return_address_register = ULEB128(16)
09             ; augmentation data length = 9 (1 + 8)
00             ; personality pointer encoding = DW_EH_PE_absptr
AA AA AA AA AA AA AA AA  ; personality 함수의 절대주소

Monologue

UW 과정은 현재 frame에 대한 초기화 후 uw_frame_state_for를 통해 한 겹씩 unwinding을 진행한다. 이 부분에 RCE를 하기 위해선 첫번째 initialization을 통과한 후 personality function을 통해 호출하는 경로를 이용해야 한다. 제대로 DWARF Oriented Programming을 하려면 http://savannah.nongnu.org/projects/katana를 이용하면서 작업해야 한다. 그러나 현재는 win 함수만 트리거 하면 되기 때문에 간단하게 구현한다. 물론 그 과정도 상당히 까다롭다.

uw_init_context_1code = uw_frame_state_for (context, &fs);로 시작된다. 이후

  /* Force the frame state to use the known cfa value.  */
  _Unwind_SetSpColumn (context, outer_cfa, &sp_slot);
  fs.regs.cfa_how = CFA_REG_OFFSET;
  fs.regs.cfa_reg = __builtin_dwarf_sp_column ();
  fs.regs.cfa_offset = 0;

  uw_update_context_1 (context, &fs);

  /* If the return address column was saved in a register in the
     initialization context, then we can't see it in the given
     call frame data.  So have the initialization context tell us.  */
  context->ra = __builtin_extract_return_addr (outer_ra);

uw_update_context_1라는 함수를 한 번 더 통과하고 나서야 initialization 단계가 끝난다. 그러나, 이 과정만 통과하면, unwinding loop에서

  1. code = uw_frame_state_for (&cur_context, &fs);에서 _URC_NO_REASON 반환
  2. fs.personality = &win으로 설정 이렇게 끝낼 수 있다.

이를 위해서 총 2회 가짜 frame을 생성해야 한다. 그러나 initialization step에서 그냥 [PIE: PIE + 0xFFFFFFFF]구간을 캡쳐하도록 하면, 계속 해당 frame이 호출되므로 한번만 짜면 해결이다.


search_object 단계 (우리의)목표 (initialization 단계)

  1. ra를 등록해서 이후 단계 함수를 trigger하는 것이 목표.
  2. s->b.sorted = 1
  3. s->b.encoding = 0x50 // aligned 여기까지 binary_search_single_encoding_fdes에 접근
  4. b.sortedfde_vector로 해석되고, count를 1로 설정
  5. u.sort=fde_vectorarray에 순서대로 p1, p2 저장 p1은 시작 주소 (0x0)를 저장한 곳을 가리키고, p2는 길이 (-1)를 저장한 곳을 가리키도록 한다. 이렇게 하면 pc가 무조건 범위 내로 들어와서 통과
  6. 최종적으로 p1이 반환

p1은 FDE로 해석되기에 FakeFDE1을 올려둔 writeable space여야 한다.

이후 _Unwind_Find_FDE에서

bases->tbase = ob->tbase;
bases->dbase = ob->dbase;
encoding = ob->s.b.encoding;
if (ob->s.b.mixed_encoding)  // 해당 없음
    encoding = get_fde_encoding (f);
// func에 f->pc_begin->
read_encoded_value_with_base(encoding, base_from_object(encoding, ob), f->pc_begin, &func);
bases->func = (void *) func;

위처럼 context의 bases를 업데이트하고 f(=fde에 저장)를 반환한다.

read_encoded_value_with_base(e,b,p,t)는 t에 p가 가리키는 값을 저장한다.

이후 uw_frame_state_for로 돌아오면 fs->pc = context->bases.func를 통해 fs->pc애 값이 저장됨을 확인할 수 있다.

아래는 CIE 변조 이전까지의 내용을 정리한 것이다. writeable space는 hdr_cache를 사용하기로 했다. 그곳 또한 DWARF-OP에 사용될 수 있으나 add.1 변조가 불가하여 cache를 사용한 방법대신 위처럼 직접 initialize하는 방법을 사용한다.

Credits

https://defcon.org/images/defcon-20/dc-20-presentations/Branco-Oakley-Bratus/DEFCON-20-Branco-Oakley-Bratus-Dwarf-Oriented-Programming.pdf

https://releases.llvm.org/15.0.0/docs/AMDGPUDwarfExtensionsForHeterogeneousDebugging.html